wenpai.org/wp-content/plugins/plat-gp-rosetta-roles/inc/class-plugin.php
2024-07-04 04:24:00 +08:00

612 lines
16 KiB
PHP

<?php
namespace WordPressdotorg\GlotPress\Rosetta_Roles;
use GP;
use GP_Locales;
use Cross_Locale_PTE;
require_once WPMU_PLUGIN_DIR . '/rosetta-roles/cross-locale-pte.php';
class Plugin {
/**
* Cache group.
*
* @var string
*/
public static $cache_group = 'plat-translate';
/**
* Database table for translation editors.
*/
const TRANSLATION_EDITORS_TABLE = 'translate_translation_editors';
/**
* Role of a per project translation editor.
*/
const TRANSLATION_EDITOR_ROLE = 'translation_editor';
/**
* Role of a general translation editor.
*/
const GENERAL_TRANSLATION_EDITOR_ROLE = 'general_translation_editor';
/**
* Role of a locale manager.
*/
const LOCALE_MANAGER_ROLE = 'locale_manager';
/**
* Paths of translation sets the user cannot edit.
*/
const READ_ONLY_TRANSLATION_SETS = [ 'sr/latin' ];
/**
* @var Plugin The singleton instance.
*/
private static $instance;
/**
* Returns always the same instance of this plugin.
*
* @return Plugin
*/
public static function get_instance() {
if ( ! ( self::$instance instanceof Plugin ) ) {
self::$instance = new Plugin();
}
return self::$instance;
}
/**
* Instantiates a new Plugin object.
*/
private function __construct() {
add_action( 'plugins_loaded', array( $this, 'plugins_loaded' ) );
}
/**
* Initializes the plugin.
*/
public function plugins_loaded() {
$GLOBALS['wpdb']->plat_translation_editors = self::TRANSLATION_EDITORS_TABLE;
add_filter( 'gp_pre_can_user', array( $this, 'pre_can_user' ), 9, 2 );
add_action( 'gp_project_created', array( $this, 'project_created' ) );
add_action( 'gp_project_saved', array( $this, 'project_saved' ) );
add_filter( 'gp_translation_set_import_status', array(
'Cross_Locale_PTE',
'gp_translation_set_import_status'
), 9, 3 );
if ( is_admin() ) {
$users = new Admin\Translators();
add_action( 'admin_menu', [ $users, 'register_page' ] );
}
}
/**
* Filter to check if the current user has permissions to approve strings, based
* on a role on the Rosetta site.
*
* @param string $verdict Verdict.
* @param array $args Array of arguments.
*
* @return bool True if user has permissions, false if not.
*/
public function pre_can_user( $verdict, $args ) {
// Logged out users have no permissions.
if ( ! is_user_logged_in() ) {
return false;
}
// 只有管理员可以删除
if ( 'delete' === $args['action'] && ! current_user_can( 'manage_options' ) ) {
return false;
}
// 禁止在根项目上新建项目
if ( 'write' === $args['action'] && empty( $args['parent_project_id'] ) && empty( $args['object_id'] ) ) {
return false;
}
// Administrators on global.wordpress.org are considered global admins in GlotPress.
if ( $this->is_global_administrator( $args['user_id'] ) ) {
return true;
}
if ( current_user_can( 'manage_options' ) ) {
return true;
}
// Grant permissions to Cross-Locale PTEs.
$cross_locale_pte_verdict = Cross_Locale_PTE::gp_pre_can_user( $verdict, $args );
if ( is_bool( $cross_locale_pte_verdict ) ) {
return $cross_locale_pte_verdict;
}
// No permissions for unknown object types.
if ( ! in_array( $args['object_type'], array(
'project|locale|set-slug',
'translation-set',
'translation',
'project'
), true ) ) {
return false;
}
// Get locale and current project ID.
$locale_and_project_id = (object) $this->get_locale_and_project_id( $args['object_type'], $args['object_id'], $args );
if ( ! $locale_and_project_id ) {
return false;
}
// Allow logged in users to submit translations.
if ( 'edit' == $args['action'] && 'translation-set' === $args['object_type'] ) {
return true;
}
// Grant permissions to import plugin/theme translations with status 'waiting'.
if ( 'import-waiting' === $args['action'] ) {
return $this->is_plugin_or_theme_project( $locale_and_project_id->project_id );
}
// The next checks are only for the 'approve' action, no permissions for other actions.
if ( 'approve' !== $args['action'] ) {
return false;
}
// Simple check to see if they're an approver or not.
$role = $this->is_approver_for_locale( $args['user_id'], $locale_and_project_id->locale );
if ( ! $role ) {
return false;
}
// Locale managers are allowed to approve all projects.
if ( self::LOCALE_MANAGER_ROLE === $role ) {
return true;
}
// Grab the list of Projects (or 'all') that the user can approve.
$project_access_list = $this->get_project_id_access_list( $args['user_id'], $locale_and_project_id->locale );
if ( ! $project_access_list ) {
return false;
}
// Short circuit the check if user can approve all projects.
if ( in_array( 'all', $project_access_list ) ) {
return true;
}
// If current project is a parent ID.
if ( in_array( (string) $locale_and_project_id->project_id, $project_access_list, true ) ) {
return true;
}
// A user is allowed to approve sub projects as well.
$parent_project_ids = $this->get_parent_project_ids( $locale_and_project_id->project_id );
if ( array_intersect( $project_access_list, $parent_project_ids ) ) {
return true;
}
return false;
}
/**
* Retrieves all parent project IDs of a project.
*
* @param int $project_id ID of a project.
*
* @return array List of project IDs that are parents of a project.
*/
private function get_parent_project_ids( $project_id ) {
$last_changed = wp_cache_get_last_changed( self::$cache_group );
$cache_key = 'project:' . $project_id . ':parents:' . $last_changed;
$cache = wp_cache_get( $cache_key, self::$cache_group );
if ( false !== $cache ) {
return $cache;
}
$parent_project_ids = [];
$parent_project = GP::$project->get( $project_id );
$parent_project_id = $parent_project->parent_project_id;
while ( $parent_project_id ) {
$parent_project_ids[] = $parent_project_id;
$parent_project = GP::$project->get( $parent_project_id );
$parent_project_id = $parent_project->parent_project_id;
}
wp_cache_set( $cache_key, $parent_project_ids, self::$cache_group );
return $parent_project_ids;
}
/**
* Determines if the project is for a plugin or theme.
*
* @param int $project_id Project ID.
*
* @return bool True, if project is a plugin/theme, false if not.
*/
public function is_plugin_or_theme_project( $project_id ) {
$project = GP::$project->get( $project_id );
if ( ! $project ) {
return false;
}
return ( 0 === strpos( $project->path, 'plugins/' ) || 0 === strpos( $project->path, 'themes/' ) );
}
/**
* Callback for when a project is created.
*/
public function project_created() {
$this->clear_project_cache();
}
/**
* Callback for when a project is saved.
*/
public function project_saved() {
$this->clear_project_cache();
}
/**
* Determines if a given user is a Admin.
*
* @param int $user_id User ID.
*
* @return bool True, if user is an admin, false if not.
*/
public function is_global_administrator( $user_id ) {
return is_super_admin( $user_id );
}
/**
* Determines if a given user is a Translation Approver for a Locale.
*
* @param int $user_id User ID.
* @param string $locale_slug The Locale for which we are checking.
*
* @return false|string Role, if user is an approver, false if not.
*/
public function is_approver_for_locale( $user_id, $locale_slug ) {
static $cache = null;
if ( null === $cache ) {
$cache = array();
}
if ( isset( $cache[ $user_id ][ $locale_slug ] ) ) {
return $cache[ $user_id ][ $locale_slug ];
}
if ( ! isset( $cache[ $user_id ] ) ) {
$cache[ $user_id ] = array();
}
// Get blog prefix of the associated Rosetta site.
if ( ! $blog_prefix = $this->get_blog_prefix( $locale_slug ) ) {
$cache[ $user_id ][ $locale_slug ] = false;
return false;
}
$user = get_user_by( 'id', $user_id );
$cap_key = $blog_prefix . 'capabilities';
if ( ! isset( $user->{$cap_key} ) ) {
$cache[ $user_id ][ $locale_slug ] = false;
return false;
}
$capabilities = $user->{$cap_key};
if ( ! empty( $capabilities[ self::LOCALE_MANAGER_ROLE ] ) ) {
$cache[ $user_id ][ $locale_slug ] = self::LOCALE_MANAGER_ROLE;
return self::LOCALE_MANAGER_ROLE;
} elseif ( ! empty( $capabilities[ self::GENERAL_TRANSLATION_EDITOR_ROLE ] ) ) {
$cache[ $user_id ][ $locale_slug ] = self::GENERAL_TRANSLATION_EDITOR_ROLE;
return self::GENERAL_TRANSLATION_EDITOR_ROLE;
} elseif ( ! empty( $capabilities[ self::TRANSLATION_EDITOR_ROLE ] ) ) {
$cache[ $user_id ][ $locale_slug ] = self::TRANSLATION_EDITOR_ROLE;
return self::TRANSLATION_EDITOR_ROLE;
} else {
$cache[ $user_id ][ $locale_slug ] = false;
return false;
}
}
/**
* Retrieves a list of project ID's which a user can approve for.
*
* This is likely to be incorrrect in the event that the user is a Translation Editor or Global Admin.
* The array item 'all' is special, which means to allow access to all projects.
*
* @param int $user_id User ID.
* @param string $locale_slug The Locale for which we are checking.
* @param bool $include_children Whether to include the children project ID's in the return.
*
* @return array A list of the Project ID's for which the current user can approve translations for.
*/
public function get_project_id_access_list( $user_id, $locale_slug, $include_children = false ) {
global $wpdb;
static $cache = null;
if ( null === $cache ) {
$cache = array();
}
if ( isset( $cache[ $user_id ][ $locale_slug ] ) ) {
$project_access_list = $cache[ $user_id ][ $locale_slug ];
} else {
$project_access_list = $wpdb->get_col( $wpdb->prepare( "
SELECT project_id FROM
{$wpdb->plat_translation_editors}
WHERE user_id = %d AND locale = %s
", $user_id, $locale_slug ) );
if ( ! isset( $cache[ $user_id ] ) ) {
$cache[ $user_id ] = array();
}
$cache[ $user_id ][ $locale_slug ] = $project_access_list;
}
if ( ! $project_access_list ) {
return array();
}
if ( in_array( '0', $project_access_list, true ) ) {
$project_access_list = array( 'all' );
}
// If we don't want the children, or the user has access to all projects.
if ( ! $include_children || in_array( 'all', $project_access_list ) ) {
return $project_access_list;
}
// A user is allowed to approve sub projects as well.
$allowed_sub_project_ids = array();
foreach ( $project_access_list as $project_id ) {
if ( 'all' === $project_id ) {
continue;
}
$sub_project_ids = $this->get_sub_project_ids( $project_id );
if ( $sub_project_ids ) {
$allowed_sub_project_ids = array_merge( $allowed_sub_project_ids, $sub_project_ids );
}
}
// $project_access_list contains parent project IDs, merge them with the sub-project IDs.
$project_access_list = array_merge( $project_access_list, $allowed_sub_project_ids );
return $project_access_list;
}
/**
* Fetches all projects from database.
*
* @return array List of projects with ID and parent ID.
*/
public function get_all_projects() {
global $wpdb;
static $projects = null;
if ( null !== $projects ) {
return $projects;
}
$_projects = $wpdb->get_results( "
SELECT
id, parent_project_id
FROM {$wpdb->gp_projects}
ORDER BY id
" );
$projects = array();
foreach ( $_projects as $project ) {
$project->sub_projects = array();
$projects[ $project->id ] = $project;
}
return $projects;
}
/**
* Returns projects as a hierarchy tree.
*
* @return array The project tree.
*/
public function get_project_tree() {
static $project_tree = null;
if ( null !== $project_tree ) {
return $project_tree;
}
$projects = $this->get_all_projects();
$project_tree = array();
foreach ( $projects as $project_id => $project ) {
$projects[ $project->parent_project_id ]->sub_projects[ $project_id ] = &$projects[ $project_id ];
if ( ! $project->parent_project_id ) {
$project_tree[ $project_id ] = &$projects[ $project_id ];
}
}
return $project_tree;
}
/**
* Returns all sub project IDs of a parent ID.
*
* @param int $project_id Parent ID.
*
* @return array IDs of the sub projects.
*/
public function get_sub_project_ids( $project_id ) {
$last_changed = wp_cache_get_last_changed( self::$cache_group );
$cache_key = 'project:' . $project_id . ':childs:' . $last_changed;
$cache = wp_cache_get( $cache_key, self::$cache_group );
if ( false !== $cache ) {
return $cache;
}
$project_tree = $this->get_project_tree();
$project_branch = $this->get_project_branch( $project_id, $project_tree );
$project_ids = array();
if ( isset( $project_branch->sub_projects ) ) {
$project_ids = Utils::array_keys_multi( $project_branch->sub_projects, 'sub_projects' );
}
wp_cache_set( $cache_key, $project_ids, self::$cache_group );
return $project_ids;
}
/**
* Returns a specific branch of a hierarchy tree.
*
* @param int $project_id Project ID.
* @param array $projects Hierarchy tree of projects.
*
* @return mixed False if project ID doesn't exist, project branch on success.
*/
public function get_project_branch( $project_id, $projects ) {
if ( ! is_array( $projects ) ) {
return false;
}
foreach ( $projects as $project ) {
if ( $project->id == $project_id ) {
return $project;
}
if ( isset( $project->sub_projects ) ) {
$sub = $this->get_project_branch( $project_id, $project->sub_projects );
if ( $sub ) {
return $sub;
}
}
}
return false;
}
/**
* Updates `last_changed` value for the cache group.
*/
public function clear_project_cache() {
wp_cache_set( 'last_changed', microtime(), self::$cache_group );
}
/**
* Extracts project ID and locale slug from object type and ID.
*
* @param string $object_type Current object type.
* @param string $object_id Current object ID.
* @param array $args Optional. Array of additional arguments.
*
* @return array|false Locale and project ID, false on failure.
*/
public function get_locale_and_project_id( $object_type, $object_id, $args = array() ) {
static $set_cache = array();
switch ( $object_type ) {
case 'translation' :
if ( empty( $args['extra']['translation']->translation_set_id ) ) {
break;
}
$translation_set_id = $args['extra']['translation']->translation_set_id;
if ( isset( $set_cache[ $translation_set_id ] ) ) {
$set = $set_cache[ $translation_set_id ];
} else {
$set = GP::$translation_set->get( $args['extra']['translation']->translation_set_id );
$set_cache[ $translation_set_id ] = $set;
}
return array(
'locale' => $set->locale,
'set_slug' => $set->slug,
'project_id' => (int) $set->project_id
);
case 'translation-set' :
if ( isset( $set_cache[ $object_id ] ) ) {
$set = $set_cache[ $object_id ];
} else {
$set = GP::$translation_set->get( $object_id );
$set_cache[ $object_id ] = $set;
}
return array(
'locale' => $set->locale,
'set_slug' => $set->slug,
'project_id' => (int) $set->project_id
);
case 'project|locale|set-slug' :
list( $project_id, $locale, $set_slug ) = explode( '|', $object_id );
return array( 'locale' => $locale, 'set_slug' => $set_slug, 'project_id' => (int) $project_id );
}
return false;
}
/**
* Returns the blog prefix of a locale.
*
* @param string $locale_slug Slug of GlotPress locale.
*
* @return bool|string Blog prefix on success, false on failure.
*/
public function get_blog_prefix( $locale_slug ) {
global $wpdb;
static $ros_locale_assoc;
$gp_locale = GP_Locales::by_slug( $locale_slug );
if ( ! $gp_locale || ! isset( $gp_locale->wp_locale ) ) {
return false;
}
$wp_locale = $gp_locale->wp_locale;
if ( ! isset( $ros_locale_assoc ) ) {
$ros_locale_assoc = $wpdb->get_results( 'SELECT locale, subdomain FROM plat_locales', OBJECT_K );
}
if ( isset( $ros_locale_assoc[ $wp_locale ] ) ) {
$subdomain = $ros_locale_assoc[ $wp_locale ]->subdomain;
} else {
return false;
}
$result = get_sites( [
'network_id' => get_current_network_id(),
'domain' => "$subdomain.wenpai.org",
'path' => '/',
'number' => 1,
] );
$site = array_shift( $result );
if ( $site ) {
return 'plat_' . $site->blog_id . '_';
}
return false;
}
}