class-language-pack-upgrader.php 15 KB

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