[ Index ]

PHP Cross Reference of WordPress Trunk (Updated Daily)

Search

title

Body

[close]

/wp-admin/includes/ -> class-language-pack-upgrader.php (source)

   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&#8230;' ), '<span class="code pre">%s</span>' );
 119          $this->strings['unpack_package']      = __( 'Unpacking the update&#8230;' );
 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&#8230;' );
 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  }


Generated : Fri Oct 30 08:20:34 2026 Cross-referenced by PHPXref