WordPress.org

Making WordPress.org

Ticket #2000: 2000.2.diff

File 2000.2.diff, 23.8 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 0000000..d58d59c
    - +  
     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                $values_to_add = array();
     142                foreach ( $projects_to_add as $project_id ) {
     143                        $values_to_add[] = $wpdb->prepare( '(%d, %d, %s)',
     144                                self::$user->ID,
     145                                $project_id,
     146                                self::ALL_LOCALES
     147                        );
     148                }
     149
     150                if ( $values_to_add ) {
     151                        $wpdb->query( "
     152                                INSERT INTO {$wpdb->wporg_translation_editors}
     153                                ( `user_id`,`project_id`, `locale` )
     154                                VALUES " . implode( ', ', $values_to_add )
     155                        );
     156                }
     157
     158                $values_to_remove = array_map( 'intval', $projects_to_remove );
     159                if ( $values_to_remove ) {
     160                        $wpdb->query( $wpdb->prepare( "
     161                                DELETE FROM {$wpdb->wporg_translation_editors}
     162                                WHERE `user_id` = %d AND `locale` = %s
     163                                AND project_id IN (" . implode( ', ', $values_to_remove ) . ')',
     164                        self::$user->ID, self::ALL_LOCALES ) );
     165                }
     166
     167                wp_redirect( add_query_arg( array( 'user_id' => self::$user->ID ), menu_page_url( 'cross-locale-pte', false ) ) );
     168                exit;
     169        }
     170
     171        /**
     172         * Render the page to edit a single Cross-Locale PTE.
     173         */
     174        public static function render_edit_page() {
     175                $user = self::$user;
     176                $project_access_list = self::get_users_projects( $user->ID );
     177                $last_updated = get_blog_option( WPORG_TRANSLATE_BLOGID, 'wporg_projects_last_updated' );
     178
     179                wp_localize_script( 'rosetta-roles', '_rosettaProjectsSettings', array(
     180                        'l10n' => array(
     181                                'searchPlaceholder' => esc_attr__( 'Search...', 'rosetta' ),
     182                        ),
     183                        'lastUpdated' => $last_updated,
     184                        'accessList' => $project_access_list,
     185                ) );
     186
     187                $feedback_message = '';
     188                require __DIR__ . '/views/edit-cross-locale-pte.php';
     189        }
     190
     191        /**
     192         * Retrieves the projects for which a user has cross-locale PTE permissions.
     193         *
     194         * @param int $user_id User ID.
     195         * @return array List of project IDs.
     196         */
     197        public static function get_users_projects( $user_id ) {
     198                global $wpdb;
     199
     200                $projects = $wpdb->get_col( $wpdb->prepare( "
     201                        SELECT project_id FROM
     202                        {$wpdb->wporg_translation_editors}
     203                        WHERE user_id = %d AND locale = %s
     204                ", $user_id, self::ALL_LOCALES ) );
     205
     206                return $projects;
     207        }
     208        /**
     209         * Retrieves the projects for which a user has cross-locale PTE permissions.
     210         *
     211         * @return array List of User IDs.
     212         */
     213        public static function get_all_users() {
     214                global $wpdb;
     215
     216                $rows = $wpdb->get_results( $wpdb->prepare( "
     217                        SELECT te.user_id, te.project_id, p.name AS project_name FROM
     218                        {$wpdb->wporg_translation_editors} te
     219                        JOIN translate_projects p ON te.project_id = p.id
     220                        WHERE te.locale = %s
     221                ", self::ALL_LOCALES ) );
     222
     223                $user_ids = array();
     224                foreach ( $rows as $row ) {
     225                        if ( ! isset( $user_ids[ $row->user_id ] ) ) {
     226                                $user = get_user_by( 'id', $row->user_id );
     227                                if ( ! $user ) {
     228                                        continue;
     229                                }
     230                                $row->user_login = $user->user_login;
     231                                $row->email = $user->email;
     232                                $row->display_name = $user->display_name;
     233                                $row->projects = array( $row->project_id => $row->project_name );
     234                                $user_ids[ $row->user_id ] = $row;
     235                        } else {
     236                                $user_ids[ $row->user_id ]->projects[ $row->project_id ] = $row->project_name;
     237                        }
     238                }
     239
     240                return $user_ids;
     241        }
     242
     243        /**
     244         * Check for the Cross-Locale PTE permission for the project.
     245         *
     246         * @param int $user_id    The User ID.
     247         * @param int $project_id The Project ID.
     248         * @return string|bool The verdict.
     249         */
     250        public static function user_has_cross_locale_permission( $user_id, $project_id ) {
     251                static $cache = null;
     252
     253                if ( null === $cache ) {
     254                        $cache = array();
     255                }
     256
     257                $user_id = intval( $user_id );
     258                $project_id = intval( $project_id );
     259
     260                if ( isset( $cache[ $user_id ][ $project_id ] ) ) {
     261                        return $cache[ $user_id ][ $project_id ];
     262                }
     263
     264                if ( ! isset( $cache[ $user_id ] ) ) {
     265                        $cache[ $user_id ] = array();
     266                }
     267
     268                global $wpdb;
     269                $result = $wpdb->get_col( $wpdb->prepare( "
     270                        SELECT user_id FROM
     271                        {$wpdb->wporg_translation_editors}
     272                        WHERE user_id = %d AND project_id = %d AND locale = %s
     273                ", $user_id, $project_id, self::ALL_LOCALES ) );
     274
     275                if ( $result && intval( $result[0]->user_id ) === $user_id ) {
     276                        return $cache[ $user_id ][ $project_id ] = true;
     277                }
     278
     279                return $cache[ $user_id ][ $project_id ] = false;
     280        }
     281
     282        /**
     283         * Enforce not-overwriting current translation by others while importing.
     284         *
     285         * @param string         $status          The desired status.
     286         * @param GP_Translation $new_translation The new translation.
     287         * @param GP_Translation $old_translation The old translation.
     288         * @return string The new status.
     289         */
     290        public static function gp_translation_set_import_status( $status, $new_translation, $old_translation ) {
     291                if ( ! isset( $old_translation->translation_set_id ) ) {
     292                        return $status;
     293                }
     294
     295                if ( 'current' !== $old_translation->translation_status ) {
     296                        return $status;
     297                }
     298
     299                if ( GP::$permission->current_user_can( 'cross-pte', 'translation-set', $old_translation->translation_set_id ) ) {
     300                        // Set to waiting if a current translation exists by another user.
     301                        if ( intval( $old_translation->user_id ) !== intval( get_current_user_id() ) ) {
     302                                return 'waiting';
     303                        }
     304                }
     305                return $status;
     306        }
     307
     308        /**
     309         * The GlotPress filter for Cross-Locale PTE.
     310         *
     311         * A Cross-Locale PTE is defined through an entry in the permission table 'cross-pte' with the
     312         * object_id referring to a project id.
     313         * A user with this permission will have 'approve' rights for all translation-sets within this
     314         * project. Usually having approval rights for a translation-set also means that the user has
     315         * approval rights for all translations, but not a Cross-Locale PTE:
     316         * If a current translation exists by another user then overwriting (through UI or import) is not
     317         * possible.
     318         *
     319         * @param string|bool $verdict The verdict from an earlier filter.
     320         * @param array       $args    Arguments that describe the object to judge for.
     321         * @return string|bool The verdict for the object.
     322         */
     323        public static function gp_pre_can_user( $verdict, $args ) {
     324                if ( 'cross-pte' === $args['action'] ) {
     325                        $verdict = self::gp_pre_can_user_cross_pte( $verdict, $args );
     326
     327                        if ( is_bool( $verdict ) ) {
     328                                return $verdict;
     329                        }
     330                }
     331
     332                if ( 'approve' === $args['action'] ) {
     333                        if ( 'translation' === $args['object_type'] ) {
     334                                $verdict = self::gp_pre_can_user_approve_translation( $verdict, $args );
     335                        } elseif ( 'translation-set' === $args['object_type'] ) {
     336                                $verdict = self::gp_pre_can_user_approve_translation_set( $verdict, $args );
     337                        }
     338                }
     339
     340                return $verdict;
     341
     342        }
     343
     344        /**
     345         * A GlotPress sub-filter for the permission 'cross-lte'.
     346         *
     347         * @param string|bool $verdict The verdict from an earlier filter.
     348         * @param array       $args    Arguments that describe the object to judge for.
     349         * @return string|bool The verdict for the object.
     350         */
     351        public static function gp_pre_can_user_cross_pte( $verdict, $args ) {
     352                if ( GP::$permission->user_can( $args['user'], 'admin' ) ) {
     353                        // Admins shouldn't have this because it will end up restricting them.
     354                        return false;
     355                }
     356
     357                if ( 'translation-set' === $args['object_type'] ) {
     358                        if ( isset( $args['extra']['set']->id ) && intval( $args['extra']['set']->id ) === intval( $args['object_id'] ) ) {
     359                                $set = $args['extra']['set'];
     360                        } else {
     361                                $set = GP::$translation_set->get( $args['object_id'] );
     362                        }
     363
     364                        // Allow on all translation-sets within the project.
     365                        if ( $set ) {
     366                                return GP::$permission->user_can( $args['user'], 'cross-pte', 'project', $set->project_id );
     367                        }
     368                } elseif ( 'project' === $args['object_type'] ) {
     369                        return self::user_has_cross_locale_permission( $args['user'], $args['object_id'] );
     370                }
     371
     372                return $verdict;
     373        }
     374
     375        /**
     376         * A GlotPress sub-filter for the permission 'approve' and object 'translation'.
     377         *
     378         * @param string|bool $verdict The verdict from an earlier filter.
     379         * @param array       $args    Arguments that describe the object to judge for.
     380         * @return string|bool The verdict for the object.
     381         */
     382        public static function gp_pre_can_user_approve_translation( $verdict, $args ) {
     383                if ( isset( $args['extra']['translation']->translation_set_id ) && intval( $args['extra']['translation']->id ) === intval( $args['object_id'] ) ) {
     384                        $translation = $args['extra']['translation'];
     385                } else {
     386                        $translation = GP::$translation->get( $args['object_id'] );
     387                }
     388
     389                if ( ! $translation ) {
     390                        return $verdict;
     391                }
     392
     393                static $current_translation_by_user;
     394                $cache_key = $args['user']->ID . '_' . $translation->original_id;
     395
     396                if ( isset( $current_translation_by_user[ $cache_key ] ) ) {
     397                        return $current_translation_by_user[ $cache_key ];
     398                }
     399
     400                if ( GP::$permission->user_can( $args['user'], 'cross-pte', 'translation-set', $translation->translation_set_id ) ) {
     401                        $current_translation = GP::$translation->find_one( array( 'translation_set_id' => $translation->translation_set_id, 'original_id' => $translation->original_id, 'status' => 'current' ) );
     402                        if ( $current_translation && intval( $current_translation->user_id ) !== $args['user']->ID ) {
     403                                // Current translation was authored by someone else. Disallow setting to current.
     404                                return $current_translation_by_user[ $cache_key ] = false;
     405                        }
     406
     407                        // No current translation exists or it was translated by me: allow.
     408                        return $current_translation_by_user[ $cache_key ] = true;
     409                }
     410
     411                // Allows usage of the re-implementation below.
     412                if ( GP::$permission->user_can( $args['user'], 'approve', 'translation-set', $translation->translation_set_id ) ) {
     413                        return true;
     414                }
     415
     416                return $verdict;
     417        }
     418
     419        /**
     420         * A GlotPress sub-filter for the permission 'approve' and object 'translation-set'.
     421         *
     422         * @param string|bool $verdict The verdict from an earlier filter.
     423         * @param array       $args    Arguments that describe the object to judge for.
     424         * @return string|bool The verdict for the object.
     425         */
     426        public static function gp_pre_can_user_approve_translation_set( $verdict, $args ) {
     427                // Re-implementation of gp_route_translation_set_permissions_to_validator_permissions().
     428                if ( isset( $args['extra']['set']->id ) && intval( $args['extra']['set']->id ) === intval( $args['object_id'] ) ) {
     429                        $set = $args['extra']['set'];
     430                } else {
     431                        $set = GP::$translation_set->get( $args['object_id'] );
     432                }
     433
     434                if ( $set ) {
     435                        if ( GP::$permission->user_can( $args['user'], 'cross-pte', 'project', $set->project_id ) ) {
     436                                return true;
     437                        }
     438
     439                        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 ) );
     440                }
     441
     442                return $verdict;
     443        }
     444}
  • 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 7231623..9be2c8c 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 d15c603..f19d848 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 0000000..231d18d
    - +  
     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 0000000..d672626
    - +  
     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 3c990d0..1ed519e 100644
    class Plugin { 
    6767                add_filter( 'gp_pre_can_user', array( $this, 'pre_can_user' ), 9 , 2 );
    6868                add_action( 'gp_project_created', array( $this, 'project_created' ) );
    6969                add_action( 'gp_project_saved', array( $this, 'project_saved' ) );
     70                add_filter( 'gp_translation_set_import_status', array( 'Cross_Locale_PTE', 'gp_translation_set_import_status' ), 9, 3 );
    7071
    7172                if ( is_admin() ) {
    7273                        $users = new Admin\Translators();
    class Plugin { 
    9899                        return true;
    99100                }
    100101
     102                // Grant permissions to Cross-Locale PTEs.
     103                $cross_locale_pte_verdict = Cross_Locale_PTE::gp_pre_can_user( $verdict, $args );
     104                if ( is_bool( $cross_locale_pte_verdict ) ) {
     105                        return $cross_locale_pte_verdict;
     106                }
     107
    101108                // No permissions for unknown object types.
    102109                if ( ! in_array( $args['object_type'], array( 'project|locale|set-slug', 'translation-set' ) ) ) {
    103110                        return false;