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

988 lines
30 KiB
PHP
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<?php
/**
* Plugin Name: Rosetta Roles
* Plugin URI: https://wordpress.org/
* Description: WordPress interface for managing roles.
* Author: ocean90
* Version: 1.2
*/
if ( ! class_exists( 'GP_Locales' ) ) {
require_once WP_CONTENT_DIR . '/plugins/glotpress/locales/locales.php';
}
require __DIR__ . '/cross-locale-pte.php';
class Rosetta_Roles {
/**
* 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';
/**
* Capabaility to promote translation editor.
*/
const MANAGE_TRANSLATION_EDITORS_CAP = 'manage_translation_editors';
/**
* Holds the GlotPress locale of current site.
*
* @var GP_Locale
*/
private $gp_locale = null;
public $translation_editors_page;
/**
* Constructor.
*/
public function __construct() {
global $wpdb;
add_action( 'plugins_loaded', array( $this, 'plugins_loaded' ) );
$wpdb->plat_translation_editors = self::TRANSLATION_EDITORS_TABLE;
}
/**
* Attaches hooks once plugins are loaded.
*/
public function plugins_loaded() {
$locale = get_locale();
$gp_locale = GP_Locales::by_field( 'wp_locale', $locale );
if ( ! $gp_locale ) {
return;
}
$this->gp_locale = $gp_locale;
$this->register_roles();
add_action( 'admin_menu', array( $this, 'register_translation_editors_page' ) );
add_filter( 'set-screen-option', array( $this, 'save_custom_screen_options' ), 10, 3 );
add_action( 'after_setup_theme', array( $this, 'register_resources_nav_menu' ) );
add_action( 'translation_editor_added', array( $this, 'update_plat_profile_badge' ) );
add_action( 'translation_editor_removed', array( $this, 'update_plat_profile_badge' ) );
add_action( 'translation_editor_added', array( $this, 'send_email_notification' ), 10, 2 );
add_action( 'translation_editor_updated', array( $this, 'send_email_notification' ), 10, 2 );
add_action( 'wp_ajax_rosetta-get-projects', array( $this, 'ajax_rosetta_get_projects' ) );
// 取消加载 Cross_Locale_PTE因为我们用不到
//Cross_Locale_PTE::init_admin();
}
public function register_roles() {
add_role(
self::TRANSLATION_EDITOR_ROLE,
__( 'Translation Editor', 'rosetta' ),
array(
'read' => true,
'edit_posts' => true,
'upload_files' => true,
)
);
add_role(
self::GENERAL_TRANSLATION_EDITOR_ROLE,
__( 'General Translation Editor', 'rosetta' ),
array(
'read' => true,
'edit_posts' => true,
'upload_files' => true,
)
);
}
/**
* Registers a nav menu for storing resources for translation editors.
*/
public function register_resources_nav_menu() {
register_nav_menu( 'rosetta_translation_editor_resources', __( 'Resources for translation editors', 'rosetta' ) );
}
/**
* Registers page for managing translation editors.
*/
public function register_translation_editors_page() {
$this->translation_editors_page = add_menu_page(
__( 'Translation Editors', 'rosetta' ),
__( 'Translation Editors', 'rosetta' ),
self::MANAGE_TRANSLATION_EDITORS_CAP,
'translation-editors',
array( $this, 'render_translation_editors_page' ),
'dashicons-translation',
71 // After Users
);
add_action( 'load-' . $this->translation_editors_page, array( $this, 'load_translation_editors_page' ) );
add_action( 'load-' . $this->translation_editors_page, array( $this, 'register_screen_options' ) );
add_action( 'admin_print_scripts-' . $this->translation_editors_page, array( __CLASS__, 'enqueue_scripts' ) );
add_action( 'admin_footer-' . $this->translation_editors_page, array( __CLASS__, 'print_js_templates' ) );
add_action( 'admin_print_styles-' . $this->translation_editors_page, array( __CLASS__, 'enqueue_styles' ) );
}
/**
* Enqueues scripts.
*/
public static function enqueue_scripts() {
wp_enqueue_script( 'string_score', plugins_url( '/js/string_score.min.js', __FILE__ ), array(), '0.1.22', true );
wp_enqueue_script( 'rosetta-roles', plugins_url( '/js/rosetta-roles.js', __FILE__ ), array( 'jquery', 'wp-backbone', 'string_score' ), 12, true );
}
/**
* Enqueues styles.
*/
public static function enqueue_styles() {
$suffix = is_rtl() ? '-rtl' : '';
wp_enqueue_style( 'rosetta-roles', plugins_url( "/css/rosetta-roles$suffix.css", __FILE__ ), array(), '4' );
}
/**
* Prints JavaScript templates.
*/
public static function print_js_templates() {
?>
<script id="tmpl-project-checkbox" type="text/html">
<# if ( ! data.checkedSubProjects ) {
#>
<label>
<input type="checkbox" class="input-checkbox"
<#
if ( data.checked ) {
#> checked="checked"<#
}
#>
/>
{{data.name}}
<span class="project-slug">({{data.slug}})</span>
</label>
<# } else { #>
<label>
<input type="radio" class="input-radio" checked="checked" /> {{data.name}}
<span class="project-slug">({{data.slug}})</span>
</label>
<# } #>
</script>
<?php
}
/**
* Registers a 'per_page' screen option for the list table.
*/
public function register_screen_options() {
$option = 'per_page';
$args = array(
'default' => 10,
'option' => 'translation_editors_per_page',
);
add_screen_option( $option, $args );
}
/**
* Adds the 'per_page' screen option to the whitelist so it gets saved.
*
* @param bool|int $new_value Screen option value. Default false to skip.
* @param string $option The option name.
* @param int $value The number of rows to use.
* @return bool|int New screen option value.
*/
public function save_custom_screen_options( $new_value, $option, $value ) {
if ( 'translation_editors_per_page' !== $option ) {
return $new_value;
}
$value = (int) $value;
if ( $value < 1 || $value > 999 ) {
return $new_value;
}
return $value;
}
/**
* Loads either the overview or the edit handler.
*/
public function load_translation_editors_page() {
if ( ! empty( $_REQUEST['user_id'] ) ) {
$this->load_edit_translation_editor( $_REQUEST['user_id'] );
} else {
$this->load_translation_editors();
}
}
/**
* Renders either the overview or the edit view.
*/
public function render_translation_editors_page() {
if ( ! empty( $_REQUEST['user_id'] ) ) {
$this->render_edit_translation_editor( $_REQUEST['user_id'] );
} else {
$this->render_translation_editors();
}
}
/**
* Handler for overview page.
*/
private function load_translation_editors() {
$list_table = $this->get_translation_editors_list_table();
$action = $list_table->current_action();
$redirect = menu_page_url( 'translation-editors', false );
if ( $action ) {
switch ( $action ) {
case 'add-translation-editor':
check_admin_referer( 'add-translation-editor', '_nonce_add-translation-editor' );
if ( ! current_user_can( self::MANAGE_TRANSLATION_EDITORS_CAP ) ) {
wp_redirect( $redirect );
exit;
}
$user_details = null;
$user = wp_unslash( $_REQUEST['user'] );
if ( false !== strpos( $user, '@' ) ) {
$user_details = get_user_by( 'email', $user );
} elseif ( is_numeric( $user ) ) {
$user_details = get_user_by( 'id', $user );
} else {
$user_details = get_user_by( 'login', $user );
if ( ! $user_details ) {
$user_details = get_user_by( 'slug', $user );
}
}
if ( ! $user_details ) {
wp_redirect( add_query_arg( array( 'error' => 'no-user-found' ), $redirect ) );
exit;
}
if ( ! is_user_member_of_blog( $user_details->ID ) ) {
$added = add_existing_user_to_blog( array( 'user_id' => $user_details->ID, 'role' => 'subscriber' ) );
if ( ! $added || is_wp_error( $added ) ) {
wp_redirect( add_query_arg( array( 'error' => 'not-added-to-site' ), $redirect ) );
exit;
}
// Refresh user data
$user_details = get_user_by( 'id', $user_details->ID );
}
if ( in_array( self::TRANSLATION_EDITOR_ROLE, $user_details->roles ) || in_array( self::GENERAL_TRANSLATION_EDITOR_ROLE, $user_details->roles ) ) {
wp_redirect( add_query_arg( array( 'error' => 'user-exists' ), $redirect ) );
exit;
}
$projects = empty( $_REQUEST['projects'] ) ? '' : $_REQUEST['projects'];
if ( 'custom' === $projects ) {
$this->update_translation_editor( $user_details );
$redirect = add_query_arg( 'user_id', $user_details->ID, $redirect );
wp_redirect( add_query_arg( array( 'update' => 'user-added-custom-projects' ), $redirect ) );
exit;
} else {
$this->update_translation_editor( $user_details, array( 'all' ) );
wp_redirect( add_query_arg( array( 'update' => 'user-added' ), $redirect ) );
exit;
}
case 'remove-translation-editors':
check_admin_referer( 'bulk-translation-editors' );
if ( ! current_user_can( self::MANAGE_TRANSLATION_EDITORS_CAP ) ) {
wp_redirect( $redirect );
exit;
}
if ( empty( $_REQUEST['translation-editors'] ) ) {
wp_redirect( $redirect );
exit;
}
$count = 0;
$user_ids = array_map( 'intval', (array) $_REQUEST['translation-editors'] );
foreach ( $user_ids as $user_id ) {
$this->remove_translation_editor( $user_id );
$count++;
}
wp_redirect( add_query_arg( array( 'update' => 'user-removed', 'count' => $count ), $redirect ) );
exit;
case 'remove-translation-editor':
check_admin_referer( 'remove-translation-editor' );
if ( ! current_user_can( self::MANAGE_TRANSLATION_EDITORS_CAP ) ) {
wp_redirect( $redirect );
exit;
}
if ( empty( $_REQUEST['translation-editor'] ) ) {
wp_redirect( $redirect );
exit;
}
$user_id = (int) $_REQUEST['translation-editor'];
$this->remove_translation_editor( $user_id );
wp_redirect( add_query_arg( array( 'update' => 'user-removed' ), $redirect ) );
exit;
}
}
}
/**
* Handler for editing a translation editor.
*
* @param int $user_id User ID of a translation editor.
*/
private function load_edit_translation_editor( $user_id ) {
$redirect = menu_page_url( 'translation-editors', false );
if ( ! current_user_can( self::MANAGE_TRANSLATION_EDITORS_CAP ) ) {
wp_redirect( $redirect );
exit;
}
$user_details = get_user_by( 'id', $user_id );
if ( ! $user_details ) {
wp_redirect( add_query_arg( array( 'error' => 'no-user-found' ), $redirect ) );
exit;
}
if ( ! is_user_member_of_blog( $user_details->ID ) ) {
wp_redirect( add_query_arg( array( 'error' => 'not-a-member' ), $redirect ) );
exit;
}
if ( ! in_array( self::TRANSLATION_EDITOR_ROLE, $user_details->roles ) && ! in_array( self::GENERAL_TRANSLATION_EDITOR_ROLE, $user_details->roles ) ) {
wp_redirect( add_query_arg( array( 'error' => 'user-cannot' ), $redirect ) );
exit;
}
$action = empty( $_REQUEST['action'] ) ? '' : $_REQUEST['action'];
switch ( $action ) {
case 'update-translation-editor':
check_admin_referer( 'update-translation-editor_' . $user_details->ID );
$redirect = add_query_arg( 'user_id', $user_details->ID, $redirect );
$all_projects = $this->get_translate_projects();
$all_projects = wp_list_pluck( $all_projects, 'id' );
$all_projects = array_map( 'intval', $all_projects );
$projects = explode( ',', $_REQUEST['projects'] );
if ( in_array( 'all', $projects, true ) ) {
$this->update_translation_editor( $user_details, array( 'all' ) );
} else {
$projects = array_map( 'intval', $projects );
$projects = array_values( array_intersect( $all_projects, $projects ) );
$this->update_translation_editor( $user_details, $projects );
}
wp_redirect( add_query_arg( array( 'update' => 'user-updated' ), $redirect ) );
exit;
}
}
/**
* Removes a translation editor.
*
* @param int|WP_User $user User ID or object.
* @return bool True on success, false on failure.
*/
private function remove_translation_editor( $user ) {
global $wpdb;
if ( ! $user instanceof WP_User ) {
$user = get_user_by( 'id', $user );
}
if ( ! $user->exists() ) {
return false;
}
if ( in_array( self::TRANSLATION_EDITOR_ROLE, $user->roles ) ) {
$user->remove_role( self::TRANSLATION_EDITOR_ROLE );
}
if ( in_array( self::GENERAL_TRANSLATION_EDITOR_ROLE, $user->roles ) ) {
$user->remove_role( self::GENERAL_TRANSLATION_EDITOR_ROLE );
}
$wpdb->query( $wpdb->prepare( "
DELETE FROM {$wpdb->plat_translation_editors}
WHERE `user_id` = %d AND `locale` = %s
", $user->ID, $this->gp_locale->slug ) );
do_action( 'translation_editor_removed', $user );
return true;
}
/**
* Creates or updates a translation editor.
*
* @param int|WP_User $user User ID or object.
* @param array $projects The projects to which the user should get assigned.
* Pass `array( 'all' )` to make their a general translation
* editor.
* @return bool True on success, false on failure.
*/
private function update_translation_editor( $user, $projects = array() ) {
global $wpdb;
if ( ! $user instanceof WP_User ) {
$user = get_user_by( 'id', $user );
}
if ( ! $user->exists() ) {
return false;
}
$update = in_array( self::TRANSLATION_EDITOR_ROLE, $user->roles ) || in_array( self::GENERAL_TRANSLATION_EDITOR_ROLE, $user->roles );
$projects = array_map( 'strval', $projects );
$current_projects = $this->get_users_projects( $user->ID );
$projects_to_add = $projects_to_remove = array();
if ( in_array( 'all', $projects, true ) ) {
$projects_to_remove = array_diff( $current_projects, array( '0' ) );
if ( ! in_array( '0', $current_projects, true ) ) {
$projects_to_add[] = '0';
}
if ( in_array( self::TRANSLATION_EDITOR_ROLE, $user->roles ) ) {
$user->remove_role( self::TRANSLATION_EDITOR_ROLE );
}
if ( ! in_array( self::GENERAL_TRANSLATION_EDITOR_ROLE, $user->roles ) ) {
$user->add_role( self::GENERAL_TRANSLATION_EDITOR_ROLE );
}
} else {
$projects_to_remove = array_diff( $current_projects, $projects );
$projects_to_add = array_diff( $projects, $current_projects );
if ( in_array( self::GENERAL_TRANSLATION_EDITOR_ROLE, $user->roles ) ) {
$user->remove_role( self::GENERAL_TRANSLATION_EDITOR_ROLE );
}
if ( ! in_array( self::TRANSLATION_EDITOR_ROLE, $user->roles ) ) {
$user->add_role( self::TRANSLATION_EDITOR_ROLE );
}
}
$values_to_add = array();
foreach ( $projects_to_add as $project_id ) {
$values_to_add[] = $wpdb->prepare( '(%d, %d, %s, %s, %s)',
$user->ID,
$project_id,
$this->gp_locale->slug,
'default',
current_time( 'mysql', 1 )
);
}
if ( $values_to_add ) {
$wpdb->query( "
INSERT INTO {$wpdb->plat_translation_editors}
(`user_id`,`project_id`, `locale`, `locale_slug`, `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 ) . ")
", $user->ID, $this->gp_locale->slug ) );
}
if ( $update ) {
do_action( 'translation_editor_updated', $user, $projects_to_add, $projects_to_remove );
} else {
do_action( 'translation_editor_added', $user, $projects_to_add, $projects_to_remove );
}
return true;
}
/**
* Handles the update of the translation editor badges on
* profiles.wordpress.org.
*
* @param \WP_User $user The user object of the translation editor.
*/
public function update_plat_profile_badge( $user ) {
global $wpdb;
$action = 'translation_editor_added' === current_filter() ? 'add' : 'remove';
// Remove badge only when all roles have been removed.
if ( 'remove' === $action ) {
$count = (int) $wpdb->get_var(
$wpdb->prepare(
"SELECT COUNT(*) FROM {$wpdb->plat_translation_editors}
WHERE `user_id` = %d AND `locale` != 'all-locales'",
$user->ID
)
);
if ( 0 !== $count ) {
return;
}
}
}
/**
* Sends an email to the new translation editor.
*
* @param \WP_User $user The user object of the translation editor.
* @param array $projects_added List of project IDs.
*/
public function send_email_notification( $user, $projects_added ) {
// Don't send an email if no new projects have been added.
if ( ! $projects_added ) {
return;
}
$to = $user->user_email;
$subject = __( 'You have been added to a WordPress project as a translation editor', 'rosetta' );
if ( [ '0' ] === $projects_added ) {
// General Translation Editor.
/* translators: Do not translate the placeholders USERNAME, LOCALENAME, LOCALEURL, CURRENTUSERNAME. */
$message = __(
'Howdy ###USERNAME###,
We are happy to inform you that you have been successfully added as a General Translation Editor of WordPress for ###LOCALENAME###.
As a General Translation Editor you have access to submit and approve translations for all projects available at ###LOCALEURL###.
Alongside WordPress itself, its good to prioritize translating the projects that ship with it first The default themes like Twenty Seventeen or Twenty Sixteen, and Akismet.
Please get to know how the team works by reading the Translators Handbook https://make.wordpress.org/polyglots/handbook/, a good place to start is the General Expectations page.
As a General Translation Editor for the locale, we request that you fill out your WordPress.org profile (https://profiles.wordpress.org/profile/), register on Slack and provide a way for translation contributors to reach you.
We also ask all WordPress General Translation Editors to subscribe for notifications for their locales, you can find the notification subscription settings in your profile settings (https://profiles.wordpress.org/profile/notifications/).
The Polyglots team connects on Slack. Check out https://make.wordpress.org/meetings/#polyglots for upcoming team meetings. Wed love to have you there if you can make it. Register from https://chat.wordpress.org.
If you have any questions about the processes or need any help, reach the team on Slack or on https://make.wordpress.org/polyglots/.
The access has been granted by ###CURRENTUSERNAME###.
Welcome to the WordPress Polyglots team and happy translating!',
'rosetta'
);
$message = str_replace(
[
'###USERNAME###',
'###LOCALENAME###',
'###LOCALEURL###',
'###CURRENTUSERNAME###',
],
[
$user->user_login,
'#' . $this->gp_locale->wp_locale . ' (' . $this->gp_locale->native_name . ')',
'https://translate.wenpai.org/locale/' . $this->gp_locale->slug,
wp_get_current_user()->user_login,
],
$message
);
} else {
// Project Translation Editor.
/* translators: Do not translate the placeholders USERNAME, LOCALENAME, PROJECTLIST, RESOURCESLIST, CURRENTUSERNAME. */
$message = __(
'Howdy ###USERNAME###,
We are happy to inform you that you have been successfully added as a Project Translation Editor for ###LOCALENAME### for the following projects:
###PROJECTLIST###
You have been added to these projects either by your own request or by the request of the project author.
Before translating, please get to know how the team works by reading the Translators Handbook https://make.wordpress.org/polyglots/handbook/, a good place to start is the General Expectations page.
Your local translation team can be found on https://make.wordpress.org/polyglots/teams/. Make sure you get familiar with the documentation about translating in your language that other contributors from your team have prepared.
###RESOURCESLIST###
The Polyglots team connects on Slack. Check out https://make.wordpress.org/meetings/#polyglots for upcoming team meetings. Wed love to have you there if you can make it. Register from https://chat.wordpress.org/.
If you have any questions about the processes or need any help, reach the team on Slack or on https://make.wordpress.org/polyglots/.
The access has been granted by ###CURRENTUSERNAME###.
Welcome to the WordPress Polyglots team and happy translating.',
'rosetta'
);
$projects = $this->get_translate_projects();
$project_tree = $this->get_project_tree( $projects, 0, 1 );
$project_list = [];
foreach ( $projects_added as $project_id ) {
if ( $projects[ $project_id ] ) {
$parent = $this->get_parent_project( $project_tree, $project_id );
if ( $parent->id != $project_id ) {
$name = sprintf(
/* translators: 1: Parent project name, 2: Child project name */
__( '%1$s &rarr; %2$s', 'rosetta' ),
$parent->name,
$projects[ $project_id ]->name
);
} else {
$name = $projects[ $project_id ]->name;
}
$name = sprintf(
'%s: %s',
$name,
esc_url( 'https://translate.wenpai.org/projects/' . $projects[ $project_id ]->path )
);
$project_list[] = html_entity_decode( $name, ENT_QUOTES, get_bloginfo( 'charset' ) );
}
}
$resources_list = '';
if ( has_nav_menu( 'rosetta_translation_editor_resources' ) ) {
$resources_list = (string) wp_nav_menu( [
'fallback_cb' => '__return_false',
'theme_location' => 'rosetta_translation_editor_resources',
'container' => false,
'echo' => false,
'depth' => 1,
'items_wrap' => '%3$s',
// Custom walker that returns plain text links.
'walker' => new class() extends Walker_Nav_Menu {
public function start_lvl( &$output, $depth = 0, $args = array() ) {
$output .= "\n";
}
public function end_lvl( &$output, $depth = 0, $args = array() ) {
$output .= "\n";
}
public function start_el( &$output, $item, $depth = 0, $args = array(), $id = 0 ) {
$href = ! empty( $item->url ) ? $item->url : '';
$title = apply_filters( 'the_title', $item->title, $item->ID );
$title = apply_filters( 'nav_menu_item_title', $title, $item, $args, $depth );
$item_output = $title;
if ( $href ) {
$item_output .= ': ' . esc_url( $href );
}
$output .= apply_filters( 'walker_nav_menu_start_el', $item_output, $item, $depth, $args );
}
public function end_el( &$output, $item, $depth = 0, $args = array() ) {
$output .= "\n";
}
},
] );
}
$message = str_replace(
[
'###USERNAME###',
'###LOCALENAME###',
'###PROJECTLIST###',
'###RESOURCESLIST###',
'###CURRENTUSERNAME###',
],
[
$user->user_login,
'#' . $this->gp_locale->wp_locale . ' (' . $this->gp_locale->native_name . ')',
implode( "\n", $project_list ),
$resources_list,
wp_get_current_user()->user_login,
],
$message
);
}
$headers = "From: \"WordPress Polyglots\" <donotreply@wordpress.org>\n" . 'Content-Type: text/plain; charset="' . get_option( 'blog_charset' ) . "\"\n";
wp_mail( $to, $subject, $message, $headers );
}
/**
* Retrieves the assigned projects of a user
*
* @param int $user_id User ID.
* @return array List of project IDs.
*/
public 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, $this->gp_locale->slug ) );
if ( ! $projects ) {
return array();
}
if ( in_array( '0', $projects, true ) ) {
return array( 'all' );
}
return $projects;
}
/**
* Renders the overview page.
*/
private function render_translation_editors() {
$list_table = $this->get_translation_editors_list_table();
$list_table->prepare_items();
$feedback_message = $this->get_feedback_message();
require __DIR__ . '/views/translation-editors.php';
}
/**
* Renders the edit page.
*/
private function render_edit_translation_editor( $user_id ) {
global $wpdb;
$project_access_list = $this->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(
'searchPlaceholder' => esc_attr__( 'Search...', 'rosetta' ),
),
'lastUpdated' => $last_updated,
'accessList' => $project_access_list,
) );
$feedback_message = $this->get_feedback_message();
require __DIR__ . '/views/edit-translation-editor.php';
}
/**
* Ajax handler for retrieving projects.
*/
public function ajax_rosetta_get_projects() {
$projects = $this->get_translate_projects();
array_walk( $projects, function( $value ) {
unset( $value->path ); // Path is not needed.
} );
$project_tree = $this->get_project_tree( $projects, 0, 1 );
// Sort the tree and remove array keys.
usort( $project_tree, array( $this, '_sort_name_callback' ) );
foreach ( $project_tree as $key => $project ) {
if ( isset( $project->sub_projects ) ) {
usort( $project->sub_projects, array( $this, '_sort_name_callback' ) );
$project->sub_projects = array_values( $project->sub_projects );
}
}
$project_tree = array_values( $project_tree );
ob_start( 'ob_gzhandler' ); // Compress JSON.
wp_send_json_success( $project_tree );
}
/**
* Returns a feedback message based on the current request.
*
* @return string HTML formatted message.
*/
private function get_feedback_message() {
$message = '';
if ( ! empty( $_REQUEST['update'] ) && ! empty( $_REQUEST['error'] ) ) {
return $message;
}
$count = empty( $_REQUEST['count'] ) ? 1 : (int) $_REQUEST['count'];
$messages = array(
'update' => array(
'user-updated' => __( 'Translation editor updated.', 'rosetta' ),
'user-added' => __( 'New translation editor added.', 'rosetta' ),
'user-added-custom-projects' => __( 'New translation editor added. You can select the projects now.', 'rosetta' ),
'user-removed' => sprintf( _n( '%s translation editor removed.', '%s translation editors removed.', $count, 'rosetta' ), number_format_i18n( $count ) ),
),
'error' => array(
'no-user-found' => __( 'The user couldn&#8217;t be found.', 'rosetta' ),
'not-a-member' => __( 'The user is not a member of this site.', 'rosetta' ),
'not-added-to-site' => __( 'The user couldn&#8217;t be added to this site.', 'rosetta' ),
'user-cannot' => __( 'The user is not a translation editor.', 'rosetta' ),
'user-exists' => __( 'The user is already a translation editor.', 'rosetta' ),
),
);
if ( isset( $_REQUEST['error'], $messages['error'][ $_REQUEST['error'] ] ) ) {
$message = sprintf(
'<div class="notice notice-error"><p>%s</p></div>',
$messages['error'][ $_REQUEST['error'] ]
);
} elseif ( isset( $_REQUEST['update'], $messages['update'][ $_REQUEST['update'] ] ) ) {
$message = sprintf(
'<div class="notice notice-success"><p>%s</p></div>',
$messages['update'][ $_REQUEST['update'] ]
);
}
return $message;
}
/**
* Wrapper for the custom list table which lists translation editors.
*
* @return Rosetta_Translation_Editors_List_Table The list table.
*/
private function get_translation_editors_list_table() {
static $list_table;
require_once __DIR__ . '/class-translation-editors-list-table.php';
if ( isset( $list_table ) ) {
return $list_table;
}
$projects = $this->get_translate_projects();
$project_tree = $this->get_project_tree( $projects, 0, 1 );
$args = array(
'user_roles' => array( self::TRANSLATION_EDITOR_ROLE, self::GENERAL_TRANSLATION_EDITOR_ROLE ),
'projects' => $projects,
'project_tree' => $project_tree,
'rosetta_roles' => $this,
);
$list_table = new Rosetta_Translation_Editors_List_Table( $args );
return $list_table;
}
/**
* Fetches all parent projects and their direct sub-projects like 'wp'
* and 'wp/dev' or 'wp-plugins' and 'wp-plugins/wordpress-importer'.
*
* @return array List of projects.
*/
private function get_translate_projects() {
global $wpdb;
static $projects = null;
if ( null !== $projects ) {
return $projects;
}
$ignore_project_ids = array(
13, // BuddyPress
58, // bbPress
67, // WordCamp Base Theme
91, // GlotPress
481, // Plugin Directory
2804, // Waiting
369622, // Disabled
);
$_projects = $wpdb->get_results( '
SELECT id, name, parent_project_id, slug, path
FROM ' . $wpdb->gp_projects . '
WHERE
id NOT IN(' . implode( ',', $ignore_project_ids ) . ') AND
LENGTH(path) - LENGTH(REPLACE(path, "/", "")) BETWEEN 0 AND 1;
' );
$projects = array();
foreach ( $_projects as $project ) {
$project->name = html_entity_decode( $project->name, ENT_QUOTES, 'UTF-8' );
$projects[ $project->id ] = $project;
}
return $projects;
}
/**
* Transforms a flat array to a hierarchy tree.
*
* @param array $projects The projects.
* @param int $parent_id Optional. Parent ID. Default 0.
* @param int $max_depth Optional. Max depth to avoid endless recursion. Default 5.
* @return array The project tree.
*/
public function get_project_tree( $projects, $parent_id = 0, $max_depth = 5 ) {
if ( $max_depth < 0 ) { // Avoid an endless recursion.
return;
}
$tree = array();
foreach ( $projects as $project ) {
if ( $project->parent_project_id == $parent_id ) {
$sub_projects = $this->get_project_tree( $projects, $project->id, $max_depth - 1 );
if ( $sub_projects ) {
$project->sub_projects = $sub_projects;
}
$tree[ $project->id ] = $project;
}
}
return $tree;
}
/**
* Returns the parent project for a sub project.
*
* @param array $tree The project tree.
* @param int $child_id The project tree.
* @return object The parent project.
*/
public function get_parent_project( $tree, $child_id ) {
$parent = null;
foreach ( $tree as $project ) {
if ( $project->id == $child_id ) {
$parent = $project;
break;
}
if ( isset( $project->sub_projects ) ) {
$parent = $this->get_parent_project( $project->sub_projects, $child_id );
if ( $parent ) {
$parent = $project;
break;
}
}
}
return $parent;
}
private function _sort_name_callback( $a, $b ) {
return strnatcasecmp( $a->name, $b->name );
}
}
new Rosetta_Roles();