[ Index ]

PHP Cross Reference of GlotPress

title

Body

[close]

/gp-includes/things/ -> original.php (source)

   1  <?php
   2  /**
   3   * Things: GP_Original class
   4   *
   5   * @package GlotPress
   6   * @subpackage Things
   7   * @since 1.0.0
   8   */
   9  
  10  /**
  11   * Core class used to implement the originals.
  12   *
  13   * @since 1.0.0
  14   */
  15  class GP_Original extends GP_Thing {
  16  
  17      var $table_basename           = 'gp_originals';
  18      var $field_names              = array( 'id', 'project_id', 'context', 'singular', 'plural', 'references', 'comment', 'status', 'priority', 'date_added' );
  19      var $int_fields               = array( 'id', 'project_id', 'priority' );
  20      var $non_updatable_attributes = array( 'id', 'path' );
  21  
  22      public $id;
  23      public $project_id;
  24      public $context;
  25      public $singular;
  26      public $plural;
  27      public $references;
  28      public $comment;
  29      public $status;
  30      public $priority;
  31      public $date_added;
  32  
  33      static $priorities = array(
  34          '-2' => 'hidden',
  35          '-1' => 'low',
  36          '0'  => 'normal',
  37          '1'  => 'high',
  38      );
  39  
  40      static $count_cache_group = 'active_originals_count_by_project_id';
  41  
  42      /**
  43       * Sets restriction rules for fields.
  44       *
  45       * @since 1.0.0
  46       *
  47       * @param GP_Validation_Rules $rules The validation rules instance.
  48       */
  49  	public function restrict_fields( $rules ) {
  50          $rules->singular_should_not_be( 'empty_string' );
  51          $rules->status_should_not_be( 'empty' );
  52          $rules->project_id_should_be( 'positive_int' );
  53          $rules->priority_should_be( 'int' );
  54          $rules->priority_should_be( 'between', -2, 1 );
  55      }
  56  
  57      /**
  58       * Normalizes an array with key-value pairs representing
  59       * a GP_Original object.
  60       *
  61       * @since 1.0.0
  62       *
  63       * @param array $args Arguments for a GP_Original object.
  64       * @return array Normalized arguments for a GP_Original object.
  65       */
  66  	public function normalize_fields( $args ) {
  67          foreach ( array( 'plural', 'context', 'references', 'comment' ) as $field ) {
  68              if ( isset( $args['parent_project_id'] ) ) {
  69                  $args[ $field ] = $this->force_false_to_null( $args[ $field ] );
  70              }
  71          }
  72  
  73          if ( isset( $args['priority'] ) && ! is_numeric( $args['priority'] ) ) {
  74              $args['priority'] = $this->priority_by_name( $args['priority'] );
  75              if ( is_null( $args['priority'] ) ) {
  76                  unset( $args['priority'] );
  77              }
  78          }
  79  
  80          $args = parent::normalize_fields( $args );
  81  
  82          return $args;
  83      }
  84  
  85  	public function by_project_id( $project_id ) {
  86          return $this->many( "SELECT * FROM $this->table WHERE project_id= %d AND status = '+active'", $project_id );
  87      }
  88  
  89      /**
  90       * Retrieves the number of originals for a project.
  91       *
  92       * @since 1.0.0
  93       * @since 2.1.0 Added the `$type` parameter.
  94       *
  95       * @param int    $project_id The ID of a project.
  96       * @param string $type       The return type. 'total' for public and hidden counts, 'hidden'
  97       *                           for hidden count, 'public' for public count, 'all' for all three
  98       *                           values. Default 'total'.
  99       * @return object|int Object when `$type` is 'all', non-negative integer in all other cases.
 100       */
 101  	public function count_by_project_id( $project_id, $type = 'total' ) {
 102          global $wpdb;
 103  
 104          // If an unknown type has been passed in, just return a 0 result immediately instead of running the SQL code.
 105          if ( ! in_array( $type, array( 'total', 'hidden', 'public', 'all' ), true ) ) {
 106              return 0;
 107          }
 108  
 109          // Get the cache and use it if possible.
 110          $cached = wp_cache_get( $project_id, self::$count_cache_group );
 111          if ( false !== $cached && is_object( $cached ) ) { // Since 2.1.0 stdClass.
 112              if ( 'all' === $type ) {
 113                  return $cached;
 114              } elseif ( isset( $cached->$type ) ) {
 115                  return $cached->$type;
 116              }
 117  
 118              // If we've fallen through for some reason, make sure to return an integer 0.
 119              return 0;
 120          }
 121  
 122          // No cache values found so let's query the database for the results.
 123          $counts = $wpdb->get_row(
 124              $wpdb->prepare(
 125                  "SELECT
 126                      COUNT(*) AS total,
 127                      COUNT( CASE WHEN priority = '-2' THEN priority END ) AS `hidden`,
 128                      COUNT( CASE WHEN priority <> '-2' THEN priority END ) AS `public`
 129                  FROM {$wpdb->gp_originals}
 130                  WHERE
 131                      project_id = %d AND status = '+active'",
 132                  $project_id
 133              ),
 134              ARRAY_A
 135          );
 136  
 137          // Make sure $wpdb->get_row() returned an array, if not set all results to 0.
 138          if ( ! is_array( $counts ) ) {
 139              $counts = array(
 140                  'total'  => 0,
 141                  'hidden' => 0,
 142                  'public' => 0,
 143              );
 144          }
 145  
 146          // Make sure counts are integers.
 147          $counts = (object) array_map( 'intval', $counts );
 148  
 149          wp_cache_set( $project_id, $counts, self::$count_cache_group );
 150  
 151          if ( 'all' === $type ) {
 152              return $counts;
 153          } elseif ( isset( $counts->$type ) ) {
 154              return $counts->$type;
 155          }
 156  
 157          // If we've fallen through for some reason, make sure to return an integer 0.
 158          return 0;
 159      }
 160  
 161  	public function by_project_id_and_entry( $project_id, $entry, $status = null ) {
 162          global $wpdb;
 163  
 164          $entry->plural  = isset( $entry->plural ) ? $entry->plural : null;
 165          $entry->context = isset( $entry->context ) ? $entry->context : null;
 166  
 167          $where = array();
 168          // now each condition has to contain a %s not to break the sequence
 169          $where[] = is_null( $entry->context ) ? '(context IS NULL OR %s IS NULL)' : 'context = BINARY %s';
 170          $where[] = 'singular = BINARY %s';
 171          $where[] = is_null( $entry->plural ) ? '(plural IS NULL OR %s IS NULL)' : 'plural = BINARY %s';
 172          $where[] = 'project_id = %d';
 173  
 174          if ( ! is_null( $status ) ) {
 175              $where[] = $wpdb->prepare( 'status = %s', $status );
 176          }
 177  
 178          $where = implode( ' AND ', $where );
 179  
 180          return $this->one( "SELECT * FROM $this->table WHERE $where", $entry->context, $entry->singular, $entry->plural, $project_id );
 181      }
 182  
 183  	public function import_for_project( $project, $translations ) {
 184          global $wpdb;
 185  
 186          $originals_added = $originals_existing = $originals_obsoleted = $originals_fuzzied = $originals_error = 0;
 187  
 188          $all_originals_for_project = $this->many_no_map( "SELECT * FROM $this->table WHERE project_id= %d", $project->id );
 189          $originals_by_key          = array();
 190          foreach ( $all_originals_for_project as $original ) {
 191              $entry = new Translation_Entry(
 192                  array(
 193                      'singular' => $original->singular,
 194                      'plural'   => $original->plural,
 195                      'context'  => $original->context,
 196                  )
 197              );
 198  
 199              $originals_by_key[ $entry->key() ] = $original;
 200          }
 201  
 202          $obsolete_originals = array_filter(
 203              $originals_by_key,
 204              function( $entry ) {
 205                  return ( '-obsolete' == $entry->status );
 206              }
 207          );
 208  
 209          $possibly_added = $possibly_dropped = array();
 210  
 211          foreach ( $translations->entries as $key => $entry ) {
 212              $wpdb->queries = array();
 213  
 214              // Context needs to match VARCHAR(255) in the database schema.
 215              if ( mb_strlen( $entry->context ) > 255 ) {
 216                  $entry->context                         = mb_substr( $entry->context, 0, 255 );
 217                  $translations->entries[ $entry->key() ] = $entry;
 218              }
 219  
 220              $data = array(
 221                  'project_id' => $project->id,
 222                  'context'    => $entry->context,
 223                  'singular'   => $entry->singular,
 224                  'plural'     => $entry->plural,
 225                  'comment'    => $entry->extracted_comments,
 226                  'references' => implode( ' ', $entry->references ),
 227                  'status'     => '+active',
 228              );
 229  
 230              // Set the Priority if specified as a flag.
 231              if ( $entry->flags ) {
 232                  foreach ( self::$priorities as $priority => $text ) {
 233                      if ( in_array( "gp-priority: {$text}", $entry->flags ) ) {
 234                          $data['priority'] = $priority;
 235                          break;
 236                      }
 237                  }
 238              }
 239  
 240              /**
 241               * Filter the data of an original being imported or updated.
 242               *
 243               * This filter is called twice per each entry. First time during determining if the original
 244               * already exists. The second time it is called before a new original is added or a close
 245               * old match is set fuzzy with this new data.
 246               *
 247               * @since 1.0.0
 248               *
 249               * @param array $data {
 250               *     An array that describes a single entry being imported or updated.
 251               *
 252               *     @type string $project_id Project id to import into.
 253               *     @type string $context    Context information.
 254               *     @type string $singular   Translation string of the singular form.
 255               *     @type string $plural     Translation string of the plural form.
 256               *     @type string $comment    Comment for translators.
 257               *     @type string $references Referenced in code. A single reference is represented by a file
 258               *                              path followed by a colon and a line number. Multiple references
 259               *                              are separated by spaces.
 260               *     @type string $status     Status of the imported original.
 261               * }
 262               * @param Translation_Entry $entry The translation entry.
 263               */
 264              $data = apply_filters( 'gp_import_original_array', $data, $entry );
 265  
 266              // Original exists, let's update it.
 267              if ( isset( $originals_by_key[ $entry->key() ] ) ) {
 268                  $original = $originals_by_key[ $entry->key() ];
 269                  // But only if it's different, like a changed 'references', 'comment', or 'status' field.
 270                  if ( GP::$original->is_different_from( $data, $original ) ) {
 271                      $this->update( $data, array( 'id' => $original->id ) );
 272                      $originals_existing++;
 273                  }
 274              } else {
 275                  // We can't find this in our originals. Let's keep it for later.
 276                  $possibly_added[] = $entry;
 277              }
 278          }
 279  
 280          // Mark missing strings as possible removals.
 281          foreach ( $originals_by_key as $key => $value ) {
 282              if ( '-obsolete' != $value->status && is_array( $translations->entries ) && ! array_key_exists( $key, $translations->entries ) ) {
 283                  $possibly_dropped[ $key ] = $value;
 284              }
 285          }
 286          $comparison_array = array_unique( array_merge( array_keys( $possibly_dropped ), array_keys( $obsolete_originals ) ) );
 287  
 288          $prev_suspend_cache = wp_suspend_cache_invalidation( true );
 289  
 290          foreach ( $possibly_added as $entry ) {
 291              $data = array(
 292                  'project_id' => $project->id,
 293                  'context'    => $entry->context,
 294                  'singular'   => $entry->singular,
 295                  'plural'     => $entry->plural,
 296                  'comment'    => $entry->extracted_comments,
 297                  'references' => implode( ' ', $entry->references ),
 298                  'status'     => '+active',
 299              );
 300  
 301              // Set the Priority if specified as a flag.
 302              if ( $entry->flags ) {
 303                  foreach ( self::$priorities as $priority => $text ) {
 304                      if ( in_array( "gp-priority: {$text}", $entry->flags ) ) {
 305                          $data['priority'] = $priority;
 306                          break;
 307                      }
 308                  }
 309              }
 310  
 311              /** This filter is documented in gp-includes/things/original.php */
 312              $data = apply_filters( 'gp_import_original_array', $data, $entry );
 313  
 314              // Search for match in the dropped strings and existing obsolete strings.
 315              $close_original = $this->closest_original( $entry->key(), $comparison_array );
 316  
 317              // We found a match - probably a slightly changed string.
 318              if ( $close_original ) {
 319                  $original = $originals_by_key[ $close_original ];
 320  
 321                  /**
 322                   * Filters whether to set existing translations to fuzzy.
 323                   *
 324                   * This filter is called when a new  string closely match an existing possibly dropped string.
 325                   *
 326                   * @since 2.3.0
 327                   *
 328                   * @param bool   $do_fuzzy Whether to set existing translations to fuzzy. Default true.
 329                   * @param object $data     The new original data.
 330                   * @param object $original The previous original being replaced.
 331                   */
 332                  $do_fuzzy = apply_filters( 'gp_set_translations_for_original_to_fuzzy', true, (object) $data, $original );
 333  
 334                  // We'll update the old original...
 335                  $this->update( $data, array( 'id' => $original->id ) );
 336  
 337                  // and set existing translations to fuzzy.
 338                  if ( $do_fuzzy ) {
 339                      $this->set_translations_for_original_to_fuzzy( $original->id );
 340                      $originals_fuzzied++;
 341                  } else {
 342                      $originals_existing++;
 343                  }
 344  
 345                  // No need to obsolete it now.
 346                  unset( $possibly_dropped[ $close_original ] );
 347  
 348                  continue;
 349              } else { // Completely new string
 350                  $created = GP::$original->create( $data );
 351  
 352                  if ( ! $created ) {
 353                      $originals_error++;
 354                      continue;
 355                  }
 356  
 357                  $originals_added++;
 358              }
 359          }
 360  
 361          // Mark remaining possibly dropped strings as obsolete.
 362          foreach ( $possibly_dropped as $key => $value ) {
 363              $this->update( array( 'status' => '-obsolete' ), array( 'id' => $value->id ) );
 364              $originals_obsoleted++;
 365          }
 366  
 367          wp_suspend_cache_invalidation( $prev_suspend_cache );
 368  
 369          // Clear cache when the amount of strings are changed.
 370          if ( $originals_added > 0 || $originals_existing > 0 || $originals_fuzzied > 0 || $originals_obsoleted > 0 ) {
 371              wp_cache_delete( $project->id, self::$count_cache_group );
 372              gp_clean_translation_sets_cache( $project->id );
 373          }
 374  
 375          /**
 376           * Fires after originals have been imported.
 377           *
 378           * @since 1.0.0
 379           *
 380           * @param string $project_id          Project ID the import was made to.
 381           * @param int    $originals_added     Number or total originals added.
 382           * @param int    $originals_existing  Number of existing originals updated.
 383           * @param int    $originals_obsoleted Number of originals that were marked as obsolete.
 384           * @param int    $originals_fuzzied   Number of originals that were close matches of old ones and thus marked as fuzzy.
 385           * @param int    $originals_error     Number of originals that were not imported due to an error.
 386           */
 387          do_action( 'gp_originals_imported', $project->id, $originals_added, $originals_existing, $originals_obsoleted, $originals_fuzzied, $originals_error );
 388  
 389          return array( $originals_added, $originals_existing, $originals_fuzzied, $originals_obsoleted, $originals_error );
 390      }
 391  
 392  	public function set_translations_for_original_to_fuzzy( $original_id ) {
 393          $translations = GP::$translation->find_many( "original_id = '$original_id' AND status = 'current'" );
 394          foreach ( $translations as $translation ) {
 395              $translation->set_status( 'fuzzy' );
 396          }
 397      }
 398  
 399  	public function is_different_from( $data, $original = null ) {
 400          if ( ! $original ) {
 401              $original = $this;
 402          }
 403  
 404          foreach ( $data as $field => $value ) {
 405              if ( $original->$field != $value ) {
 406                  return true;
 407              }
 408          }
 409          return false;
 410      }
 411  
 412  	public function priority_by_name( $name ) {
 413          $by_name = array_flip( self::$priorities );
 414          return isset( $by_name[ $name ] ) ? $by_name[ $name ] : null;
 415      }
 416  
 417  	public function closest_original( $input, $other_strings ) {
 418          /**
 419           * Filters the preemptive return value of closest original check.
 420           *
 421           * @since 3.0.0
 422           *
 423           * @param string|false|null $pre           A preemptive return value of closest original
 424           *                                         check. Default false.
 425           * @param string            $input         Input string.
 426           * @param array             $other_strings List of strings to check against input string.
 427           */
 428          $pre = apply_filters( 'gp_pre_closest_original', false, $input, $other_strings );
 429          if ( false !== $pre ) {
 430              return $pre;
 431          }
 432  
 433          if ( empty( $other_strings ) ) {
 434              return null;
 435          }
 436  
 437          $input_length       = mb_strlen( $input );
 438          $closest_similarity = 0;
 439  
 440          foreach ( $other_strings as $compared_string ) {
 441              $compared_string_length = mb_strlen( $compared_string );
 442  
 443              /**
 444               * Filter the maximum length difference allowed when comparing originals for a close match when importing.
 445               *
 446               * @since 1.0.0
 447               *
 448               * @param float $max_length_diff The times compared string length can differ from the input string.
 449               */
 450              $max_length_diff = apply_filters( 'gp_original_import_max_length_diff', 0.5 );
 451  
 452              if ( abs( ( $input_length - $compared_string_length ) / $input_length ) > $max_length_diff ) {
 453                  continue;
 454              }
 455  
 456              $similarity = gp_string_similarity( $input, $compared_string );
 457  
 458              if ( $similarity > $closest_similarity ) {
 459                  $closest            = $compared_string;
 460                  $closest_similarity = $similarity;
 461              }
 462          }
 463  
 464          if ( ! isset( $closest ) ) {
 465              return null;
 466          }
 467  
 468          /**
 469           * Filter the minimum allowed similarity to be considered as a close match.
 470           *
 471           * @since 1.0.0
 472           *
 473           * @param float $similarity Minimum allowed similarity.
 474           */
 475          $min_score    = apply_filters( 'gp_original_import_min_similarity_diff', 0.8 );
 476          $close_enough = ( $closest_similarity > $min_score );
 477  
 478          /**
 479           * Fires after determining string similarity.
 480           *
 481           * @since 1.0.0
 482           *
 483           * @param string $input              The original string to match against.
 484           * @param string $closest            Closest matching string.
 485           * @param float  $closest_similarity The similarity between strings that was calculated.
 486           * @param bool   $close_enough       Whether the closest was be determined as close enough match.
 487           */
 488          do_action( 'gp_post_string_similarity_test', $input, $closest, $closest_similarity, $close_enough );
 489  
 490          if ( $close_enough ) {
 491              return $closest;
 492          } else {
 493              return null;
 494          }
 495      }
 496  
 497  	public function get_matching_originals_in_other_projects() {
 498          $where   = array();
 499          $where[] = 'singular = BINARY %s';
 500          $where[] = is_null( $this->plural ) ? '(plural IS NULL OR %s IS NULL)' : 'plural = BINARY %s';
 501          $where[] = is_null( $this->context ) ? '(context IS NULL OR %s IS NULL)' : 'context = BINARY %s';
 502          $where[] = 'project_id != %d';
 503          $where[] = "status = '+active'";
 504          $where   = implode( ' AND ', $where );
 505  
 506          return GP::$original->many( "SELECT * FROM $this->table WHERE $where", $this->singular, $this->plural, $this->context, $this->project_id );
 507      }
 508  
 509      /**
 510       * Deletes an original and all of its translations.
 511       *
 512       * @since 3.0.0
 513       *
 514       * @return bool
 515       */
 516  	public function delete() {
 517          GP::$translation->delete_many( array( 'original_id' => $this->id ) );
 518  
 519          return parent::delete();
 520      }
 521  
 522      // Triggers
 523  
 524      /**
 525       * Executes after creating an original.
 526       *
 527       * @since 1.0.0
 528       *
 529       * @return bool
 530       */
 531  	public function after_create() {
 532          /**
 533           * Fires after a new original is created.
 534           *
 535           * @since 1.0.0
 536           *
 537           * @param GP_original $original The original that was created.
 538           */
 539          do_action( 'gp_original_created', $this );
 540  
 541          return true;
 542      }
 543  
 544      /**
 545       * Executes after saving an original.
 546       *
 547       * @since 2.0.0
 548       * @since 3.0.0 Added the `$original_before` parameter.
 549       *
 550       * @param GP_Original $original_before Original before the update.
 551       * @return bool
 552       */
 553  	public function after_save( $original_before ) {
 554          /**
 555           * Fires after an original is saved.
 556           *
 557           * @since 2.0.0
 558           * @since 3.0.0 Added the `$original_before` parameter.
 559           *
 560           * @param GP_Original $original        Original following the update.
 561           * @param GP_Original $original_before Original before the update.
 562           */
 563          do_action( 'gp_original_saved', $this, $original_before );
 564  
 565          return true;
 566      }
 567  
 568      /**
 569       * Executes after deleting an original.
 570       *
 571       * @since 2.0.0
 572       *
 573       * @return bool
 574       */
 575  	public function after_delete() {
 576          /**
 577           * Fires after an original is deleted.
 578           *
 579           * @since 2.0.0
 580           *
 581           * @param GP_original $original The original that was deleted.
 582           */
 583          do_action( 'gp_original_deleted', $this );
 584  
 585          return true;
 586      }
 587  }
 588  GP::$original = new GP_Original();


Generated: Tue Mar 19 01:01:21 2024 Cross-referenced by PHPXref 0.7.1