WordPress.org

Making WordPress.org

Ticket #2000: 2000.4.diff

File 2000.4.diff, 24.2 KB (added by akirk, 5 years ago)
  • new file global.wordpress.org/public_html/wp-content/mu-plugins/roles/cross-locale-pte.php

    diff --git global.wordpress.org/public_html/wp-content/mu-plugins/roles/cross-locale-pte.php global.wordpress.org/public_html/wp-content/mu-plugins/roles/cross-locale-pte.php
    new file mode 100644
    index 00000000..80f31978
    - +  
     1<?php
     2/**
     3 * Plugin Name: Cross-Locale PTE
     4 * Plugin URI: https://meta.trac.wordpress.org/ticket/2000
     5 * Description: Implements a user that can approve and import translations in all
     6 * translation-sets of a project but cannot overwrite current translations by others.
     7 */
     8class Cross_Locale_PTE {
     9
     10        /**
     11         * The special locale name.
     12         */
     13        const ALL_LOCALES = 'all-locales';
     14
     15        /**
     16         * The capability the user needs to have to manage Cross-Locale PTEs.
     17         */
     18        const MANAGE_CROSS_LOCALE_PTES_CAP = 'manage_network_users';
     19
     20        /**
     21         * Cache group.
     22         *
     23         * @var string
     24         */
     25        public static $cache_group = 'wporg-translate';
     26
     27        /**
     28         * The admin page hook suffix.
     29         *
     30         * @var string
     31         */
     32        private static $admin_page;
     33
     34        /**
     35         * The user that is being administered.
     36         *
     37         * @var WP_User
     38         */
     39        private static $user;
     40
     41        /**
     42         * Init Admin hooks.
     43         */
     44        public static function init_admin() {
     45                if ( current_user_can( self::MANAGE_CROSS_LOCALE_PTES_CAP ) ) {
     46                        add_action( 'admin_menu', array( __CLASS__, 'register_admin_page' ) );
     47                }
     48        }
     49
     50        /**
     51         * Register the Cross-Locale PTE Admin page in wp-admin.
     52         */
     53        public static function register_admin_page() {
     54                self::$admin_page = add_menu_page(
     55                        __( 'Cross-Locale PTE', 'rosetta' ),
     56                        __( 'Cross-Locale PTE', 'rosetta' ),
     57                        self::MANAGE_CROSS_LOCALE_PTES_CAP,
     58                        'cross-locale-pte',
     59                        array( __CLASS__, 'render_admin_page' ),
     60                        'dashicons-translation',
     61                        71 // After Users.
     62                );
     63
     64                add_action( 'load-' . self::$admin_page, array( __CLASS__, 'handle_admin_post' ) );
     65                add_action( 'admin_print_scripts-' . self::$admin_page, array( 'Rosetta_Roles', 'enqueue_scripts' ) );
     66                add_action( 'admin_footer-' . self::$admin_page, array( 'Rosetta_Roles', 'print_js_templates' ) );
     67                add_action( 'admin_print_styles-' . self::$admin_page, array( 'Rosetta_Roles', 'enqueue_styles' ) );
     68        }
     69
     70        /**
     71         * Handle POST requests for the admin page.
     72         */
     73        public static function handle_admin_post() {
     74                $redirect = menu_page_url( 'cross-locale-pte', false );
     75
     76                if ( ! current_user_can( self::MANAGE_CROSS_LOCALE_PTES_CAP ) ) {
     77                        wp_redirect( $redirect );
     78                        exit;
     79                }
     80
     81                if ( ! empty( $_REQUEST['user'] ) ) {
     82                        check_admin_referer( 'cross-locale-pte', '_nonce_cross-locale-pte' );
     83
     84                        self::$user = get_user_by( 'login', $_REQUEST['user'] );
     85                        if ( ! self::$user ) {
     86                                self::$user = get_user_by( 'email', $_REQUEST['user'] );
     87                        }
     88
     89                        if ( self::$user ) {
     90                                wp_redirect( add_query_arg( array( 'user_id' => self::$user->ID ), $redirect ) );
     91                        } else {
     92                                wp_redirect( add_query_arg( array( 'error' => 'no-user-found' ), $redirect ) );
     93                        }
     94                        exit;
     95                }
     96
     97                if ( ! empty( $_REQUEST['user_id'] ) ) {
     98                        self::$user = get_user_by( 'id', $_REQUEST['user_id'] );
     99                        if ( ! self::$user ) {
     100                                wp_redirect( add_query_arg( array( 'error' => 'no-user-found' ), $redirect ) );
     101                                exit;
     102                        }
     103                }
     104
     105                if ( ! empty( $_REQUEST['action'] ) ) {
     106                        switch ( $_REQUEST['action'] ) {
     107                                case 'update-cross-locale-pte':
     108                                        check_admin_referer( 'update-cross-locale-pte_' . self::$user->ID );
     109                                        return self::update_cross_locale_pte();
     110                        }
     111
     112                        return self::render_edit_page();
     113                }
     114        }
     115
     116        /**
     117         * Render the Cross-Locale PTE overview page in the admin.
     118         */
     119        public static function render_admin_page() {
     120                if ( ! empty( $_REQUEST['user_id'] ) ) {
     121                        return self::render_edit_page( $_REQUEST['user_id'] );
     122                }
     123
     124                $feedback_message = '';
     125                $cross_locale_pte_users = self::get_all_users();
     126                require __DIR__ . '/views/cross-locale-pte.php';
     127        }
     128
     129        /**
     130         * Update the projects for the Cross-Locale PTE.
     131         */
     132        public static function update_cross_locale_pte() {
     133                global $wpdb;
     134
     135                $projects = array_map( 'strval', explode( ',', $_REQUEST['projects'] ) );
     136                $current_projects = self::get_users_projects( self::$user->ID );
     137
     138                $projects_to_remove = array_diff( $current_projects, $projects );
     139                $projects_to_add = array_diff( $projects, $current_projects );
     140
     141                $now = current_time( 'mysql', 1 );
     142
     143                $values_to_add = array();
     144                foreach ( $projects_to_add as $project_id ) {
     145                        $values_to_add[] = $wpdb->prepare( '(%d, %d, %s, %s)',
     146                                self::$user->ID,
     147                                $project_id,
     148                                self::ALL_LOCALES,
     149                                $now
     150                        );
     151                }
     152
     153                if ( $values_to_add ) {
     154                        $wpdb->query( "
     155                                INSERT INTO {$wpdb->wporg_translation_editors}
     156                                ( `user_id`,`project_id`, `locale`, `date_added` )
     157                                VALUES " . implode( ', ', $values_to_add )
     158                        );
     159                }
     160
     161                $values_to_remove = array_map( 'intval', $projects_to_remove );
     162                if ( $values_to_remove ) {
     163                        $wpdb->query( $wpdb->prepare( "
     164                                DELETE FROM {$wpdb->wporg_translation_editors}
     165                                WHERE `user_id` = %d AND `locale` = %s
     166                                AND project_id IN (" . implode( ', ', $values_to_remove ) . ')',
     167                        self::$user->ID, self::ALL_LOCALES ) );
     168                }
     169
     170                wp_redirect( add_query_arg( array( 'user_id' => self::$user->ID ), menu_page_url( 'cross-locale-pte', false ) ) );
     171                exit;
     172        }
     173
     174        /**
     175         * Render the page to edit a single Cross-Locale PTE.
     176         */
     177        public static function render_edit_page() {
     178                $user = self::$user;
     179                $project_access_list = self::get_users_projects( $user->ID );
     180                $last_updated = get_blog_option( WPORG_TRANSLATE_BLOGID, 'wporg_projects_last_updated' );
     181
     182                wp_localize_script( 'rosetta-roles', '_rosettaProjectsSettings', array(
     183                        'l10n' => array(
     184                                'searchPlaceholder' => esc_attr__( 'Search...', 'rosetta' ),
     185                        ),
     186                        'lastUpdated' => $last_updated,
     187                        'accessList' => $project_access_list,
     188                ) );
     189
     190                $feedback_message = '';
     191                require __DIR__ . '/views/edit-cross-locale-pte.php';
     192        }
     193
     194        /**
     195         * Retrieves the projects for which a user has cross-locale PTE permissions.
     196         *
     197         * @param int $user_id User ID.
     198         * @return array List of project IDs.
     199         */
     200        public static function get_users_projects( $user_id ) {
     201                global $wpdb;
     202
     203                $projects = $wpdb->get_col( $wpdb->prepare( "
     204                        SELECT project_id FROM
     205                        {$wpdb->wporg_translation_editors}
     206                        WHERE user_id = %d AND locale = %s
     207                ", $user_id, self::ALL_LOCALES ) );
     208
     209                return $projects;
     210        }
     211        /**
     212         * Retrieves the projects for which a user has cross-locale PTE permissions.
     213         *
     214         * @return array List of User IDs.
     215         */
     216        public static function get_all_users() {
     217                global $wpdb;
     218
     219                $rows = $wpdb->get_results( $wpdb->prepare( "
     220                        SELECT te.user_id, te.project_id, p.name AS project_name FROM
     221                        {$wpdb->wporg_translation_editors} te
     222                        JOIN translate_projects p ON te.project_id = p.id
     223                        WHERE te.locale = %s
     224                ", self::ALL_LOCALES ) );
     225
     226                $user_ids = array();
     227                foreach ( $rows as $row ) {
     228                        if ( ! isset( $user_ids[ $row->user_id ] ) ) {
     229                                $user = get_user_by( 'id', $row->user_id );
     230                                if ( ! $user ) {
     231                                        continue;
     232                                }
     233                                $row->user_login = $user->user_login;
     234                                $row->email = $user->user_email;
     235                                $row->display_name = $user->display_name;
     236                                $row->projects = array( $row->project_id => $row->project_name );
     237                                $user_ids[ $row->user_id ] = $row;
     238                        } else {
     239                                $user_ids[ $row->user_id ]->projects[ $row->project_id ] = $row->project_name;
     240                        }
     241                }
     242
     243                return $user_ids;
     244        }
     245
     246        /**
     247         * Check for the Cross-Locale PTE permission for the project.
     248         *
     249         * @param WP_User $user   The user.
     250         * @param int $project_id The Project ID.
     251         * @return string|bool The verdict.
     252         */
     253        public static function user_has_cross_locale_permission( $user, $project_id ) {
     254                static $cache = null;
     255
     256                if ( null === $cache ) {
     257                        $cache = array();
     258                }
     259
     260                $user_id = intval( $user->ID );
     261                $project_id = intval( $project_id );
     262
     263                if ( isset( $cache[ $user_id ][ $project_id ] ) ) {
     264                        return $cache[ $user_id ][ $project_id ];
     265                }
     266
     267                if ( ! isset( $cache[ $user_id ] ) ) {
     268                        $cache[ $user_id ] = array();
     269                }
     270
     271                global $wpdb;
     272                $result = $wpdb->get_col( $wpdb->prepare( "
     273                        SELECT te.user_id FROM
     274                        {$wpdb->wporg_translation_editors} te
     275                        JOIN translate_projects p ON ( te.project_id = p.id OR te.project_id = p.parent_project_id )
     276                        WHERE te.user_id = %d AND p.id = %d AND te.locale = %s
     277                ", $user_id, $project_id, self::ALL_LOCALES ) );
     278
     279                if ( $result && intval( $result[0] ) === $user_id ) {
     280                        return $cache[ $user_id ][ $project_id ] = true;
     281                }
     282
     283                return $cache[ $user_id ][ $project_id ] = false;
     284        }
     285
     286        /**
     287         * Enforce not-overwriting current translation by others while importing.
     288         *
     289         * @param string         $status          The desired status.
     290         * @param GP_Translation $new_translation The new translation.
     291         * @param GP_Translation $old_translation The old translation.
     292         * @return string The new status.
     293         */
     294        public static function gp_translation_set_import_status( $status, $new_translation, $old_translation ) {
     295                if ( ! isset( $old_translation->translation_set_id ) ) {
     296                        return $status;
     297                }
     298
     299                if ( 'current' !== $old_translation->translation_status ) {
     300                        return $status;
     301                }
     302
     303                if ( GP::$permission->current_user_can( 'cross-pte', 'translation-set', $old_translation->translation_set_id ) ) {
     304                        // Set to waiting if a current translation exists by another user.
     305                        if ( intval( $old_translation->user_id ) !== intval( get_current_user_id() ) ) {
     306                                return 'waiting';
     307                        }
     308                }
     309                return $status;
     310        }
     311
     312        /**
     313         * The GlotPress filter for Cross-Locale PTE.
     314         *
     315         * A Cross-Locale PTE is defined through an entry in the permission table 'cross-pte' with the
     316         * object_id referring to a project id.
     317         * A user with this permission will have 'approve' rights for all translation-sets within this
     318         * project. Usually having approval rights for a translation-set also means that the user has
     319         * approval rights for all translations, but not a Cross-Locale PTE:
     320         * If a current translation exists by another user then overwriting (through UI or import) is not
     321         * possible.
     322         *
     323         * @param string|bool $verdict The verdict from an earlier filter.
     324         * @param array       $args    Arguments that describe the object to judge for.
     325         * @return string|bool The verdict for the object.
     326         */
     327        public static function gp_pre_can_user( $verdict, $args ) {
     328                if ( 'cross-pte' === $args['action'] ) {
     329                        $verdict = self::gp_pre_can_user_cross_pte( $verdict, $args );
     330
     331                        if ( is_bool( $verdict ) ) {
     332                                return $verdict;
     333                        }
     334                }
     335
     336                if ( 'approve' === $args['action'] ) {
     337                        if ( 'translation' === $args['object_type'] ) {
     338                                $verdict = self::gp_pre_can_user_approve_translation( $verdict, $args );
     339                        } elseif ( 'translation-set' === $args['object_type'] ) {
     340                                $verdict = self::gp_pre_can_user_approve_translation_set( $verdict, $args );
     341                        }
     342                }
     343
     344                return $verdict;
     345
     346        }
     347
     348        /**
     349         * A GlotPress sub-filter for the permission 'cross-lte'.
     350         *
     351         * @param string|bool $verdict The verdict from an earlier filter.
     352         * @param array       $args    Arguments that describe the object to judge for.
     353         * @return string|bool The verdict for the object.
     354         */
     355        public static function gp_pre_can_user_cross_pte( $verdict, $args ) {
     356                if ( GP::$permission->user_can( $args['user'], 'admin' ) ) {
     357                        // Admins shouldn't have this because it will end up restricting them.
     358                        return false;
     359                }
     360
     361                if ( 'translation-set' === $args['object_type'] ) {
     362                        if ( isset( $args['extra']['set']->id ) && intval( $args['extra']['set']->id ) === intval( $args['object_id'] ) ) {
     363                                $set = $args['extra']['set'];
     364                        } else {
     365                                $set = GP::$translation_set->get( $args['object_id'] );
     366                        }
     367
     368                        // Allow on all translation-sets within the project.
     369                        if ( $set ) {
     370                                return GP::$permission->user_can( $args['user'], 'cross-pte', 'project', $set->project_id );
     371                        }
     372                } elseif ( 'project' === $args['object_type'] ) {
     373                        return self::user_has_cross_locale_permission( $args['user'], $args['object_id'] );
     374                }
     375
     376                return $verdict;
     377        }
     378
     379        /**
     380         * A GlotPress sub-filter for the permission 'approve' and object 'translation'.
     381         *
     382         * @param string|bool $verdict The verdict from an earlier filter.
     383         * @param array       $args    Arguments that describe the object to judge for.
     384         * @return string|bool The verdict for the object.
     385         */
     386        public static function gp_pre_can_user_approve_translation( $verdict, $args ) {
     387                if ( isset( $args['extra']['translation']->translation_set_id ) && intval( $args['extra']['translation']->id ) === intval( $args['object_id'] ) ) {
     388                        $translation = $args['extra']['translation'];
     389                } else {
     390                        $translation = GP::$translation->get( $args['object_id'] );
     391                }
     392
     393                if ( ! $translation ) {
     394                        return $verdict;
     395                }
     396
     397                static $current_translation_by_user;
     398                $cache_key = $args['user']->ID . '_' . $translation->original_id;
     399
     400                if ( isset( $current_translation_by_user[ $cache_key ] ) ) {
     401                        return $current_translation_by_user[ $cache_key ];
     402                }
     403
     404                if ( GP::$permission->user_can( $args['user'], 'cross-pte', 'translation-set', $translation->translation_set_id ) ) {
     405                        $current_translation = GP::$translation->find_one( array( 'translation_set_id' => $translation->translation_set_id, 'original_id' => $translation->original_id, 'status' => 'current' ) );
     406                        if ( $current_translation && intval( $current_translation->user_id ) !== $args['user']->ID ) {
     407                                // Current translation was authored by someone else. Disallow setting to current.
     408                                return $current_translation_by_user[ $cache_key ] = false;
     409                        }
     410
     411                        // No current translation exists or it was translated by me: allow.
     412                        return $current_translation_by_user[ $cache_key ] = true;
     413                }
     414
     415                // Allows usage of the re-implementation below.
     416                if ( GP::$permission->user_can( $args['user'], 'approve', 'translation-set', $translation->translation_set_id ) ) {
     417                        return true;
     418                }
     419
     420                return $verdict;
     421        }
     422
     423        /**
     424         * A GlotPress sub-filter for the permission 'approve' and object 'translation-set'.
     425         *
     426         * @param string|bool $verdict The verdict from an earlier filter.
     427         * @param array       $args    Arguments that describe the object to judge for.
     428         * @return string|bool The verdict for the object.
     429         */
     430        public static function gp_pre_can_user_approve_translation_set( $verdict, $args ) {
     431                // Re-implementation of gp_route_translation_set_permissions_to_validator_permissions().
     432                if ( isset( $args['extra']['set']->id ) && intval( $args['extra']['set']->id ) === intval( $args['object_id'] ) ) {
     433                        $set = $args['extra']['set'];
     434                } else {
     435                        $set = GP::$translation_set->get( $args['object_id'] );
     436                }
     437
     438                if ( $set ) {
     439                        if ( GP::$permission->user_can( $args['user'], 'cross-pte', 'project', $set->project_id ) ) {
     440                                return true;
     441                        }
     442
     443                        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 ) );
     444                }
     445
     446                return $verdict;
     447        }
     448}
  • global.wordpress.org/public_html/wp-content/mu-plugins/roles/js/rosetta-roles.js

    diff --git global.wordpress.org/public_html/wp-content/mu-plugins/roles/js/rosetta-roles.js global.wordpress.org/public_html/wp-content/mu-plugins/roles/js/rosetta-roles.js
    index 7231623c..9be2c8cf 100644
     
    253253                        });
    254254
    255255                        $( '#project-loading' ).remove();
     256                        var firstActive = view.$el.find( 'li.active' );
     257                        if ( ! firstActive.length ) {
     258                                firstActive = view.$el.find( 'li input[type=radio]' );
     259                        }
     260                        if ( ! firstActive.length ) {
     261                                firstActive = view.$el.find( 'li:first-child' );
     262                        }
     263                        firstActive.closest( 'li' ).addClass( 'active' );
    256264
    257265                        this.views.ready();
    258266
  • global.wordpress.org/public_html/wp-content/mu-plugins/roles/rosetta-roles.php

    diff --git global.wordpress.org/public_html/wp-content/mu-plugins/roles/rosetta-roles.php global.wordpress.org/public_html/wp-content/mu-plugins/roles/rosetta-roles.php
    index d15c603d..f19d848c 100644
    if ( ! class_exists( 'GP_Locales' ) ) { 
    1111        require_once GLOTPRESS_LOCALES_PATH;
    1212}
    1313
     14require __DIR__ . '/cross-locale-pte.php';
     15
    1416class Rosetta_Roles {
    1517        /**
    1618         * Endpoint for profiles.wordpress.org updates.
    class Rosetta_Roles { 
    7476                add_action( 'translation_editor_removed', array( $this, 'update_wporg_profile_badge' ) );
    7577
    7678                add_action( 'wp_ajax_rosetta-get-projects', array( $this, 'ajax_rosetta_get_projects' ) );
     79
     80                Cross_Locale_PTE::init_admin();
    7781        }
    7882
    7983        /**
    class Rosetta_Roles { 
    9296
    9397                add_action( 'load-' . $this->translation_editors_page, array( $this, 'load_translation_editors_page' ) );
    9498                add_action( 'load-' . $this->translation_editors_page, array( $this, 'register_screen_options' ) );
    95                 add_action( 'admin_print_scripts-' . $this->translation_editors_page, array( $this, 'enqueue_scripts' ) );
    96                 add_action( 'admin_footer-' . $this->translation_editors_page, array( $this, 'print_js_templates' ) );
    97                 add_action( 'admin_print_styles-' . $this->translation_editors_page, array( $this, 'enqueue_styles' ) );
     99                add_action( 'admin_print_scripts-' . $this->translation_editors_page, array( __CLASS__, 'enqueue_scripts' ) );
     100                add_action( 'admin_footer-' . $this->translation_editors_page, array( __CLASS__, 'print_js_templates' ) );
     101                add_action( 'admin_print_styles-' . $this->translation_editors_page, array( __CLASS__, 'enqueue_styles' ) );
    98102        }
    99103
    100104        /**
    101105         * Enqueues scripts.
    102106         */
    103         public function enqueue_scripts() {
     107        public static function enqueue_scripts() {
    104108                wp_enqueue_script( 'string_score', plugins_url( '/js/string_score.min.js', __FILE__ ), array(), '0.1.22', true );
    105                 wp_enqueue_script( 'rosetta-roles', plugins_url( '/js/rosetta-roles.js', __FILE__ ), array( 'jquery', 'wp-backbone', 'string_score' ), '9', true );
     109                wp_enqueue_script( 'rosetta-roles', plugins_url( '/js/rosetta-roles.js', __FILE__ ), array( 'jquery', 'wp-backbone', 'string_score' ), '10', true );
    106110        }
    107111
    108112        /**
    109113         * Enqueues styles.
    110114         */
    111         public function enqueue_styles() {
     115        public static function enqueue_styles() {
    112116                $suffix = is_rtl() ? '-rtl' : '';
    113117                wp_enqueue_style( 'rosetta-roles', plugins_url( "/css/rosetta-roles$suffix.css", __FILE__ ), array(), '4' );
    114118        }
    class Rosetta_Roles { 
    116120        /**
    117121         * Prints JavaScript templates.
    118122         */
    119         public function print_js_templates() {
     123        public static function print_js_templates() {
    120124                ?>
    121125                <script id="tmpl-project-checkbox" type="text/html">
    122126                        <# if ( ! data.checkedSubProjects ) {
  • new file global.wordpress.org/public_html/wp-content/mu-plugins/roles/views/cross-locale-pte.php

    diff --git global.wordpress.org/public_html/wp-content/mu-plugins/roles/views/cross-locale-pte.php global.wordpress.org/public_html/wp-content/mu-plugins/roles/views/cross-locale-pte.php
    new file mode 100644
    index 00000000..231d18d3
    - +  
     1<div class="wrap">
     2        <h2>
     3                <?php _e( 'Cross-Locale PTEs', 'rosetta' ); ?>
     4        </h2>
     5
     6        <?php echo $feedback_message; ?>
     7
     8        <p><?php _e( 'This is the list of our current Cross-Locale PTEs.', 'rosetta' ); ?></p>
     9        <table class="wp-list-table widefat fixed striped translation-editors">
     10                <thead>
     11                <tr>
     12                        <th scope="col" id="username" class="column-username column-primary">Username</th>
     13                        <th scope="col" id="name" class="column-name">Name</th>
     14                        <th scope="col" id="email" class="column-email">E-mail</th>
     15                        <th scope="col" id="projects" class="column-projects">Projects</th></tr>
     16                </thead>
     17
     18                <tbody id="the-list">
     19                <?php $url = menu_page_url( 'cross-locale-pte', false );  ?>
     20                <?php foreach ( $cross_locale_pte_users as $user_id => $user ) : ?>
     21                        <tr>
     22                                <td class="username column-username column-primary">
     23                                        <?php echo get_avatar( $user_id, 32 ); ?>
     24                                        <a href="<?php echo esc_url( add_query_arg( array( 'user_id' => $user_id ), $url ) ); ?>"><?php echo esc_html( $user->user_login ); ?></a>
     25                                </td>
     26                                <td class="name column-name">
     27                                        <?php echo esc_html( $user->display_name ); ?>
     28                                </td>
     29                                <td class="email column-email">
     30                                        <a href="mailto:<?php echo esc_attr( $user->email ); ?>"><?php echo esc_html( $user->email ); ?></a>
     31                                </td>
     32                                <td class="projects column-projects"><?php
     33                                        asort( $user->projects );
     34                                        echo implode( ', ', array_map( 'esc_html', $user->projects ) );
     35                                        ?>
     36                                </td>
     37                        </tr>
     38                <?php endforeach; ?>
     39                </tbody>
     40        </table>
     41
     42        <?php if ( current_user_can( Rosetta_Roles::MANAGE_TRANSLATION_EDITORS_CAP ) ) : ?>
     43                <h3><?php _e( 'Add Cross-Locale PTE', 'rosetta' ); ?></h3>
     44                <p><?php _e( 'Enter the email address or username of an existing user on wordpress.org.', 'rosetta' ); ?></p>
     45                <form action="" method="post">
     46                        <table class="form-table">
     47                                <tr>
     48                                        <th scope="row"><label for="user"><?php _e( 'E-mail or Username', 'rosetta' ); ?></label></th>
     49                                        <td><input type="text" class="regular-text" name="user" id="user"></td>
     50                                </tr>
     51                        </table>
     52                        <input type="hidden" name="action" value="cross-locale-pte">
     53                        <?php wp_nonce_field( 'cross-locale-pte', '_nonce_cross-locale-pte' ) ?>
     54                        <?php submit_button( __( 'Add Cross-Locale PTE', 'rosetta' ) ); ?>
     55                </form>
     56        <?php endif; ?>
     57</div>
  • new file global.wordpress.org/public_html/wp-content/mu-plugins/roles/views/edit-cross-locale-pte.php

    diff --git global.wordpress.org/public_html/wp-content/mu-plugins/roles/views/edit-cross-locale-pte.php global.wordpress.org/public_html/wp-content/mu-plugins/roles/views/edit-cross-locale-pte.php
    new file mode 100644
    index 00000000..d6726266
    - +  
     1<div class="wrap">
     2        <h2><?php _e( 'Edit Cross-Locale PTE', 'rosetta' ); ?></h2>
     3
     4        <?php echo $feedback_message; ?>
     5
     6        <p><?php
     7                printf(
     8                        /* translators: %s: username */
     9                        __( 'You are currently editing the user %s.', 'rosetta' ),
     10                        '<em>' . $user->user_login . '</em>'
     11                );
     12        ?></p>
     13
     14        <form method="post">
     15                <table class="form-table">
     16                        <tbody>
     17                                <tr>
     18                                        <th scope="row">
     19                                                <?php _e( 'Add cross-locale PTE access for:', 'rosetta' ); ?><br>
     20                                        </th>
     21                                        <td>
     22                                                <fieldset id="projects">
     23                                                        <legend class="screen-reader-text"><span><?php _e( 'Add cross-locale PTE access for:', 'rosetta' ); ?></span></legend>
     24
     25                                                        <ul id="projects-list" class="projects-list">
     26                                                                <li id="project-loading" class="loading">
     27                                                                        <?php _e( 'Loading&hellip;', 'rosetta' ); ?>
     28                                                                </li>
     29                                                        </ul>
     30                                                </fieldset>
     31                                                <p class="description"><?php _e( 'Each project includes sub projects and newly-added sub projects.', 'rosetta' ); ?></p>
     32                                        </td>
     33                                </tr>
     34                        </tbody>
     35                </table>
     36
     37                <input type="hidden" id="project-access-list" name="projects" value="<?php esc_attr( implode( ',', $project_access_list ) ); ?>">
     38                <input type="hidden" name="action" value="update-cross-locale-pte" />
     39                <input type="hidden" name="user_id" value="<?php echo esc_attr( $user->ID ); ?>" />
     40                <?php
     41                wp_nonce_field( 'update-cross-locale-pte_' . $user->ID );
     42                submit_button( _x( 'Update', 'translation editor', 'rosetta' ) );
     43                ?>
     44        </form>
     45</div>
  • wordpress.org/public_html/wp-content/plugins/wporg-gp-rosetta-roles/inc/class-plugin.php

    diff --git wordpress.org/public_html/wp-content/plugins/wporg-gp-rosetta-roles/inc/class-plugin.php wordpress.org/public_html/wp-content/plugins/wporg-gp-rosetta-roles/inc/class-plugin.php
    index 3c990d0a..7325966d 100644
    namespace WordPressdotorg\GlotPress\Rosetta_Roles; 
    44
    55use GP;
    66use GP_Locales;
     7use Cross_Locale_PTE;
     8
     9require_once WPMU_PLUGIN_DIR . '/rosetta-network/roles/cross-locale-pte.php';
    710
    811class Plugin {
    912
    class Plugin { 
    6770                add_filter( 'gp_pre_can_user', array( $this, 'pre_can_user' ), 9 , 2 );
    6871                add_action( 'gp_project_created', array( $this, 'project_created' ) );
    6972                add_action( 'gp_project_saved', array( $this, 'project_saved' ) );
     73                add_filter( 'gp_translation_set_import_status', array( 'Cross_Locale_PTE', 'gp_translation_set_import_status' ), 9, 3 );
    7074
    7175                if ( is_admin() ) {
    7276                        $users = new Admin\Translators();
    class Plugin { 
    98102                        return true;
    99103                }
    100104
     105                // Grant permissions to Cross-Locale PTEs.
     106                $cross_locale_pte_verdict = Cross_Locale_PTE::gp_pre_can_user( $verdict, $args );
     107                if ( is_bool( $cross_locale_pte_verdict ) ) {
     108                        return $cross_locale_pte_verdict;
     109                }
     110
    101111                // No permissions for unknown object types.
    102112                if ( ! in_array( $args['object_type'], array( 'project|locale|set-slug', 'translation-set' ) ) ) {
    103113                        return false;