| [ Index ] |
PHP Cross Reference of WordPress Trunk (Updated Daily) |
[Summary view] [Print] [Text view]
1 <?php 2 /** 3 * Upgrade API: Language_Pack_Upgrader class 4 * 5 * @package WordPress 6 * @subpackage Upgrader 7 * @since 4.6.0 8 */ 9 10 /** 11 * Core class used for updating/installing language packs (translations) 12 * for plugins, themes, and core. 13 * 14 * @since 3.7.0 15 * @since 4.6.0 Moved to its own file from wp-admin/includes/class-wp-upgrader.php. 16 * 17 * @see WP_Upgrader 18 */ 19 class Language_Pack_Upgrader extends WP_Upgrader { 20 21 /** 22 * Result of the language pack upgrade. 23 * 24 * @since 3.7.0 25 * @var array|WP_Error|null $result 26 * @phpstan-var non-empty-array<string, mixed>|WP_Error|null 27 * @see WP_Upgrader::$result 28 */ 29 public $result; 30 31 /** 32 * Whether a bulk upgrade/installation is being performed. 33 * 34 * @since 3.7.0 35 * @var bool $bulk 36 */ 37 public $bulk = true; 38 39 /** 40 * Asynchronously upgrades language packs after other upgrades have been made. 41 * 42 * Hooked to the {@see 'upgrader_process_complete'} action by default. 43 * 44 * @since 3.7.0 45 * 46 * @param false|WP_Upgrader $upgrader Optional. WP_Upgrader instance or false. If `$upgrader` is 47 * a Language_Pack_Upgrader instance, the method will bail to 48 * avoid recursion. Otherwise unused. Default false. 49 */ 50 public static function async_upgrade( $upgrader = false ) { 51 // Avoid recursion. 52 if ( $upgrader && $upgrader instanceof Language_Pack_Upgrader ) { 53 return; 54 } 55 56 // Nothing to do? 57 $language_updates = wp_get_translation_updates(); 58 if ( ! $language_updates ) { 59 return; 60 } 61 62 /* 63 * Avoid messing with VCS installations, at least for now. 64 * Noted: this is not the ideal way to accomplish this. 65 */ 66 $check_vcs = new WP_Automatic_Updater(); 67 if ( $check_vcs->is_vcs_checkout( WP_CONTENT_DIR ) ) { 68 return; 69 } 70 71 foreach ( $language_updates as $key => $language_update ) { 72 $update = ! empty( $language_update->autoupdate ); 73 74 /** 75 * Filters whether to asynchronously update translation for core, a plugin, or a theme. 76 * 77 * @since 4.0.0 78 * 79 * @param bool $update Whether to update. 80 * @param object $language_update The update offer. 81 */ 82 $update = apply_filters( 'async_update_translation', $update, $language_update ); 83 84 if ( ! $update ) { 85 unset( $language_updates[ $key ] ); 86 } 87 } 88 89 if ( empty( $language_updates ) ) { 90 return; 91 } 92 93 // Re-use the automatic upgrader skin if the parent upgrader is using it. 94 if ( $upgrader && $upgrader->skin instanceof Automatic_Upgrader_Skin ) { 95 $skin = $upgrader->skin; 96 } else { 97 $skin = new Language_Pack_Upgrader_Skin( 98 array( 99 'skip_header_footer' => true, 100 ) 101 ); 102 } 103 104 $lp_upgrader = new Language_Pack_Upgrader( $skin ); 105 $lp_upgrader->bulk_upgrade( $language_updates ); 106 } 107 108 /** 109 * Initializes the upgrade strings. 110 * 111 * @since 3.7.0 112 */ 113 public function upgrade_strings() { 114 $this->strings['starting_upgrade'] = __( 'Some of your translations need updating. Sit tight for a few more seconds while they are updated as well.' ); 115 $this->strings['up_to_date'] = __( 'Your translations are all up to date.' ); 116 $this->strings['no_package'] = __( 'Update package not available.' ); 117 /* translators: %s: Package URL. */ 118 $this->strings['downloading_package'] = sprintf( __( 'Downloading translation from %s…' ), '<span class="code pre">%s</span>' ); 119 $this->strings['unpack_package'] = __( 'Unpacking the update…' ); 120 $this->strings['process_failed'] = __( 'Translation update failed.' ); 121 $this->strings['process_success'] = __( 'Translation updated successfully.' ); 122 $this->strings['remove_old'] = __( 'Removing the old version of the translation…' ); 123 $this->strings['remove_old_failed'] = __( 'Could not remove the old translation.' ); 124 } 125 126 /** 127 * Upgrades a language pack. 128 * 129 * @since 3.7.0 130 * 131 * @param object|false $update Optional. The language pack update offer, or false to upgrade all 132 * available language packs. Default false. 133 * @param array $args Optional. Other optional arguments, see 134 * Language_Pack_Upgrader::bulk_upgrade(). Default empty array. 135 * @return array|bool|WP_Error|null The result of the upgrade, or a WP_Error object instead. Null if the 136 * upgrade failed before the package was installed, for example when 137 * the package could not be downloaded. 138 */ 139 public function upgrade( $update = false, $args = array() ) { 140 if ( $update ) { 141 $update = array( $update ); 142 } 143 144 $results = $this->bulk_upgrade( $update, $args ); 145 146 if ( ! is_array( $results ) ) { 147 return $results; 148 } 149 150 return $results[0]; 151 } 152 153 /** 154 * Upgrades several language packs at once. 155 * 156 * @since 3.7.0 157 * 158 * @global WP_Filesystem_Base $wp_filesystem WordPress filesystem subclass. 159 * 160 * @param object[] $language_updates Optional. Array of language packs to update. See {@see wp_get_translation_updates()}. 161 * Default empty array. 162 * @param array $args { 163 * Other arguments for upgrading multiple language packs. Default empty array. 164 * 165 * @type bool $clear_update_cache Whether to clear the update cache when done. 166 * Default true. 167 * } 168 * @return array|bool|WP_Error Will return an array of results, or true if there are no updates, 169 * false or WP_Error for initial errors. 170 */ 171 public function bulk_upgrade( $language_updates = array(), $args = array() ) { 172 global $wp_filesystem; 173 174 $defaults = array( 175 'clear_update_cache' => true, 176 ); 177 $parsed_args = wp_parse_args( $args, $defaults ); 178 179 $this->init(); 180 $this->upgrade_strings(); 181 182 if ( ! $language_updates ) { 183 $language_updates = wp_get_translation_updates(); 184 } 185 186 if ( empty( $language_updates ) ) { 187 $this->skin->header(); 188 $this->skin->set_result( true ); 189 $this->skin->feedback( 'up_to_date' ); 190 $this->skin->bulk_footer(); 191 $this->skin->footer(); 192 return true; 193 } 194 195 if ( 'upgrader_process_complete' === current_filter() ) { 196 $this->skin->feedback( 'starting_upgrade' ); 197 } 198 199 // Remove any existing upgrade filters from the plugin/theme upgraders #WP29425 & #WP29230. 200 remove_all_filters( 'upgrader_pre_install' ); 201 remove_all_filters( 'upgrader_clear_destination' ); 202 remove_all_filters( 'upgrader_post_install' ); 203 remove_all_filters( 'upgrader_source_selection' ); 204 205 add_filter( 'upgrader_source_selection', array( $this, 'check_package' ), 10, 2 ); 206 207 $this->skin->header(); 208 209 // Connect to the filesystem first. 210 $res = $this->fs_connect( array( WP_CONTENT_DIR, WP_LANG_DIR ) ); 211 if ( ! $res ) { 212 $this->skin->footer(); 213 return false; 214 } 215 216 $results = array(); 217 218 $this->update_count = count( $language_updates ); 219 $this->update_current = 0; 220 221 /* 222 * The filesystem's mkdir() is not recursive. Make sure WP_LANG_DIR exists, 223 * as we then may need to create a /plugins or /themes directory inside of it. 224 */ 225 $remote_destination = $wp_filesystem->find_folder( WP_LANG_DIR ); 226 if ( ! $wp_filesystem->exists( $remote_destination ) ) { 227 if ( ! $wp_filesystem->mkdir( $remote_destination, FS_CHMOD_DIR ) ) { 228 return new WP_Error( 'mkdir_failed_lang_dir', $this->strings['mkdir_failed'], $remote_destination ); 229 } 230 } 231 232 $language_updates_results = array(); 233 234 foreach ( $language_updates as $language_update ) { 235 236 $this->skin->language_update = $language_update; 237 238 $destination = WP_LANG_DIR; 239 if ( 'plugin' === $language_update->type ) { 240 $destination .= '/plugins'; 241 } elseif ( 'theme' === $language_update->type ) { 242 $destination .= '/themes'; 243 } 244 245 ++$this->update_current; 246 247 $options = array( 248 'package' => $language_update->package, 249 'destination' => $destination, 250 'clear_destination' => true, 251 'abort_if_destination_exists' => false, // We expect the destination to exist. 252 'clear_working' => true, 253 'is_multi' => true, 254 'hook_extra' => array( 255 'language_update_type' => $language_update->type, 256 'language_update' => $language_update, 257 ), 258 ); 259 260 $result = $this->run( $options ); 261 262 $results[] = $this->result; 263 264 // Prevent credentials auth screen from displaying multiple times. 265 if ( false === $result ) { 266 break; 267 } 268 269 $language_updates_results[] = array( 270 'language' => $language_update->language, 271 'type' => $language_update->type, 272 'slug' => $language_update->slug ?? 'default', 273 'version' => $language_update->version, 274 ); 275 } 276 277 // Remove upgrade hooks which are not required for translation updates. 278 remove_action( 'upgrader_process_complete', array( 'Language_Pack_Upgrader', 'async_upgrade' ), 20 ); 279 remove_action( 'upgrader_process_complete', 'wp_version_check' ); 280 remove_action( 'upgrader_process_complete', 'wp_update_plugins' ); 281 remove_action( 'upgrader_process_complete', 'wp_update_themes' ); 282 283 /** This action is documented in wp-admin/includes/class-wp-upgrader.php */ 284 do_action( 285 'upgrader_process_complete', 286 $this, 287 array( 288 'action' => 'update', 289 'type' => 'translation', 290 'bulk' => true, 291 'translations' => $language_updates_results, 292 ) 293 ); 294 295 // Re-add upgrade hooks. 296 add_action( 'upgrader_process_complete', array( 'Language_Pack_Upgrader', 'async_upgrade' ), 20 ); 297 add_action( 'upgrader_process_complete', 'wp_version_check', 10, 0 ); 298 add_action( 'upgrader_process_complete', 'wp_update_plugins', 10, 0 ); 299 add_action( 'upgrader_process_complete', 'wp_update_themes', 10, 0 ); 300 301 $this->skin->bulk_footer(); 302 303 $this->skin->footer(); 304 305 // Clean up our hooks, in case something else does an upgrade on this connection. 306 remove_filter( 'upgrader_source_selection', array( $this, 'check_package' ) ); 307 308 if ( $parsed_args['clear_update_cache'] ) { 309 wp_clean_update_cache(); 310 } 311 312 return $results; 313 } 314 315 /** 316 * Checks that the package source contains .mo and .po files. 317 * 318 * Hooked to the {@see 'upgrader_source_selection'} filter by 319 * Language_Pack_Upgrader::bulk_upgrade(). 320 * 321 * @since 3.7.0 322 * 323 * @global WP_Filesystem_Base $wp_filesystem WordPress filesystem subclass. 324 * 325 * @param string|WP_Error $source The path to the downloaded package source. 326 * @param string $remote_source Remote file source location. 327 * @return string|WP_Error The source as passed, or a WP_Error object on failure. 328 */ 329 public function check_package( $source, $remote_source ) { 330 global $wp_filesystem; 331 332 if ( is_wp_error( $source ) ) { 333 return $source; 334 } 335 336 // Check that the folder contains a valid language. 337 $files = $wp_filesystem->dirlist( $remote_source ); 338 339 // Check to see if the expected files exist in the folder. 340 $po = false; 341 $mo = false; 342 $php = false; 343 foreach ( (array) $files as $file => $filedata ) { 344 if ( str_ends_with( $file, '.po' ) ) { 345 $po = true; 346 } elseif ( str_ends_with( $file, '.mo' ) ) { 347 $mo = true; 348 } elseif ( str_ends_with( $file, '.l10n.php' ) ) { 349 $php = true; 350 } 351 } 352 353 if ( $php ) { 354 return $source; 355 } 356 357 if ( ! $mo || ! $po ) { 358 return new WP_Error( 359 'incompatible_archive_pomo', 360 $this->strings['incompatible_archive'], 361 sprintf( 362 /* translators: 1: .po, 2: .mo, 3: .l10n.php */ 363 __( 'The language pack is missing either the %1$s, %2$s, or %3$s files.' ), 364 '<code>.po</code>', 365 '<code>.mo</code>', 366 '<code>.l10n.php</code>' 367 ) 368 ); 369 } 370 371 return $source; 372 } 373 374 /** 375 * Gets the name of an item being updated. 376 * 377 * @since 3.7.0 378 * 379 * @param object $update The data for an update. 380 * @return string The name of the item being updated. 381 */ 382 public function get_name_for_update( $update ) { 383 switch ( $update->type ) { 384 case 'core': 385 return 'WordPress'; // Not translated. 386 387 case 'theme': 388 $theme = wp_get_theme( $update->slug ); 389 if ( $theme->exists() ) { 390 return $theme->get( 'Name' ); 391 } 392 break; 393 case 'plugin': 394 $plugin_data = get_plugins( '/' . $update->slug ); 395 $plugin_data = reset( $plugin_data ); 396 if ( $plugin_data ) { 397 return $plugin_data['Name']; 398 } 399 break; 400 } 401 return ''; 402 } 403 404 /** 405 * Clears existing translations where this item is going to be installed into. 406 * 407 * @since 5.1.0 408 * 409 * @global WP_Filesystem_Base $wp_filesystem WordPress filesystem subclass. 410 * 411 * @param string $remote_destination The location on the remote filesystem to be cleared. 412 * @return bool|WP_Error True upon success, WP_Error on failure. 413 */ 414 public function clear_destination( $remote_destination ) { 415 global $wp_filesystem; 416 417 $language_update = $this->skin->language_update; 418 $language_directory = WP_LANG_DIR . '/'; // Local path for use with glob(). 419 420 if ( 'core' === $language_update->type ) { 421 $files = array( 422 $remote_destination . $language_update->language . '.po', 423 $remote_destination . $language_update->language . '.mo', 424 $remote_destination . $language_update->language . '.l10n.php', 425 $remote_destination . 'admin-' . $language_update->language . '.po', 426 $remote_destination . 'admin-' . $language_update->language . '.mo', 427 $remote_destination . 'admin-' . $language_update->language . '.l10n.php', 428 $remote_destination . 'admin-network-' . $language_update->language . '.po', 429 $remote_destination . 'admin-network-' . $language_update->language . '.mo', 430 $remote_destination . 'admin-network-' . $language_update->language . '.l10n.php', 431 $remote_destination . 'continents-cities-' . $language_update->language . '.po', 432 $remote_destination . 'continents-cities-' . $language_update->language . '.mo', 433 $remote_destination . 'continents-cities-' . $language_update->language . '.l10n.php', 434 ); 435 436 $json_translation_files = glob( $language_directory . $language_update->language . '-*.json' ); 437 if ( $json_translation_files ) { 438 foreach ( $json_translation_files as $json_translation_file ) { 439 $files[] = str_replace( $language_directory, $remote_destination, $json_translation_file ); 440 } 441 } 442 } else { 443 $files = array( 444 $remote_destination . $language_update->slug . '-' . $language_update->language . '.po', 445 $remote_destination . $language_update->slug . '-' . $language_update->language . '.mo', 446 $remote_destination . $language_update->slug . '-' . $language_update->language . '.l10n.php', 447 ); 448 449 $language_directory = $language_directory . $language_update->type . 's/'; 450 $json_translation_files = glob( $language_directory . $language_update->slug . '-' . $language_update->language . '-*.json' ); 451 if ( $json_translation_files ) { 452 foreach ( $json_translation_files as $json_translation_file ) { 453 $files[] = str_replace( $language_directory, $remote_destination, $json_translation_file ); 454 } 455 } 456 } 457 458 $files = array_filter( $files, array( $wp_filesystem, 'exists' ) ); 459 460 // No files to delete. 461 if ( ! $files ) { 462 return true; 463 } 464 465 // Check all files are writable before attempting to clear the destination. 466 $unwritable_files = array(); 467 468 // Check writability. 469 foreach ( $files as $file ) { 470 if ( ! $wp_filesystem->is_writable( $file ) ) { 471 // Attempt to alter permissions to allow writes and try again. 472 $wp_filesystem->chmod( $file, FS_CHMOD_FILE ); 473 if ( ! $wp_filesystem->is_writable( $file ) ) { 474 $unwritable_files[] = $file; 475 } 476 } 477 } 478 479 if ( ! empty( $unwritable_files ) ) { 480 return new WP_Error( 'files_not_writable', $this->strings['files_not_writable'], implode( ', ', $unwritable_files ) ); 481 } 482 483 foreach ( $files as $file ) { 484 if ( ! $wp_filesystem->delete( $file ) ) { 485 return new WP_Error( 'remove_old_failed', $this->strings['remove_old_failed'] ); 486 } 487 } 488 489 return true; 490 } 491 }
title
Description
Body
title
Description
Body
title
Description
Body
title
Body
| Generated : Fri Oct 30 08:20:34 2026 | Cross-referenced by PHPXref |