WordPress.org

Making WordPress.org

Changeset 1419


Ignore:
Timestamp:
03/19/2015 02:48:45 PM (5 years ago)
Author:
ocean90
Message:

Rosetta: Extend roles plugin to allow per-project permissions for validators.

see #741, #519.

Location:
sites/trunk/global.wordpress.org/public_html/wp-content/mu-plugins/roles
Files:
6 added
1 edited

Legend:

Unmodified
Added
Removed
  • sites/trunk/global.wordpress.org/public_html/wp-content/mu-plugins/roles/rosetta-roles.php

    r1402 r1419  
    11<?php
    2 
    3 add_filter( 'gettext_with_context', 'ros_rename_user_roles', 10, 4 );
    4 function ros_rename_user_roles( $translated, $text, $context, $domain ) {
    5     if ( $domain !== 'default' || $context !== 'User role' ) {
    6         return $translated;
    7     }
    8     if ( 'Validator' === $text ) {
    9         return __( 'Validator', 'rosetta' );
    10     }
    11     return $translated;
     2/**
     3 * Plugin Name: Rosetta Roles
     4 * Plugin URI: https://wordpress.org/
     5 * Description: WordPress interface for managing roles.
     6 * Author: ocean90
     7 * Version: 1.0
     8 */
     9
     10class Rosetta_Roles {
     11    /**
     12     * Endpoint for profiles.wordpress.org updates.
     13     */
     14    const PROFILES_HANDLER_URL = 'https://profiles.wordpress.org/wp-admin/admin-ajax.php';
     15
     16    /**
     17     * Holds the role of a translation editor.
     18     *
     19     * @var string
     20     */
     21    public $translation_editor_role = 'translation_editor';
     22
     23    /**
     24     * Holds the meta key of the project access list.
     25     *
     26     * @var string
     27     */
     28    public $project_access_meta_key = 'translation_editor_project_access_list';
     29
     30    /**
     31     * Constructor.
     32     */
     33    public function __construct() {
     34        add_action( 'plugins_loaded', array( $this, 'plugins_loaded' ) );
     35    }
     36
     37    /**
     38     * Attaches hooks once plugins are loaded.
     39     */
     40    public function plugins_loaded() {
     41        add_filter( 'editable_roles', array( $this, 'editable_roles' ) );
     42        add_filter( 'manage_users_columns',  array( $this, 'add_roles_column' ) );
     43        add_filter( 'manage_users_custom_column',  array( $this, 'display_user_roles' ), 10, 3 );
     44        add_action( 'admin_init', array( $this, 'role_modifications' ) );
     45        add_action( 'set_user_role', array( $this, 'restore_translation_editor_role' ), 10, 3 );
     46        add_filter( 'gettext_with_context', array( $this, 'rename_user_roles' ), 10, 4 );
     47        add_action( 'user_row_actions', array( $this, 'user_row_action_role_editor' ), 10, 2 );
     48        add_action( 'admin_menu', array( $this, 'register_translation_editors_page' ) );
     49    }
     50
     51    /**
     52     * Registers "Translation Editor" role and modifies editor role.
     53     */
     54    public function role_modifications() {
     55        if ( ! get_role( $this->translation_editor_role ) ) {
     56            add_role( $this->translation_editor_role, __( 'Translation Editor', 'rosetta' ), array( 'read' => true, 'level_0' => true ) );
     57        }
     58
     59        $editor_role = get_role( 'editor' );
     60        if ( $editor_role && ! $editor_role->has_cap( 'remove_users' ) ) {
     61            $editor_role->add_cap( 'edit_theme_options' );
     62            $editor_role->add_cap( 'list_users' );
     63            $editor_role->add_cap( 'promote_users' );
     64            $editor_role->add_cap( 'remove_users' );
     65        }
     66
     67        // Remove deprecated validator role.
     68        /*$validator_role = get_role( 'validator' );
     69        if ( $validator_role ) {
     70            remove_role( 'validator' );
     71        }*/
     72    }
     73
     74    /**
     75     * Restores the "Translation Editor" role if an user is promoted.
     76     *
     77     * @param int    $user_id   The user ID.
     78     * @param string $role      The new role.
     79     * @param array  $old_roles An array of the user's previous roles.
     80     */
     81    public function restore_translation_editor_role( $user_id, $role, $old_roles ) {
     82        if ( ! in_array( $this->translation_editor_role, $old_roles ) ) {
     83            return;
     84        }
     85
     86        $user = new WP_User( $user_id );
     87        $user->add_role( $this->translation_editor_role );
     88    }
     89
     90    /**
     91     * Removes "Translation Editor" role and "Administrator" role from
     92     * the list of editable roles.
     93     *
     94     * The list used in wp_dropdown_roles() on users list table.
     95     *
     96     * @param array $all_roles List of roles.
     97     * @return array Filtered list of editable roles.
     98     */
     99    public function editable_roles( $roles ) {
     100        unset( $roles[ $this->translation_editor_role ] );
     101
     102        if ( ! is_super_admin() && ! is_main_site() ) {
     103            unset( $roles['administrator'] );
     104        }
     105
     106        return $roles;
     107    }
     108
     109    /**
     110     * Translates the "Translation Editor" role.
     111     *
     112     * @param string $translation Translated text.
     113     * @param string $text        Text to translate.
     114     * @param string $context     Context information for the translators.
     115     * @param string $domain      Text domain.
     116     * @return string Translated user role.
     117     */
     118    public function rename_user_roles( $translation, $text, $context, $domain ) {
     119        if ( $domain !== 'default' || $context !== 'User role' ) {
     120            return $translation;
     121        }
     122
     123        if ( 'Translation Editor' === $text ) {
     124            return __( 'Translation Editor', 'rosetta' );
     125        }
     126
     127        return $translation;
     128    }
     129
     130    /**
     131     * Replaces the "Role" column with a "Roles" column.
     132     *
     133     * @param array $columns An array of column headers.
     134     * @return array An array of column headers.
     135     */
     136    public function add_roles_column( $columns ) {
     137        $posts = $columns['posts'];
     138        unset( $columns['role'], $columns['posts'] );
     139        reset( $columns );
     140        $columns['roles'] = __( 'Roles', 'rosetta' );
     141        $columns['posts'] = $posts;
     142
     143        return $columns;
     144    }
     145
     146    /**
     147     * Displays a comma separated list of user's roles.
     148     *
     149     * @param string $output      Custom column output.
     150     * @param string $column_name Column name.
     151     * @param int    $user_id     ID of the currently-listed user.
     152     * @return string Comma separated list of user's roles.
     153     */
     154    public function display_user_roles( $output, $column_name, $user_id ) {
     155        global $wp_roles;
     156
     157        if ( 'roles' == $column_name ) {
     158            $user_roles = array();
     159            $user = new WP_User( $user_id );
     160            foreach ( $user->roles as $role ) {
     161                $role_name = $wp_roles->role_names[ $role ];
     162                $role_name = translate_user_role( $role_name );
     163                $user_roles[] = $role_name;
     164            }
     165
     166            return implode( ', ', $user_roles );
     167        }
     168
     169        return $output;
     170    }
     171
     172    /**
     173     * Registers page for managing translation editors.
     174     */
     175    public function register_translation_editors_page() {
     176        $this->translation_editors_page = add_users_page(
     177            __( 'Translation Editors', 'rosetta' ),
     178            __( 'Translation Editors', 'rosetta' ),
     179            'list_users',
     180            'translation-editors',
     181            array( $this, 'render_translation_editors_page' )
     182        );
     183
     184        add_action( 'load-' . $this->translation_editors_page, array( $this, 'load_translation_editors_page' ) );
     185        add_action( 'admin_print_scripts-' . $this->translation_editors_page, array( $this, 'enqueue_scripts' ) );
     186    }
     187
     188    /**
     189     * Enqueues scripts.
     190     */
     191    public function enqueue_scripts() {
     192        wp_enqueue_script( 'rosetta-roles', plugins_url( '/js/rosetta-roles.js', __FILE__ ), array( 'jquery' ), '1', true );
     193    }
     194
     195    /**
     196     * Loads either the overview or the edit handler.
     197     */
     198    public function load_translation_editors_page() {
     199        if ( ! empty( $_REQUEST['user_id'] ) ) {
     200            $this->load_edit_translation_editor( $_REQUEST['user_id'] );
     201        } else {
     202            $this->load_translation_editors();
     203        }
     204    }
     205
     206    /**
     207     * Renders either the overview or the edit view.
     208     */
     209    public function render_translation_editors_page() {
     210        if ( ! empty( $_REQUEST['user_id'] ) ) {
     211            $this->render_edit_translation_editor( $_REQUEST['user_id'] );
     212        } else {
     213            $this->render_translation_editors();
     214        }
     215    }
     216
     217    /**
     218     * Handler for overview page.
     219     */
     220    private function load_translation_editors() {
     221        global $wpdb;
     222
     223        $list_table = $this->get_translation_editors_list_table();
     224        $action = $list_table->current_action();
     225        $redirect = menu_page_url( 'translation-editors', false );
     226
     227        if ( $action ) {
     228            switch ( $action ) {
     229                case 'add-translation-editor':
     230                    check_admin_referer( 'add-translation-editor', '_nonce_add-translation-editor' );
     231
     232                    if ( ! current_user_can( 'promote_users' ) ) {
     233                        wp_redirect( $redirect );
     234                        exit;
     235                    }
     236
     237                    $user_details = null;
     238                    $user = wp_unslash( $_REQUEST['user'] );
     239                    if ( false !== strpos( $user_email, '@' ) ) {
     240                        $user_details = get_user_by( 'email', $user );
     241                    } else {
     242                        $user_details = get_user_by( 'login', $user );
     243                    }
     244
     245                    if ( ! $user_details ) {
     246                        wp_redirect( add_query_arg( array( 'error' => 'no-user-found' ), $redirect ) );
     247                        exit;
     248                    }
     249
     250                    if ( ! is_user_member_of_blog( $user_details->ID ) ) {
     251                        wp_redirect( add_query_arg( array( 'error' => 'not-a-member' ), $redirect ) );
     252                        exit;
     253                    }
     254
     255                    if ( user_can( $user_details, $this->translation_editor_role ) ) {
     256                        wp_redirect( add_query_arg( array( 'error' => 'user-exists' ), $redirect ) );
     257                        exit;
     258                    }
     259
     260                    $user_details->add_role( $this->translation_editor_role );
     261                    $this->notify_translation_editor_update( $user_details->ID, 'add' );
     262
     263                    $projects = empty( $_REQUEST['projects'] ) ? '' : $_REQUEST['projects'];
     264                    if ( 'custom' === $projects ) {
     265                        $redirect = add_query_arg( 'user_id', $user_details->ID, $redirect );
     266                        wp_redirect( add_query_arg( array( 'update' => 'user-added-custom-projects' ), $redirect ) );
     267                        exit;
     268                    }
     269
     270                    $meta_key = $wpdb->get_blog_prefix() . $this->project_access_meta_key;
     271                    update_user_meta( $user_details->ID, $meta_key, array( 'all' ) );
     272
     273                    wp_redirect( add_query_arg( array( 'update' => 'user-added' ), $redirect ) );
     274                    exit;
     275                case 'remove-translation-editors':
     276                    check_admin_referer( 'bulk-translation-editors' );
     277
     278                    if ( ! current_user_can( 'promote_users' ) ) {
     279                        wp_redirect( $redirect );
     280                        exit;
     281                    }
     282
     283                    if ( empty( $_REQUEST['translation-editors'] ) ) {
     284                        wp_redirect( $redirect );
     285                        exit;
     286                    }
     287
     288                    $count = 0;
     289                    $meta_key = $wpdb->get_blog_prefix() . $this->project_access_meta_key;
     290                    $user_ids = array_map( 'intval', (array) $_REQUEST['translation-editors'] );
     291                    foreach ( $user_ids as $user_id ) {
     292                        $user = get_user_by( 'id', $user_id );
     293                        $user->remove_role( $this->translation_editor_role );
     294                        delete_user_meta( $user_id, $meta_key );
     295                        $this->notify_translation_editor_update( $user_id, 'remove' );
     296                        $count++;
     297                    }
     298
     299                    wp_redirect( add_query_arg( array( 'update' => 'user-removed', 'count' => $count ), $redirect ) );
     300                    exit;
     301                case 'remove-translation-editor':
     302                    check_admin_referer( 'remove-translation-editor' );
     303
     304                    if ( ! current_user_can( 'promote_users' ) ) {
     305                        wp_redirect( $redirect );
     306                        exit;
     307                    }
     308
     309                    if ( empty( $_REQUEST['translation-editor'] ) ) {
     310                        wp_redirect( $redirect );
     311                        exit;
     312                    }
     313
     314                    $user_id = (int) $_REQUEST['translation-editor'];
     315                    $user = get_user_by( 'id', $user_id );
     316                    $user->remove_role( $this->translation_editor_role );
     317                    $meta_key = $wpdb->get_blog_prefix() . $this->project_access_meta_key;
     318                    delete_user_meta( $user_id, $meta_key );
     319                    $this->notify_translation_editor_update( $user_id, 'remove' );
     320
     321                    wp_redirect( add_query_arg( array( 'update' => 'user-removed' ), $redirect ) );
     322                    exit;
     323            }
     324        }
     325    }
     326
     327    /**
     328     * Handler for editing a translation editor.
     329     *
     330     * @param  int $user_id User ID of a translation editor.
     331     */
     332    private function load_edit_translation_editor( $user_id ) {
     333        global $wpdb;
     334
     335        $redirect = menu_page_url( 'translation-editors', false );
     336
     337        if ( ! current_user_can( 'promote_users' ) ) {
     338            wp_redirect( $redirect );
     339            exit;
     340        }
     341
     342        $user_details = get_user_by( 'id', $user_id );
     343
     344        if ( ! $user_details ) {
     345            wp_redirect( add_query_arg( array( 'error' => 'no-user-found' ), $redirect ) );
     346            exit;
     347        }
     348
     349        if ( ! is_user_member_of_blog( $user_details->ID ) ) {
     350            wp_redirect( add_query_arg( array( 'error' => 'not-a-member' ), $redirect ) );
     351            exit;
     352        }
     353
     354        if ( ! user_can( $user_details, $this->translation_editor_role ) ) {
     355            wp_redirect( add_query_arg( array( 'error' => 'user-cannot' ), $redirect ) );
     356            exit;
     357        }
     358
     359        $action = empty( $_REQUEST['action'] ) ? '' : $_REQUEST['action'];
     360        switch ( $action ) {
     361            case 'update-translation-editor':
     362                check_admin_referer( 'update-translation-editor_' . $user_details->ID );
     363
     364                $redirect = add_query_arg( 'user_id', $user_details->ID, $redirect );
     365
     366                $all_projects = $this->get_translate_top_level_projects();
     367                $all_projects = wp_list_pluck( $all_projects, 'id' );
     368                $all_projects = array_map( 'intval', $all_projects );
     369
     370                $projects = (array) $_REQUEST['projects'];
     371                if ( in_array( 'all', $projects ) ) {
     372                    $projects = array( 'all' );
     373                } else {
     374                    $projects = array_map( 'intval', $projects );
     375                    $projects = array_values( array_intersect( $all_projects, $projects ) );
     376                }
     377
     378                $meta_key = $wpdb->get_blog_prefix() . $this->project_access_meta_key;
     379                update_user_meta( $user_details->ID, $meta_key, $projects );
     380
     381                wp_redirect( add_query_arg( array( 'update' => 'user-updated' ), $redirect ) );
     382                exit;
     383        }
     384    }
     385
     386    /**
     387     * Renders the overview page.
     388     */
     389    private function render_translation_editors() {
     390        $list_table = $this->get_translation_editors_list_table();
     391        $list_table->prepare_items();
     392
     393        $feedback_message = $this->get_feedback_message();
     394
     395        require __DIR__ . '/views/translation-editors.php';
     396    }
     397
     398    /**
     399     * Renders the edit page.
     400     */
     401    private function render_edit_translation_editor( $user_id ) {
     402        global $wpdb;
     403
     404        $projects = $this->get_translate_top_level_projects();
     405
     406        $meta_key = $wpdb->get_blog_prefix() . $this->project_access_meta_key;
     407        $project_access_list = get_user_meta( $user_id, $meta_key, true );
     408        if ( ! $project_access_list ) {
     409            $project_access_list = array();
     410        }
     411
     412        $feedback_message = $this->get_feedback_message();
     413
     414        require __DIR__ . '/views/edit-translation-editor.php';
     415    }
     416
     417    /**
     418     * Returns a feedback message based on the current request.
     419     *
     420     * @return string HTML formatted message.
     421     */
     422    private function get_feedback_message() {
     423        $message = '';
     424
     425        if ( ! empty( $_REQUEST['update'] ) && ! empty( $_REQUEST['error'] ) ) {
     426            return $message;
     427        }
     428
     429        $count = empty( $_REQUEST['count'] ) ? 1 : (int) $_REQUEST['count'];
     430
     431        $messages = array(
     432            'update' => array(
     433                'user-updated' => __( 'Translation editor updated.', 'rosetta' ),
     434                'user-added'   => __( 'New translation editor added.', 'rosetta' ),
     435                'user-added-custom-projects' => __( 'New translation editor added. You can select the projects now.', 'rosetta' ),
     436                'user-removed' => sprintf( _n( '%s translation editor removed.', '%s translation editors removed.', $count, 'rosetta' ), number_format_i18n( $count ) ),
     437            ),
     438
     439            'error' => array(
     440                'no-user-found' => __( 'The user couldn&#8217;t be found.', 'rosetta' ),
     441                'not-a-member'  => __( 'The user is not a member of this site.', 'rosetta' ),
     442                'user-cannot'   => __( 'The user is not a translation editor.', 'rosetta' ),
     443                'user-exists'   => __( 'The user is already a translation editor.', 'rosetta' ),
     444            ),
     445        );
     446
     447        if ( isset( $_REQUEST['error'], $messages['error'][ $_REQUEST['error'] ] ) ) {
     448            $message = sprintf(
     449                '<div class="notice notice-error"><p>%s</p></div>',
     450                $messages['error'][ $_REQUEST['error'] ]
     451            );
     452        } elseif( isset( $_REQUEST['update'], $messages['update'][ $_REQUEST['update'] ] ) ) {
     453            $message = sprintf(
     454                '<div class="notice notice-success"><p>%s</p></div>',
     455                $messages['update'][ $_REQUEST['update'] ]
     456            );
     457        }
     458
     459        return $message;
     460    }
     461
     462    /**
     463     * Wrapper for the custom list table which lists translation editors.
     464     *
     465     * @return Rosetta_Translation_Editors_List_Table The list table.
     466     */
     467    private function get_translation_editors_list_table() {
     468        global $wpdb;
     469        static $list_table;
     470
     471        require_once __DIR__ . '/class-translation-editors-list-table.php';
     472
     473        if ( isset( $list_table ) ) {
     474            return $list_table;
     475        }
     476
     477        $args = array(
     478            'user_role'               => $this->translation_editor_role,
     479            'projects'                => $this->get_translate_top_level_projects(),
     480            'project_access_meta_key' => $wpdb->get_blog_prefix() . $this->project_access_meta_key,
     481        );
     482        $list_table = new Rosetta_Translation_Editors_List_Table( $args );
     483
     484        return $list_table;
     485    }
     486
     487    /**
     488     * Notifies profiles.wordpress.org about a change.
     489     *
     490     * @param  int    $user_id User ID.
     491     * @param  string $action  Can be 'add' or 'remove'.
     492     */
     493    private function notify_translation_editor_update( $user_id, $action ) {
     494        $args = array(
     495            'body' => array(
     496                'action'      => 'wporg_handle_association',
     497                'source'      => 'polyglots',
     498                'command'     => $action,
     499                'user_id'     => $user_id,
     500                'association' => 'translation-editor',
     501            )
     502        );
     503
     504        wp_remote_post( self::PROFILES_HANDLER_URL, $args );
     505    }
     506
     507    /**
     508     * Fetches all top level projects from translate.wordpress.org.
     509     *
     510     * @return array List of projects.
     511     */
     512    private function get_translate_top_level_projects() {
     513        global $wpdb;
     514
     515        $cache = get_site_transient( 'translate-top-level-projects' );
     516        if ( false !== $cache ) {
     517            return $cache;
     518        }
     519
     520        $_projects = $wpdb->get_results( "
     521            SELECT id, name
     522            FROM translate_projects
     523            WHERE parent_project_id IS NULL
     524            ORDER BY name ASC
     525        " );
     526
     527        $projects = array();
     528        foreach ( $_projects as $project ) {
     529            $projects[ $project->id ] = $project;
     530        }
     531
     532        set_site_transient( 'translate-top-level-projects', $projects, DAY_IN_SECONDS );
     533
     534        return $projects;
     535    }
    12536}
    13537
    14 add_action( 'admin_menu', 'ros_remove_widgets_menu' );
    15 function ros_remove_widgets_menu() {
    16     remove_submenu_page( 'themes.php', 'themes.php' );
    17     remove_submenu_page( 'themes.php', 'widgets.php' );
    18 }
    19 
    20 add_filter( 'editable_roles', 'ros_editable_roles' );
    21 function ros_editable_roles( $roles ) {
    22     $subscriber = $roles['subscriber'];
    23     unset( $roles['subscriber'] );
    24     reset( $roles );
    25     $roles['subscriber'] = $subscriber;
    26     if ( ! is_super_admin() && ! is_main_site() ) {
    27         unset( $roles['administrator'] );
    28     }
    29     return $roles;
    30 }
    31 
    32 add_filter( 'admin_init', 'ros_role_modifications' );
    33 function ros_role_modifications() {
    34     if ( ! get_role( 'validator' ) ) {
    35         add_role( 'validator', __( 'Validator', 'rosetta' ), array( 'read' => true, 'level_0' => true ) );
    36     }
    37     $editor_role = get_role( 'editor' );
    38     if ( $editor_role && ! $editor_role->has_cap( 'remove_users' ) ) {
    39         $editor_role->add_cap( 'edit_theme_options' );
    40         $editor_role->add_cap( 'list_users' );
    41         $editor_role->add_cap( 'promote_users' );
    42         $editor_role->add_cap( 'remove_users' );
    43     }
    44 }
     538$GLOBALS['rosetta_roles'] = new Rosetta_Roles();
Note: See TracChangeset for help on using the changeset viewer.