[ Index ] |
PHP Cross Reference of GlotPress |
[Summary view] [Print] [Text view]
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();
title
Description
Body
title
Description
Body
title
Description
Body
title
Body
Generated: Thu Nov 21 01:01:07 2024 | Cross-referenced by PHPXref 0.7.1 |