Making WordPress.org

Changeset 1419


Ignore:
Timestamp:
03/19/2015 02:48:45 PM (11 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.