wenpai.org/wp-content/mu-plugins/rosetta-roles/cross-locale-pte.php
2024-06-21 23:57:39 +08:00

452 lines
14 KiB
PHP

<?php
/**
* Class used to implement a user that can approve and import translations in all
* translation-sets of a project but cannot overwrite current translations by others.
*/
class Cross_Locale_PTE {
/**
* The special locale name.
*/
const ALL_LOCALES = 'all-locales';
/**
* The capability the user needs to have to manage Cross-Locale PTEs.
*/
const MANAGE_CROSS_LOCALE_PTES_CAP = 'manage_network_users';
/**
* Cache group.
*
* @var string
*/
public static $cache_group = 'plat-translate';
/**
* The admin page hook suffix.
*
* @var string
*/
private static $admin_page;
/**
* The user that is being administered.
*
* @var WP_User
*/
private static $user;
/**
* Init Admin hooks.
*/
public static function init_admin() {
if ( current_user_can( self::MANAGE_CROSS_LOCALE_PTES_CAP ) ) {
add_action( 'admin_menu', array( __CLASS__, 'register_admin_page' ) );
}
}
/**
* Register the Cross-Locale PTE Admin page in wp-admin.
*/
public static function register_admin_page() {
self::$admin_page = add_menu_page(
__( 'Cross-Locale PTE', 'rosetta' ),
__( 'Cross-Locale PTE', 'rosetta' ),
self::MANAGE_CROSS_LOCALE_PTES_CAP,
'cross-locale-pte',
array( __CLASS__, 'render_admin_page' ),
'dashicons-translation',
71 // After Users.
);
add_action( 'load-' . self::$admin_page, array( __CLASS__, 'handle_admin_post' ) );
add_action( 'admin_print_scripts-' . self::$admin_page, array( 'Rosetta_Roles', 'enqueue_scripts' ) );
add_action( 'admin_footer-' . self::$admin_page, array( 'Rosetta_Roles', 'print_js_templates' ) );
add_action( 'admin_print_styles-' . self::$admin_page, array( 'Rosetta_Roles', 'enqueue_styles' ) );
}
/**
* Handle POST requests for the admin page.
*/
public static function handle_admin_post() {
$redirect = menu_page_url( 'cross-locale-pte', false );
if ( ! current_user_can( self::MANAGE_CROSS_LOCALE_PTES_CAP ) ) {
wp_redirect( $redirect );
exit;
}
if ( ! empty( $_REQUEST['user'] ) ) {
check_admin_referer( 'cross-locale-pte', '_nonce_cross-locale-pte' );
self::$user = get_user_by( 'login', $_REQUEST['user'] );
if ( ! self::$user ) {
self::$user = get_user_by( 'email', $_REQUEST['user'] );
}
if ( self::$user ) {
wp_redirect( add_query_arg( array( 'user_id' => self::$user->ID ), $redirect ) );
} else {
wp_redirect( add_query_arg( array( 'error' => 'no-user-found' ), $redirect ) );
}
exit;
}
if ( ! empty( $_REQUEST['user_id'] ) ) {
self::$user = get_user_by( 'id', $_REQUEST['user_id'] );
if ( ! self::$user ) {
wp_redirect( add_query_arg( array( 'error' => 'no-user-found' ), $redirect ) );
exit;
}
}
if ( ! empty( $_REQUEST['action'] ) ) {
switch ( $_REQUEST['action'] ) {
case 'update-cross-locale-pte':
check_admin_referer( 'update-cross-locale-pte_' . self::$user->ID );
return self::update_cross_locale_pte();
}
return self::render_edit_page();
}
}
/**
* Render the Cross-Locale PTE overview page in the admin.
*/
public static function render_admin_page() {
if ( ! empty( $_REQUEST['user_id'] ) ) {
return self::render_edit_page( $_REQUEST['user_id'] );
}
$feedback_message = '';
$cross_locale_pte_users = self::get_all_users();
require __DIR__ . '/views/cross-locale-pte.php';
}
/**
* Update the projects for the Cross-Locale PTE.
*/
public static function update_cross_locale_pte() {
global $wpdb;
$projects = array_filter( array_map( 'strval', explode( ',', $_REQUEST['projects'] ) ) );
$current_projects = self::get_users_projects( self::$user->ID );
$projects_to_remove = array_diff( $current_projects, $projects );
$projects_to_add = array_diff( $projects, $current_projects );
$now = current_time( 'mysql', 1 );
$values_to_add = array();
foreach ( $projects_to_add as $project_id ) {
$values_to_add[] = $wpdb->prepare( '(%d, %d, %s, %s)',
self::$user->ID,
$project_id,
self::ALL_LOCALES,
$now
);
}
if ( $values_to_add ) {
$wpdb->query( "
INSERT INTO {$wpdb->plat_translation_editors}
( `user_id`,`project_id`, `locale`, `date_added` )
VALUES " . implode( ', ', $values_to_add )
);
}
$values_to_remove = array_map( 'intval', $projects_to_remove );
if ( $values_to_remove ) {
$wpdb->query( $wpdb->prepare( "
DELETE FROM {$wpdb->plat_translation_editors}
WHERE `user_id` = %d AND `locale` = %s
AND project_id IN (" . implode( ', ', $values_to_remove ) . ')',
self::$user->ID, self::ALL_LOCALES ) );
}
wp_redirect( add_query_arg( array( 'user_id' => self::$user->ID ), menu_page_url( 'cross-locale-pte', false ) ) );
exit;
}
/**
* Render the page to edit a single Cross-Locale PTE.
*/
public static function render_edit_page() {
$user = self::$user;
$project_access_list = self::get_users_projects( $user->ID );
$last_updated = get_blog_option( SITE_ID_TRANSLATE, 'plat_projects_last_updated' );
wp_localize_script( 'rosetta-roles', '_rosettaProjectsSettings', array(
'l10n' => array(
'removeTranslator' => __( 'Are you sure you wish to completely remove CLPTE status for this user?', 'rosetta' ),
'searchPlaceholder' => esc_attr__( 'Search...', 'rosetta' ),
),
'lastUpdated' => $last_updated,
'accessList' => $project_access_list,
) );
$feedback_message = '';
require __DIR__ . '/views/edit-cross-locale-pte.php';
}
/**
* Retrieves the projects for which a user has cross-locale PTE permissions.
*
* @param int $user_id User ID.
* @return array List of project IDs.
*/
public static function get_users_projects( $user_id ) {
global $wpdb;
$projects = $wpdb->get_col( $wpdb->prepare( "
SELECT project_id FROM
{$wpdb->plat_translation_editors}
WHERE user_id = %d AND locale = %s
", $user_id, self::ALL_LOCALES ) );
return $projects;
}
/**
* Retrieves the projects for which a user has cross-locale PTE permissions.
*
* @return array List of User IDs.
*/
public static function get_all_users() {
global $wpdb;
$rows = $wpdb->get_results( $wpdb->prepare( "
SELECT te.user_id, te.project_id, p.name AS project_name FROM
{$wpdb->plat_translation_editors} te
JOIN {$wpdb->gp_projects} p ON te.project_id = p.id
WHERE te.locale = %s
", self::ALL_LOCALES ) );
$user_ids = array();
foreach ( $rows as $row ) {
if ( ! isset( $user_ids[ $row->user_id ] ) ) {
$user = get_user_by( 'id', $row->user_id );
if ( ! $user ) {
continue;
}
$row->user_login = $user->user_login;
$row->email = $user->user_email;
$row->display_name = $user->display_name;
$row->projects = array( $row->project_id => $row->project_name );
$user_ids[ $row->user_id ] = $row;
} else {
$user_ids[ $row->user_id ]->projects[ $row->project_id ] = $row->project_name;
}
}
return $user_ids;
}
/**
* Check for the Cross-Locale PTE permission for the project.
*
* @param WP_User $user The user.
* @param int $project_id The Project ID.
* @return string|bool The verdict.
*/
public static function user_has_cross_locale_permission( $user, $project_id ) {
static $cache = null;
if ( null === $cache ) {
$cache = array();
}
$user_id = intval( $user->ID );
$project_id = intval( $project_id );
if ( isset( $cache[ $user_id ][ $project_id ] ) ) {
return $cache[ $user_id ][ $project_id ];
}
if ( ! isset( $cache[ $user_id ] ) ) {
$cache[ $user_id ] = array();
}
global $wpdb;
$result = $wpdb->get_col( $wpdb->prepare( "
SELECT te.user_id FROM
{$wpdb->plat_translation_editors} te
JOIN {$wpdb->gp_projects} p ON ( te.project_id = p.id OR te.project_id = p.parent_project_id )
WHERE te.user_id = %d AND p.id = %d AND te.locale = %s
", $user_id, $project_id, self::ALL_LOCALES ) );
if ( $result && intval( $result[0] ) === $user_id ) {
return $cache[ $user_id ][ $project_id ] = true;
}
return $cache[ $user_id ][ $project_id ] = false;
}
/**
* Enforce not-overwriting current translation by others while importing.
*
* @param string $status The desired status.
* @param GP_Translation $new_translation The new translation.
* @param GP_Translation $old_translation The old translation.
* @return string The new status.
*/
public static function gp_translation_set_import_status( $status, $new_translation, $old_translation ) {
if ( ! isset( $old_translation->translation_set_id ) ) {
return $status;
}
if ( 'current' !== $old_translation->translation_status ) {
return $status;
}
if ( GP::$permission->current_user_can( 'cross-pte', 'translation-set', $old_translation->translation_set_id ) ) {
// Set to waiting if a current translation exists that was approved by another user.
$translation_owner_id = $old_translation->user_id_last_modified ?: $old_translation->user_id;
if ( intval( $translation_owner_id ) !== intval( get_current_user_id() ) ) {
return 'waiting';
}
}
return $status;
}
/**
* The GlotPress filter for Cross-Locale PTE.
*
* A Cross-Locale PTE is defined through an entry in the permission table 'cross-pte' with the
* object_id referring to a project id.
* A user with this permission will have 'approve' rights for all translation-sets within this
* project. Usually having approval rights for a translation-set also means that the user has
* approval rights for all translations, but not a Cross-Locale PTE:
* If a current translation exists by another user then overwriting (through UI or import) is not
* possible.
*
* @param string|bool $verdict The verdict from an earlier filter.
* @param array $args Arguments that describe the object to judge for.
* @return string|bool The verdict for the object.
*/
public static function gp_pre_can_user( $verdict, $args ) {
if ( 'cross-pte' === $args['action'] ) {
$verdict = self::gp_pre_can_user_cross_pte( $verdict, $args );
if ( is_bool( $verdict ) ) {
return $verdict;
}
}
if ( 'approve' === $args['action'] ) {
if ( 'translation' === $args['object_type'] ) {
$verdict = self::gp_pre_can_user_approve_translation( $verdict, $args );
} elseif ( 'translation-set' === $args['object_type'] ) {
$verdict = self::gp_pre_can_user_approve_translation_set( $verdict, $args );
}
}
return $verdict;
}
/**
* A GlotPress sub-filter for the permission 'cross-lte'.
*
* @param string|bool $verdict The verdict from an earlier filter.
* @param array $args Arguments that describe the object to judge for.
* @return string|bool The verdict for the object.
*/
public static function gp_pre_can_user_cross_pte( $verdict, $args ) {
if ( GP::$permission->user_can( $args['user'], 'admin' ) ) {
// Admins shouldn't have this because it will end up restricting them.
return false;
}
if ( 'translation-set' === $args['object_type'] ) {
if ( isset( $args['extra']['set']->id ) && intval( $args['extra']['set']->id ) === intval( $args['object_id'] ) ) {
$set = $args['extra']['set'];
} else {
$set = GP::$translation_set->get( $args['object_id'] );
}
// Allow on all translation-sets within the project.
if ( $set ) {
return GP::$permission->user_can( $args['user'], 'cross-pte', 'project', $set->project_id );
}
} elseif ( 'project' === $args['object_type'] ) {
return self::user_has_cross_locale_permission( $args['user'], $args['object_id'] );
}
return $verdict;
}
/**
* A GlotPress sub-filter for the permission 'approve' and object 'translation'.
*
* @param string|bool $verdict The verdict from an earlier filter.
* @param array $args Arguments that describe the object to judge for.
* @return string|bool The verdict for the object.
*/
public static function gp_pre_can_user_approve_translation( $verdict, $args ) {
if ( isset( $args['extra']['translation']->translation_set_id ) && intval( $args['extra']['translation']->id ) === intval( $args['object_id'] ) ) {
$translation = $args['extra']['translation'];
} else {
$translation = GP::$translation->get( $args['object_id'] );
}
if ( ! $translation ) {
return $verdict;
}
static $current_translation_by_user;
$cache_key = $args['user']->ID . '_' . $translation->original_id;
if ( isset( $current_translation_by_user[ $cache_key ] ) ) {
return $current_translation_by_user[ $cache_key ];
}
if ( GP::$permission->user_can( $args['user'], 'cross-pte', 'translation-set', $translation->translation_set_id ) ) {
$current_translation = GP::$translation->find_one( array( 'translation_set_id' => $translation->translation_set_id, 'original_id' => $translation->original_id, 'status' => 'current' ) );
if ( $current_translation ) {
$translation_owner_id = $current_translation->user_id_last_modified ?: $current_translation->user_id;
if ( intval( $translation_owner_id ) !== $args['user']->ID ) {
// Current translation was approved by someone else. Disallow setting to current.
return $current_translation_by_user[ $cache_key ] = false;
}
}
// No current translation exists or it was translated by me: allow.
return $current_translation_by_user[ $cache_key ] = true;
}
// Allows usage of the re-implementation below.
if ( GP::$permission->user_can( $args['user'], 'approve', 'translation-set', $translation->translation_set_id ) ) {
return true;
}
return $verdict;
}
/**
* A GlotPress sub-filter for the permission 'approve' and object 'translation-set'.
*
* @param string|bool $verdict The verdict from an earlier filter.
* @param array $args Arguments that describe the object to judge for.
* @return string|bool The verdict for the object.
*/
public static function gp_pre_can_user_approve_translation_set( $verdict, $args ) {
// Re-implementation of gp_route_translation_set_permissions_to_validator_permissions().
if ( isset( $args['extra']['set']->id ) && intval( $args['extra']['set']->id ) === intval( $args['object_id'] ) ) {
$set = $args['extra']['set'];
} else {
$set = GP::$translation_set->get( $args['object_id'] );
}
if ( $set ) {
if ( GP::$permission->user_can( $args['user'], 'cross-pte', 'project', $set->project_id ) ) {
return true;
}
return GP::$permission->user_can( $args['user'], 'approve', GP::$validator_permission->object_type, GP::$validator_permission->object_id( $set->project_id, $set->locale, $set->slug ) );
}
return $verdict;
}
}