Making WordPress.org

Ticket #1078: translate-api-plugin.diff

File translate-api-plugin.diff, 13.3 KB (added by yoavf, 10 years ago)
  • translate/gp-plugins/gp-translation-extended-api/gp-translation-extended-api.php

     
     1<?php
     2/**
     3 *  Expands the GP API by adding extended Translation endpoints.
     4 *  Ultimate goal here being inclusion in the appropriate parts of GP core.
     5 *  See: https://i18n.wordpress.com/2014/11/12/calypso-translator-scope-and-task-list/#comment-7024
     6 *
     7 *  Put this file in the folder: /glotpress/plugins/
     8 */
     9
     10
     11class GP_Route_Translation_Extended extends GP_Route_Main {
     12
     13        function __construct() {
     14                $this->template_path = dirname( __FILE__ ) . '/templates/';
     15        }
     16
     17        function translations_options_ok() {
     18                $this->tmpl( 'status-ok' );
     19        }
     20
     21
     22        function translations_get_by_originals() {
     23                if ( ! $this->api ) {
     24                        $this->die_with_error( __( "Yer not 'spose ta be here." ), 403 );
     25                }
     26
     27                $project_path          = gp_post( 'project' );
     28                $locale_slug           = gp_post( 'locale_slug' );
     29                $translation_set_slug  = gp_post( 'translation_set_slug', 'default' );
     30                $original_strings      = gp_post( 'original_strings', array() );
     31
     32                if ( ! $project_path || ! $locale_slug || ! $translation_set_slug || ! $original_strings ) {
     33
     34                        $this->die_with_404();
     35                }
     36
     37                $original_strings      = json_decode( $original_strings );
     38
     39                $project_paths = $translation_sets = array();
     40                foreach ( explode( ',', $project_path ) as $project_path ) {
     41
     42                        $project = GP::$project->by_path( $project_path );
     43                        if ( ! $project ) {
     44                                continue;
     45                        }
     46
     47                        $translation_set = GP::$translation_set->by_project_id_slug_and_locale( $project->id, $translation_set_slug, $locale_slug );
     48                        if ( ! $translation_set ) {
     49                                continue;
     50                        }
     51
     52                        $project_paths[ $project->id ] = $project_path;
     53                        $translation_sets[ $project->id ] = $translation_set;
     54                }
     55
     56                if ( empty( $translation_sets ) ) {
     57                        $this->die_with_404();
     58                }
     59                $checked_originals = array();
     60                foreach ( $original_strings as $original ) {
     61                        if ( empty( $original ) || ! property_exists( $original, 'singular' ) ) {
     62                                continue;
     63                        }
     64                        $contexts = array( false );
     65                        if ( property_exists( $original, 'context' )  && $original->context ) {
     66                                if ( is_array( $original->context ) ) {
     67                                        $contexts = $original->context;
     68                                } else {
     69                                        $contexts = array( $original->context );
     70                                }
     71                        }
     72
     73                        foreach ( $contexts as $context ) {
     74                                $key = $original->singular;
     75                                if ( $context ) {
     76                                        $original->context = $context;
     77                                        $key = $original->context . '\u0004' . $key;
     78                                } else {
     79                                        unset( $original->context );
     80                                }
     81
     82                                if ( isset( $checked_originals[ $key ] ) ) {
     83                                        continue;
     84                                }
     85                                $checked_originals[ $key ] = true;
     86
     87                                foreach ( $translation_sets as $project_id => $translation_set ) {
     88                                        $original_record = $this->by_project_id_and_entry( $project_id, $original );
     89                                        if ( ! $original_record ) {
     90                                                continue;
     91                                        }
     92
     93                                        $query_result                    = new stdClass();
     94                                        $query_result->original_id       = $original_record->id;
     95                                        $query_result->original          = $original;
     96                                        $query_result->original_comment  = $original_record->comment;
     97                                        $query_result->project           = $project_paths[ $project_id ];
     98
     99                                        $query_result->translations  = GP::$translation->find_many_no_map( "original_id = '{$query_result->original_id}' AND translation_set_id = '{$translation_set->id}' AND ( status = 'waiting' OR status = 'fuzzy' OR status = 'current' )" );
     100
     101                                        $translations[] = $query_result;
     102                                        continue 2;
     103                                }
     104
     105                                $translations[ 'originals_not_found' ][] = $original;
     106                        }
     107
     108
     109                }
     110                $this->tmpl( 'translations-extended', get_defined_vars(), true );
     111        }
     112
     113        function save_translation() {
     114                if ( ! $this->api ) {
     115                        $this->die_with_error( __( "Yer not 'spose ta be here." ), 403 );
     116                }
     117
     118                $this->logged_in_or_forbidden();
     119
     120                $project_paths         = gp_post( 'project' );
     121                $locale_slug           = gp_post( 'locale_slug' );
     122                $translation_set_slug  = gp_post( 'translation_set_slug', 'default' );
     123
     124                if ( ! $project_paths || ! $locale_slug || ! $translation_set_slug ) {
     125                        $this->die_with_404();
     126                }
     127
     128                $project_ids = array_map( function( $project_path ) {
     129                        return GP::$project->by_path( $project_path )->id;
     130                }, explode( ',', $project_paths ) );
     131
     132                if ( empty( $project_ids ) ) {
     133                        $this->die_with_404();
     134                }
     135
     136
     137                $locale = GP_Locales::by_slug( $locale_slug );
     138                if ( ! $locale ) {
     139                        $this->die_with_404();
     140                }
     141
     142                $output = array();
     143                foreach( gp_post( 'translation', array() ) as $original_id => $translations ) {
     144
     145                        $original = GP::$original->get( $original_id );
     146                        if ( ! $original || ! in_array( $original->project_id, $project_ids ) ) {
     147                                $this->die_with_404();
     148                        }
     149
     150                        $project = GP::$project->get( $original->project_id );
     151
     152                        $translation_set = GP::$translation_set->by_project_id_slug_and_locale( $original->project_id, $translation_set_slug, $locale_slug );
     153                        if ( ! $translation_set ) {
     154                                $this->die_with_404();
     155                        }
     156
     157                        $data = compact('original_id');
     158                        $data['user_id'] = GP::$user->current()->id;
     159                        $data['translation_set_id'] = $translation_set->id;
     160
     161                        foreach( range( 0, GP::$translation->get_static( 'number_of_plural_translations' ) ) as $i ) {
     162                                if ( isset( $translations[$i] ) ) $data["translation_$i"] = $translations[$i];
     163                        }
     164
     165                        $data['warnings'] = GP::$translation_warnings->check( $original->singular, $original->plural, $translations, $locale );
     166
     167                        if ( empty( $data['warnings'] ) && ( $this->can( 'approve', 'translation-set', $translation_set->id ) || $this->can( 'write', 'project', $project->id ) ) ) {
     168                                $data['status'] = 'current';
     169                        } else {
     170                                $data['status'] = 'waiting';
     171                        }
     172
     173                        $existing_translations = GP::$translation->for_translation( $project, $translation_set, 'no-limit', array('original_id' => $original_id, 'status' => 'current_or_waiting' ), array() );
     174                        foreach( $existing_translations as $e ) {
     175                                if ( array_pad( $translations, $locale->nplurals, null ) == $e->translations ) {
     176                                        return $this->die_with_error( __( 'Identical current or waiting translation already exists.' ), 409 );
     177                                }
     178                        }
     179
     180                        $translation = GP::$translation->create( $data );
     181                        if ( ! $translation->validate() ) {
     182                                $error_output = $translation->errors;
     183                                $translation->delete();
     184                                $this->die_with_error( $error_output, 422 );
     185                        }
     186
     187                        do_action( 'extended_api_save', $project, $locale, $translation );
     188
     189                        if ( 'current' == $data['status'] ) {
     190                                $translation->set_status( 'current' );
     191                        }
     192
     193                        gp_clean_translation_set_cache( $translation_set->id );
     194                        $translations = GP::$translation->find_many_no_map( "original_id = '{$original_id}' AND translation_set_id = '{$translation_set->id}' AND ( status = 'waiting' OR status = 'fuzzy' OR status = 'current' )" );
     195                        if ( ! $translations ) {
     196                                $output[$original_id] = false;
     197                        }
     198
     199                        $output[$original_id] = $translations;
     200                }
     201
     202                $translations = $output;
     203                $this->tmpl( 'translations-extended', get_defined_vars(), true );
     204        }
     205
     206        function set_status( $translation_id ) {
     207                if ( ! $this->api ) {
     208                        $this->die_with_error( __( "Yer not 'spose ta be here." ), 403 );
     209                }
     210
     211                $translation = GP::$translation->get( $translation_id );
     212                if ( ! $translation ) {
     213                        $this->die_with_error( 'Translation doesn&#8217;t exist!' );
     214                }
     215
     216                $this->can_approve_translation_or_forbidden( $translation );
     217
     218                $result = $translation->set_status( gp_post( 'status' ) );
     219                if ( ! $result ) {
     220                        $this->die_with_error( 'Error in saving the translation status!' );
     221                }
     222
     223                $translations = $this->translation_record_by_id( $translation_id );
     224                if ( ! $translations ) {
     225                        $this->die_with_error( 'Error in retrieving translation record!' );
     226                }
     227
     228                $this->tmpl( 'translations-extended', get_defined_vars() );
     229        }
     230
     231        private function can_approve_translation_or_forbidden( $translation ) {
     232                $can_reject_self = ( GP::$user->current()->id == $translation->user_id && $translation->status == "waiting" );
     233                if ( $can_reject_self ) {
     234                        return;
     235                }
     236                $this->can_or_forbidden( 'approve', 'translation-set', $translation->translation_set_id );
     237        }
     238
     239        private function translation_record_by_id( $translation_id ) {
     240                global $gpdb;
     241                return $gpdb->get_results( $gpdb->prepare( "SELECT * FROM $gpdb->translations WHERE id = %d", $translation_id ) );
     242        }
     243
     244        // A slightly modified version og GP_Original->by_project_id_and_entry without the BINARY search keyword
     245        // to make sure the index on the gp_originals table is used
     246        private function by_project_id_and_entry( $project_id, $entry, $status = "+active" ) {
     247                global $gpdb;
     248
     249                $entry->plural  = isset( $entry->plural ) ? $entry->plural : null;
     250                $entry->context = isset( $entry->context ) ? $entry->context : null;
     251
     252                $where = array();
     253
     254                $where[] = is_null( $entry->context ) ? '(context IS NULL OR %s IS NULL)' : 'context = %s';
     255                $where[] = 'singular = %s';
     256                $where[] = is_null( $entry->plural ) ? '(plural IS NULL OR %s IS NULL)' : 'plural = %s';
     257                $where[] = 'project_id = %d';
     258                $where[] = $gpdb->prepare( 'status = %s', $status );
     259
     260                $where = implode( ' AND ', $where );
     261
     262                $query = "SELECT * FROM $gpdb->originals WHERE $where";
     263                $result = GP::$original->one( $query, $entry->context, $entry->singular, $entry->plural, $project_id );
     264                if ( ! $result ) {
     265                        return null;
     266                }
     267                // we want case sensitive matching but this can't be done with MySQL while continuing to use the index
     268                // therefore we do an additional check here
     269                if ( $result->singular === $entry->singular ) {
     270                        return $result;
     271                }
     272
     273                // and get the whole result set here and check each entry manually
     274                $results = GP::$original->many( $query . ' AND id != %d', $entry->context, $entry->singular, $entry->plural, $project_id, $result->id );
     275                foreach ( $results as $result ) {
     276                        if ( $result->singular === $entry->singular ) {
     277                                return $result;
     278                        }
     279                }
     280
     281                return null;
     282        }
     283}
     284
     285class GP_Translation_Extended_API_Loader extends GP_Plugin {
     286        function __construct() {
     287                add_action( 'before_request', array( $this, 'add_cors_support' ) );
     288                parent::__construct();
     289                $this->init_new_routes();
     290        }
     291
     292
     293
     294        private function is_whitelisted_domain( $origin_url ) {
     295                $domain = parse_url( $origin_url, PHP_URL_HOST );
     296                if ( 'wordpress.org' == $domain || gp_endswith( $domain, '.wordpress.org' ) ) {
     297                        return true;
     298                }
     299        }
     300
     301        function add_cors_support( $route ) {
     302
     303                if ( ! isset( GP::$current_route->api ) || false == GP::$current_route->api ) {
     304                        return;
     305                }
     306
     307                if ( isset( $_SERVER['HTTP_ORIGIN'] ) && $this->is_whitelisted_domain( $_SERVER['HTTP_ORIGIN'] ) ) {
     308                        header( 'Access-Control-Allow-Origin: ' . esc_url_raw( $_SERVER['HTTP_ORIGIN'] ), true );
     309                        header( 'Access-Control-Allow-Credentials: true', true );
     310                        header( 'Access-Control-Allow-Headers: x-request-debug', true );
     311                }
     312
     313                return;
     314        }
     315
     316
     317
     318
     319        function init_new_routes() {
     320                GP::$router->add( '/translations/-new', array( 'GP_Route_Translation_Extended', 'save_translation' ), 'post' );
     321                GP::$router->add( '/translations/-new', array( 'GP_Route_Translation_Extended', 'translations_options_ok' ), 'options' );
     322                GP::$router->add( '/translations/(\d+)/-set-status', array( 'GP_Route_Translation_Extended', 'set_status' ), 'post' );
     323                GP::$router->add( '/translations/(\d+)/-set-status', array( 'GP_Route_Translation_Extended', 'translations_options_ok' ), 'options' );
     324                GP::$router->add( '/translations/-query-by-originals', array( 'GP_Route_Translation_Extended', 'translations_get_by_originals' ), 'post' );
     325                GP::$router->add( '/translations/-query-by-originals', array( 'GP_Route_Translation_Extended', 'translations_options_ok' ), 'options' );
     326        }
     327}
     328
     329GP::$plugins->translation_extended_api = new GP_Translation_Extended_API_Loader();
  • translate/gp-plugins/gp-translation-extended-api/templates/status-ok.api.php

    Property changes on: translate/gp-plugins/gp-translation-extended-api/gp-translation-extended-api.php
    ___________________________________________________________________
    Added: svn:eol-style
    ## -0,0 +1 ##
    +native
    \ No newline at end of property
     
     1{"status":"ok"}
  • translate/gp-plugins/gp-translation-extended-api/templates/translations-extended.api.php

    Property changes on: translate/gp-plugins/gp-translation-extended-api/templates/status-ok.api.php
    ___________________________________________________________________
    Added: svn:eol-style
    ## -0,0 +1 ##
    +native
    \ No newline at end of property
     
     1<?php
     2        echo json_encode( $translations );
     3 No newline at end of file