theme.php 33 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936
  1. <?php
  2. /**
  3. * WordPress Theme Administration API
  4. *
  5. * @package WordPress
  6. * @subpackage Administration
  7. */
  8. /**
  9. * Remove a theme
  10. *
  11. * @since 2.8.0
  12. *
  13. * @global WP_Filesystem_Base $wp_filesystem WordPress filesystem subclass.
  14. *
  15. * @param string $stylesheet Stylesheet of the theme to delete.
  16. * @param string $redirect Redirect to page when complete.
  17. * @return bool|null|WP_Error True on success, false if `$stylesheet` is empty, WP_Error on failure.
  18. * Null if filesystem credentials are required to proceed.
  19. */
  20. function delete_theme( $stylesheet, $redirect = '' ) {
  21. global $wp_filesystem;
  22. if ( empty( $stylesheet ) ) {
  23. return false;
  24. }
  25. if ( empty( $redirect ) ) {
  26. $redirect = wp_nonce_url( 'themes.php?action=delete&stylesheet=' . urlencode( $stylesheet ), 'delete-theme_' . $stylesheet );
  27. }
  28. ob_start();
  29. $credentials = request_filesystem_credentials( $redirect );
  30. $data = ob_get_clean();
  31. if ( false === $credentials ) {
  32. if ( ! empty( $data ) ) {
  33. include_once( ABSPATH . 'wp-admin/admin-header.php' );
  34. echo $data;
  35. include( ABSPATH . 'wp-admin/admin-footer.php' );
  36. exit;
  37. }
  38. return;
  39. }
  40. if ( ! WP_Filesystem( $credentials ) ) {
  41. ob_start();
  42. request_filesystem_credentials( $redirect, '', true ); // Failed to connect, Error and request again.
  43. $data = ob_get_clean();
  44. if ( ! empty( $data ) ) {
  45. include_once( ABSPATH . 'wp-admin/admin-header.php' );
  46. echo $data;
  47. include( ABSPATH . 'wp-admin/admin-footer.php' );
  48. exit;
  49. }
  50. return;
  51. }
  52. if ( ! is_object( $wp_filesystem ) ) {
  53. return new WP_Error( 'fs_unavailable', __( 'Could not access filesystem.' ) );
  54. }
  55. if ( is_wp_error( $wp_filesystem->errors ) && $wp_filesystem->errors->has_errors() ) {
  56. return new WP_Error( 'fs_error', __( 'Filesystem error.' ), $wp_filesystem->errors );
  57. }
  58. // Get the base plugin folder.
  59. $themes_dir = $wp_filesystem->wp_themes_dir();
  60. if ( empty( $themes_dir ) ) {
  61. return new WP_Error( 'fs_no_themes_dir', __( 'Unable to locate WordPress theme directory.' ) );
  62. }
  63. $themes_dir = trailingslashit( $themes_dir );
  64. $theme_dir = trailingslashit( $themes_dir . $stylesheet );
  65. $deleted = $wp_filesystem->delete( $theme_dir, true );
  66. if ( ! $deleted ) {
  67. return new WP_Error(
  68. 'could_not_remove_theme',
  69. /* translators: %s: Theme name. */
  70. sprintf( __( 'Could not fully remove the theme %s.' ), $stylesheet )
  71. );
  72. }
  73. $theme_translations = wp_get_installed_translations( 'themes' );
  74. // Remove language files, silently.
  75. if ( ! empty( $theme_translations[ $stylesheet ] ) ) {
  76. $translations = $theme_translations[ $stylesheet ];
  77. foreach ( $translations as $translation => $data ) {
  78. $wp_filesystem->delete( WP_LANG_DIR . '/themes/' . $stylesheet . '-' . $translation . '.po' );
  79. $wp_filesystem->delete( WP_LANG_DIR . '/themes/' . $stylesheet . '-' . $translation . '.mo' );
  80. $json_translation_files = glob( WP_LANG_DIR . '/themes/' . $stylesheet . '-' . $translation . '-*.json' );
  81. if ( $json_translation_files ) {
  82. array_map( array( $wp_filesystem, 'delete' ), $json_translation_files );
  83. }
  84. }
  85. }
  86. // Remove the theme from allowed themes on the network.
  87. if ( is_multisite() ) {
  88. WP_Theme::network_disable_theme( $stylesheet );
  89. }
  90. // Force refresh of theme update information.
  91. delete_site_transient( 'update_themes' );
  92. return true;
  93. }
  94. /**
  95. * Get the Page Templates available in this theme
  96. *
  97. * @since 1.5.0
  98. * @since 4.7.0 Added the `$post_type` parameter.
  99. *
  100. * @param WP_Post|null $post Optional. The post being edited, provided for context.
  101. * @param string $post_type Optional. Post type to get the templates for. Default 'page'.
  102. * @return array Key is the template name, value is the filename of the template
  103. */
  104. function get_page_templates( $post = null, $post_type = 'page' ) {
  105. return array_flip( wp_get_theme()->get_page_templates( $post, $post_type ) );
  106. }
  107. /**
  108. * Tidies a filename for url display by the theme editor.
  109. *
  110. * @since 2.9.0
  111. * @access private
  112. *
  113. * @param string $fullpath Full path to the theme file
  114. * @param string $containingfolder Path of the theme parent folder
  115. * @return string
  116. */
  117. function _get_template_edit_filename( $fullpath, $containingfolder ) {
  118. return str_replace( dirname( dirname( $containingfolder ) ), '', $fullpath );
  119. }
  120. /**
  121. * Check if there is an update for a theme available.
  122. *
  123. * Will display link, if there is an update available.
  124. *
  125. * @since 2.7.0
  126. * @see get_theme_update_available()
  127. *
  128. * @param WP_Theme $theme Theme data object.
  129. */
  130. function theme_update_available( $theme ) {
  131. echo get_theme_update_available( $theme );
  132. }
  133. /**
  134. * Retrieve the update link if there is a theme update available.
  135. *
  136. * Will return a link if there is an update available.
  137. *
  138. * @since 3.8.0
  139. *
  140. * @staticvar object $themes_update
  141. *
  142. * @param WP_Theme $theme WP_Theme object.
  143. * @return false|string HTML for the update link, or false if invalid info was passed.
  144. */
  145. function get_theme_update_available( $theme ) {
  146. static $themes_update = null;
  147. if ( ! current_user_can( 'update_themes' ) ) {
  148. return false;
  149. }
  150. if ( ! isset( $themes_update ) ) {
  151. $themes_update = get_site_transient( 'update_themes' );
  152. }
  153. if ( ! ( $theme instanceof WP_Theme ) ) {
  154. return false;
  155. }
  156. $stylesheet = $theme->get_stylesheet();
  157. $html = '';
  158. if ( isset( $themes_update->response[ $stylesheet ] ) ) {
  159. $update = $themes_update->response[ $stylesheet ];
  160. $theme_name = $theme->display( 'Name' );
  161. $details_url = add_query_arg(
  162. array(
  163. 'TB_iframe' => 'true',
  164. 'width' => 1024,
  165. 'height' => 800,
  166. ),
  167. $update['url']
  168. ); //Theme browser inside WP? replace this, Also, theme preview JS will override this on the available list.
  169. $update_url = wp_nonce_url( admin_url( 'update.php?action=upgrade-theme&amp;theme=' . urlencode( $stylesheet ) ), 'upgrade-theme_' . $stylesheet );
  170. if ( ! is_multisite() ) {
  171. if ( ! current_user_can( 'update_themes' ) ) {
  172. $html = sprintf(
  173. /* translators: 1: Theme name, 2: Theme details URL, 3: Additional link attributes, 4: Version number. */
  174. '<p><strong>' . __( 'There is a new version of %1$s available. <a href="%2$s" %3$s>View version %4$s details</a>.' ) . '</strong></p>',
  175. $theme_name,
  176. esc_url( $details_url ),
  177. sprintf(
  178. 'class="thickbox open-plugin-details-modal" aria-label="%s"',
  179. /* translators: 1: Theme name, 2: Version number. */
  180. esc_attr( sprintf( __( 'View %1$s version %2$s details' ), $theme_name, $update['new_version'] ) )
  181. ),
  182. $update['new_version']
  183. );
  184. } elseif ( empty( $update['package'] ) ) {
  185. $html = sprintf(
  186. /* translators: 1: Theme name, 2: Theme details URL, 3: Additional link attributes, 4: Version number. */
  187. '<p><strong>' . __( 'There is a new version of %1$s available. <a href="%2$s" %3$s>View version %4$s details</a>. <em>Automatic update is unavailable for this theme.</em>' ) . '</strong></p>',
  188. $theme_name,
  189. esc_url( $details_url ),
  190. sprintf(
  191. 'class="thickbox open-plugin-details-modal" aria-label="%s"',
  192. /* translators: 1: Theme name, 2: Version number. */
  193. esc_attr( sprintf( __( 'View %1$s version %2$s details' ), $theme_name, $update['new_version'] ) )
  194. ),
  195. $update['new_version']
  196. );
  197. } else {
  198. $html = sprintf(
  199. /* translators: 1: Theme name, 2: Theme details URL, 3: Additional link attributes, 4: Version number, 5: Update URL, 6: Additional link attributes. */
  200. '<p><strong>' . __( 'There is a new version of %1$s available. <a href="%2$s" %3$s>View version %4$s details</a> or <a href="%5$s" %6$s>update now</a>.' ) . '</strong></p>',
  201. $theme_name,
  202. esc_url( $details_url ),
  203. sprintf(
  204. 'class="thickbox open-plugin-details-modal" aria-label="%s"',
  205. /* translators: 1: Theme name, 2: Version number. */
  206. esc_attr( sprintf( __( 'View %1$s version %2$s details' ), $theme_name, $update['new_version'] ) )
  207. ),
  208. $update['new_version'],
  209. $update_url,
  210. sprintf(
  211. 'aria-label="%s" id="update-theme" data-slug="%s"',
  212. /* translators: %s: Theme name. */
  213. esc_attr( sprintf( __( 'Update %s now' ), $theme_name ) ),
  214. $stylesheet
  215. )
  216. );
  217. }
  218. }
  219. }
  220. return $html;
  221. }
  222. /**
  223. * Retrieve list of WordPress theme features (aka theme tags).
  224. *
  225. * @since 3.1.0
  226. *
  227. * @param bool $api Optional. Whether try to fetch tags from the WordPress.org API. Defaults to true.
  228. * @return array Array of features keyed by category with translations keyed by slug.
  229. */
  230. function get_theme_feature_list( $api = true ) {
  231. // Hard-coded list is used if api not accessible.
  232. $features = array(
  233. __( 'Subject' ) => array(
  234. 'blog' => __( 'Blog' ),
  235. 'e-commerce' => __( 'E-Commerce' ),
  236. 'education' => __( 'Education' ),
  237. 'entertainment' => __( 'Entertainment' ),
  238. 'food-and-drink' => __( 'Food & Drink' ),
  239. 'holiday' => __( 'Holiday' ),
  240. 'news' => __( 'News' ),
  241. 'photography' => __( 'Photography' ),
  242. 'portfolio' => __( 'Portfolio' ),
  243. ),
  244. __( 'Features' ) => array(
  245. 'accessibility-ready' => __( 'Accessibility Ready' ),
  246. 'custom-background' => __( 'Custom Background' ),
  247. 'custom-colors' => __( 'Custom Colors' ),
  248. 'custom-header' => __( 'Custom Header' ),
  249. 'custom-logo' => __( 'Custom Logo' ),
  250. 'editor-style' => __( 'Editor Style' ),
  251. 'featured-image-header' => __( 'Featured Image Header' ),
  252. 'featured-images' => __( 'Featured Images' ),
  253. 'footer-widgets' => __( 'Footer Widgets' ),
  254. 'full-width-template' => __( 'Full Width Template' ),
  255. 'post-formats' => __( 'Post Formats' ),
  256. 'sticky-post' => __( 'Sticky Post' ),
  257. 'theme-options' => __( 'Theme Options' ),
  258. ),
  259. __( 'Layout' ) => array(
  260. 'grid-layout' => __( 'Grid Layout' ),
  261. 'one-column' => __( 'One Column' ),
  262. 'two-columns' => __( 'Two Columns' ),
  263. 'three-columns' => __( 'Three Columns' ),
  264. 'four-columns' => __( 'Four Columns' ),
  265. 'left-sidebar' => __( 'Left Sidebar' ),
  266. 'right-sidebar' => __( 'Right Sidebar' ),
  267. ),
  268. );
  269. if ( ! $api || ! current_user_can( 'install_themes' ) ) {
  270. return $features;
  271. }
  272. $feature_list = get_site_transient( 'wporg_theme_feature_list' );
  273. if ( ! $feature_list ) {
  274. set_site_transient( 'wporg_theme_feature_list', array(), 3 * HOUR_IN_SECONDS );
  275. }
  276. if ( ! $feature_list ) {
  277. $feature_list = themes_api( 'feature_list', array() );
  278. if ( is_wp_error( $feature_list ) ) {
  279. return $features;
  280. }
  281. }
  282. if ( ! $feature_list ) {
  283. return $features;
  284. }
  285. set_site_transient( 'wporg_theme_feature_list', $feature_list, 3 * HOUR_IN_SECONDS );
  286. $category_translations = array(
  287. 'Layout' => __( 'Layout' ),
  288. 'Features' => __( 'Features' ),
  289. 'Subject' => __( 'Subject' ),
  290. );
  291. // Loop over the wporg canonical list and apply translations
  292. $wporg_features = array();
  293. foreach ( (array) $feature_list as $feature_category => $feature_items ) {
  294. if ( isset( $category_translations[ $feature_category ] ) ) {
  295. $feature_category = $category_translations[ $feature_category ];
  296. }
  297. $wporg_features[ $feature_category ] = array();
  298. foreach ( $feature_items as $feature ) {
  299. if ( isset( $features[ $feature_category ][ $feature ] ) ) {
  300. $wporg_features[ $feature_category ][ $feature ] = $features[ $feature_category ][ $feature ];
  301. } else {
  302. $wporg_features[ $feature_category ][ $feature ] = $feature;
  303. }
  304. }
  305. }
  306. return $wporg_features;
  307. }
  308. /**
  309. * Retrieves theme installer pages from the WordPress.org Themes API.
  310. *
  311. * It is possible for a theme to override the Themes API result with three
  312. * filters. Assume this is for themes, which can extend on the Theme Info to
  313. * offer more choices. This is very powerful and must be used with care, when
  314. * overriding the filters.
  315. *
  316. * The first filter, {@see 'themes_api_args'}, is for the args and gives the action
  317. * as the second parameter. The hook for {@see 'themes_api_args'} must ensure that
  318. * an object is returned.
  319. *
  320. * The second filter, {@see 'themes_api'}, allows a plugin to override the WordPress.org
  321. * Theme API entirely. If `$action` is 'query_themes', 'theme_information', or 'feature_list',
  322. * an object MUST be passed. If `$action` is 'hot_tags', an array should be passed.
  323. *
  324. * Finally, the third filter, {@see 'themes_api_result'}, makes it possible to filter the
  325. * response object or array, depending on the `$action` type.
  326. *
  327. * Supported arguments per action:
  328. *
  329. * | Argument Name | 'query_themes' | 'theme_information' | 'hot_tags' | 'feature_list' |
  330. * | -------------------| :------------: | :-----------------: | :--------: | :--------------: |
  331. * | `$slug` | No | Yes | No | No |
  332. * | `$per_page` | Yes | No | No | No |
  333. * | `$page` | Yes | No | No | No |
  334. * | `$number` | No | No | Yes | No |
  335. * | `$search` | Yes | No | No | No |
  336. * | `$tag` | Yes | No | No | No |
  337. * | `$author` | Yes | No | No | No |
  338. * | `$user` | Yes | No | No | No |
  339. * | `$browse` | Yes | No | No | No |
  340. * | `$locale` | Yes | Yes | No | No |
  341. * | `$fields` | Yes | Yes | No | No |
  342. *
  343. * @since 2.8.0
  344. *
  345. * @param string $action API action to perform: 'query_themes', 'theme_information',
  346. * 'hot_tags' or 'feature_list'.
  347. * @param array|object $args {
  348. * Optional. Array or object of arguments to serialize for the Themes API.
  349. *
  350. * @type string $slug The theme slug. Default empty.
  351. * @type int $per_page Number of themes per page. Default 24.
  352. * @type int $page Number of current page. Default 1.
  353. * @type int $number Number of tags to be queried.
  354. * @type string $search A search term. Default empty.
  355. * @type string $tag Tag to filter themes. Default empty.
  356. * @type string $author Username of an author to filter themes. Default empty.
  357. * @type string $user Username to query for their favorites. Default empty.
  358. * @type string $browse Browse view: 'featured', 'popular', 'updated', 'favorites'.
  359. * @type string $locale Locale to provide context-sensitive results. Default is the value of get_locale().
  360. * @type array $fields {
  361. * Array of fields which should or should not be returned.
  362. *
  363. * @type bool $description Whether to return the theme full description. Default false.
  364. * @type bool $sections Whether to return the theme readme sections: description, installation,
  365. * FAQ, screenshots, other notes, and changelog. Default false.
  366. * @type bool $rating Whether to return the rating in percent and total number of ratings.
  367. * Default false.
  368. * @type bool $ratings Whether to return the number of rating for each star (1-5). Default false.
  369. * @type bool $downloaded Whether to return the download count. Default false.
  370. * @type bool $downloadlink Whether to return the download link for the package. Default false.
  371. * @type bool $last_updated Whether to return the date of the last update. Default false.
  372. * @type bool $tags Whether to return the assigned tags. Default false.
  373. * @type bool $homepage Whether to return the theme homepage link. Default false.
  374. * @type bool $screenshots Whether to return the screenshots. Default false.
  375. * @type int $screenshot_count Number of screenshots to return. Default 1.
  376. * @type bool $screenshot_url Whether to return the URL of the first screenshot. Default false.
  377. * @type bool $photon_screenshots Whether to return the screenshots via Photon. Default false.
  378. * @type bool $template Whether to return the slug of the parent theme. Default false.
  379. * @type bool $parent Whether to return the slug, name and homepage of the parent theme. Default false.
  380. * @type bool $versions Whether to return the list of all available versions. Default false.
  381. * @type bool $theme_url Whether to return theme's URL. Default false.
  382. * @type bool $extended_author Whether to return nicename or nicename and display name. Default false.
  383. * }
  384. * }
  385. * @return object|array|WP_Error Response object or array on success, WP_Error on failure. See the
  386. * {@link https://developer.wordpress.org/reference/functions/themes_api/ function reference article}
  387. * for more information on the make-up of possible return objects depending on the value of `$action`.
  388. */
  389. function themes_api( $action, $args = array() ) {
  390. // include an unmodified $wp_version
  391. include( ABSPATH . WPINC . '/version.php' );
  392. if ( is_array( $args ) ) {
  393. $args = (object) $args;
  394. }
  395. if ( 'query_themes' == $action ) {
  396. if ( ! isset( $args->per_page ) ) {
  397. $args->per_page = 24;
  398. }
  399. }
  400. if ( ! isset( $args->locale ) ) {
  401. $args->locale = get_user_locale();
  402. }
  403. if ( ! isset( $args->wp_version ) ) {
  404. $args->wp_version = substr( $wp_version, 0, 3 ); // X.y
  405. }
  406. /**
  407. * Filters arguments used to query for installer pages from the WordPress.org Themes API.
  408. *
  409. * Important: An object MUST be returned to this filter.
  410. *
  411. * @since 2.8.0
  412. *
  413. * @param object $args Arguments used to query for installer pages from the WordPress.org Themes API.
  414. * @param string $action Requested action. Likely values are 'theme_information',
  415. * 'feature_list', or 'query_themes'.
  416. */
  417. $args = apply_filters( 'themes_api_args', $args, $action );
  418. /**
  419. * Filters whether to override the WordPress.org Themes API.
  420. *
  421. * Passing a non-false value will effectively short-circuit the WordPress.org API request.
  422. *
  423. * If `$action` is 'query_themes', 'theme_information', or 'feature_list', an object MUST
  424. * be passed. If `$action` is 'hot_tags', an array should be passed.
  425. *
  426. * @since 2.8.0
  427. *
  428. * @param false|object|array $override Whether to override the WordPress.org Themes API. Default false.
  429. * @param string $action Requested action. Likely values are 'theme_information',
  430. * 'feature_list', or 'query_themes'.
  431. * @param object $args Arguments used to query for installer pages from the Themes API.
  432. */
  433. $res = apply_filters( 'themes_api', false, $action, $args );
  434. if ( ! $res ) {
  435. $url = 'http://api.wordpress.org/themes/info/1.2/';
  436. $url = add_query_arg(
  437. array(
  438. 'action' => $action,
  439. 'request' => $args,
  440. ),
  441. $url
  442. );
  443. $http_url = $url;
  444. $ssl = wp_http_supports( array( 'ssl' ) );
  445. if ( $ssl ) {
  446. $url = set_url_scheme( $url, 'https' );
  447. }
  448. $http_args = array(
  449. 'user-agent' => 'WordPress/' . $wp_version . '; ' . home_url( '/' ),
  450. );
  451. $request = wp_remote_get( $url, $http_args );
  452. if ( $ssl && is_wp_error( $request ) ) {
  453. if ( ! wp_doing_ajax() ) {
  454. trigger_error(
  455. sprintf(
  456. /* translators: %s: Support forums URL. */
  457. __( 'An unexpected error occurred. Something may be wrong with WordPress.org or this server&#8217;s configuration. If you continue to have problems, please try the <a href="%s">support forums</a>.' ),
  458. __( 'https://wordpress.org/support/forums/' )
  459. ) . ' ' . __( '(WordPress could not establish a secure connection to WordPress.org. Please contact your server administrator.)' ),
  460. headers_sent() || WP_DEBUG ? E_USER_WARNING : E_USER_NOTICE
  461. );
  462. }
  463. $request = wp_remote_get( $http_url, $http_args );
  464. }
  465. if ( is_wp_error( $request ) ) {
  466. $res = new WP_Error(
  467. 'themes_api_failed',
  468. sprintf(
  469. /* translators: %s: Support forums URL. */
  470. __( 'An unexpected error occurred. Something may be wrong with WordPress.org or this server&#8217;s configuration. If you continue to have problems, please try the <a href="%s">support forums</a>.' ),
  471. __( 'https://wordpress.org/support/forums/' )
  472. ),
  473. $request->get_error_message()
  474. );
  475. } else {
  476. $res = json_decode( wp_remote_retrieve_body( $request ), true );
  477. if ( is_array( $res ) ) {
  478. // Object casting is required in order to match the info/1.0 format.
  479. $res = (object) $res;
  480. } elseif ( null === $res ) {
  481. $res = new WP_Error(
  482. 'themes_api_failed',
  483. sprintf(
  484. /* translators: %s: Support forums URL. */
  485. __( 'An unexpected error occurred. Something may be wrong with WordPress.org or this server&#8217;s configuration. If you continue to have problems, please try the <a href="%s">support forums</a>.' ),
  486. __( 'https://wordpress.org/support/forums/' )
  487. ),
  488. wp_remote_retrieve_body( $request )
  489. );
  490. }
  491. if ( isset( $res->error ) ) {
  492. $res = new WP_Error( 'themes_api_failed', $res->error );
  493. }
  494. }
  495. // Back-compat for info/1.2 API, upgrade the theme objects in query_themes to objects.
  496. if ( 'query_themes' == $action ) {
  497. foreach ( $res->themes as $i => $theme ) {
  498. $res->themes[ $i ] = (object) $theme;
  499. }
  500. }
  501. // Back-compat for info/1.2 API, downgrade the feature_list result back to an array.
  502. if ( 'feature_list' == $action ) {
  503. $res = (array) $res;
  504. }
  505. }
  506. /**
  507. * Filters the returned WordPress.org Themes API response.
  508. *
  509. * @since 2.8.0
  510. *
  511. * @param array|object|WP_Error $res WordPress.org Themes API response.
  512. * @param string $action Requested action. Likely values are 'theme_information',
  513. * 'feature_list', or 'query_themes'.
  514. * @param object $args Arguments used to query for installer pages from the WordPress.org Themes API.
  515. */
  516. return apply_filters( 'themes_api_result', $res, $action, $args );
  517. }
  518. /**
  519. * Prepare themes for JavaScript.
  520. *
  521. * @since 3.8.0
  522. *
  523. * @param WP_Theme[] $themes Optional. Array of theme objects to prepare.
  524. * Defaults to all allowed themes.
  525. *
  526. * @return array An associative array of theme data, sorted by name.
  527. */
  528. function wp_prepare_themes_for_js( $themes = null ) {
  529. $current_theme = get_stylesheet();
  530. /**
  531. * Filters theme data before it is prepared for JavaScript.
  532. *
  533. * Passing a non-empty array will result in wp_prepare_themes_for_js() returning
  534. * early with that value instead.
  535. *
  536. * @since 4.2.0
  537. *
  538. * @param array $prepared_themes An associative array of theme data. Default empty array.
  539. * @param WP_Theme[]|null $themes An array of theme objects to prepare, if any.
  540. * @param string $current_theme The current theme slug.
  541. */
  542. $prepared_themes = (array) apply_filters( 'pre_prepare_themes_for_js', array(), $themes, $current_theme );
  543. if ( ! empty( $prepared_themes ) ) {
  544. return $prepared_themes;
  545. }
  546. // Make sure the current theme is listed first.
  547. $prepared_themes[ $current_theme ] = array();
  548. if ( null === $themes ) {
  549. $themes = wp_get_themes( array( 'allowed' => true ) );
  550. if ( ! isset( $themes[ $current_theme ] ) ) {
  551. $themes[ $current_theme ] = wp_get_theme();
  552. }
  553. }
  554. $updates = array();
  555. if ( current_user_can( 'update_themes' ) ) {
  556. $updates_transient = get_site_transient( 'update_themes' );
  557. if ( isset( $updates_transient->response ) ) {
  558. $updates = $updates_transient->response;
  559. }
  560. }
  561. WP_Theme::sort_by_name( $themes );
  562. $parents = array();
  563. foreach ( $themes as $theme ) {
  564. $slug = $theme->get_stylesheet();
  565. $encoded_slug = urlencode( $slug );
  566. $parent = false;
  567. if ( $theme->parent() ) {
  568. $parent = $theme->parent();
  569. $parents[ $slug ] = $parent->get_stylesheet();
  570. $parent = $parent->display( 'Name' );
  571. }
  572. $customize_action = null;
  573. if ( current_user_can( 'edit_theme_options' ) && current_user_can( 'customize' ) ) {
  574. $customize_action = esc_url(
  575. add_query_arg(
  576. array(
  577. 'return' => urlencode( esc_url_raw( remove_query_arg( wp_removable_query_args(), wp_unslash( $_SERVER['REQUEST_URI'] ) ) ) ),
  578. ),
  579. wp_customize_url( $slug )
  580. )
  581. );
  582. }
  583. $prepared_themes[ $slug ] = array(
  584. 'id' => $slug,
  585. 'name' => $theme->display( 'Name' ),
  586. 'screenshot' => array( $theme->get_screenshot() ), // @todo multiple
  587. 'description' => $theme->display( 'Description' ),
  588. 'author' => $theme->display( 'Author', false, true ),
  589. 'authorAndUri' => $theme->display( 'Author' ),
  590. 'version' => $theme->display( 'Version' ),
  591. 'tags' => $theme->display( 'Tags' ),
  592. 'parent' => $parent,
  593. 'active' => $slug === $current_theme,
  594. 'hasUpdate' => isset( $updates[ $slug ] ),
  595. 'hasPackage' => isset( $updates[ $slug ] ) && ! empty( $updates[ $slug ]['package'] ),
  596. 'update' => get_theme_update_available( $theme ),
  597. 'actions' => array(
  598. 'activate' => current_user_can( 'switch_themes' ) ? wp_nonce_url( admin_url( 'themes.php?action=activate&amp;stylesheet=' . $encoded_slug ), 'switch-theme_' . $slug ) : null,
  599. 'customize' => $customize_action,
  600. 'delete' => current_user_can( 'delete_themes' ) ? wp_nonce_url( admin_url( 'themes.php?action=delete&amp;stylesheet=' . $encoded_slug ), 'delete-theme_' . $slug ) : null,
  601. ),
  602. );
  603. }
  604. // Remove 'delete' action if theme has an active child
  605. if ( ! empty( $parents ) && array_key_exists( $current_theme, $parents ) ) {
  606. unset( $prepared_themes[ $parents[ $current_theme ] ]['actions']['delete'] );
  607. }
  608. /**
  609. * Filters the themes prepared for JavaScript, for themes.php.
  610. *
  611. * Could be useful for changing the order, which is by name by default.
  612. *
  613. * @since 3.8.0
  614. *
  615. * @param array $prepared_themes Array of theme data.
  616. */
  617. $prepared_themes = apply_filters( 'wp_prepare_themes_for_js', $prepared_themes );
  618. $prepared_themes = array_values( $prepared_themes );
  619. return array_filter( $prepared_themes );
  620. }
  621. /**
  622. * Print JS templates for the theme-browsing UI in the Customizer.
  623. *
  624. * @since 4.2.0
  625. */
  626. function customize_themes_print_templates() {
  627. ?>
  628. <script type="text/html" id="tmpl-customize-themes-details-view">
  629. <div class="theme-backdrop"></div>
  630. <div class="theme-wrap wp-clearfix" role="document">
  631. <div class="theme-header">
  632. <button type="button" class="left dashicons dashicons-no"><span class="screen-reader-text"><?php _e( 'Show previous theme' ); ?></span></button>
  633. <button type="button" class="right dashicons dashicons-no"><span class="screen-reader-text"><?php _e( 'Show next theme' ); ?></span></button>
  634. <button type="button" class="close dashicons dashicons-no"><span class="screen-reader-text"><?php _e( 'Close details dialog' ); ?></span></button>
  635. </div>
  636. <div class="theme-about wp-clearfix">
  637. <div class="theme-screenshots">
  638. <# if ( data.screenshot && data.screenshot[0] ) { #>
  639. <div class="screenshot"><img src="{{ data.screenshot[0] }}" alt="" /></div>
  640. <# } else { #>
  641. <div class="screenshot blank"></div>
  642. <# } #>
  643. </div>
  644. <div class="theme-info">
  645. <# if ( data.active ) { #>
  646. <span class="current-label"><?php _e( 'Current Theme' ); ?></span>
  647. <# } #>
  648. <h2 class="theme-name">{{{ data.name }}}<span class="theme-version">
  649. <?php
  650. /* translators: %s: Theme version. */
  651. printf( __( 'Version: %s' ), '{{ data.version }}' );
  652. ?>
  653. </span></h2>
  654. <h3 class="theme-author">
  655. <?php
  656. /* translators: %s: Theme author link. */
  657. printf( __( 'By %s' ), '{{{ data.authorAndUri }}}' );
  658. ?>
  659. </h3>
  660. <# if ( data.stars && 0 != data.num_ratings ) { #>
  661. <div class="theme-rating">
  662. {{{ data.stars }}}
  663. <a class="num-ratings" target="_blank" href="{{ data.reviews_url }}">
  664. <?php
  665. printf(
  666. '%1$s <span class="screen-reader-text">%2$s</span>',
  667. /* translators: %s: Number of ratings. */
  668. sprintf( __( '(%s ratings)' ), '{{ data.num_ratings }}' ),
  669. /* translators: Accessibility text. */
  670. __( '(opens in a new tab)' )
  671. );
  672. ?>
  673. </a>
  674. </div>
  675. <# } #>
  676. <# if ( data.hasUpdate ) { #>
  677. <div class="notice notice-warning notice-alt notice-large" data-slug="{{ data.id }}">
  678. <h3 class="notice-title"><?php _e( 'Update Available' ); ?></h3>
  679. {{{ data.update }}}
  680. </div>
  681. <# } #>
  682. <# if ( data.parent ) { #>
  683. <p class="parent-theme">
  684. <?php
  685. printf(
  686. /* translators: %s: Theme name. */
  687. __( 'This is a child theme of %s.' ),
  688. '<strong>{{{ data.parent }}}</strong>'
  689. );
  690. ?>
  691. </p>
  692. <# } #>
  693. <p class="theme-description">{{{ data.description }}}</p>
  694. <# if ( data.tags ) { #>
  695. <p class="theme-tags"><span><?php _e( 'Tags:' ); ?></span> {{{ data.tags }}}</p>
  696. <# } #>
  697. </div>
  698. </div>
  699. <div class="theme-actions">
  700. <# if ( data.active ) { #>
  701. <button type="button" class="button button-primary customize-theme"><?php _e( 'Customize' ); ?></button>
  702. <# } else if ( 'installed' === data.type ) { #>
  703. <?php if ( current_user_can( 'delete_themes' ) ) { ?>
  704. <# if ( data.actions && data.actions['delete'] ) { #>
  705. <a href="{{{ data.actions['delete'] }}}" data-slug="{{ data.id }}" class="button button-secondary delete-theme"><?php _e( 'Delete' ); ?></a>
  706. <# } #>
  707. <?php } ?>
  708. <button type="button" class="button button-primary preview-theme" data-slug="{{ data.id }}"><?php _e( 'Live Preview' ); ?></button>
  709. <# } else { #>
  710. <button type="button" class="button theme-install" data-slug="{{ data.id }}"><?php _e( 'Install' ); ?></button>
  711. <button type="button" class="button button-primary theme-install preview" data-slug="{{ data.id }}"><?php _e( 'Install &amp; Preview' ); ?></button>
  712. <# } #>
  713. </div>
  714. </div>
  715. </script>
  716. <?php
  717. }
  718. /**
  719. * Determines whether a theme is technically active but was paused while
  720. * loading.
  721. *
  722. * For more information on this and similar theme functions, check out
  723. * the {@link https://developer.wordpress.org/themes/basics/conditional-tags/
  724. * Conditional Tags} article in the Theme Developer Handbook.
  725. *
  726. * @since 5.2.0
  727. *
  728. * @param string $theme Path to the theme directory relative to the themes directory.
  729. * @return bool True, if in the list of paused themes. False, not in the list.
  730. */
  731. function is_theme_paused( $theme ) {
  732. if ( ! isset( $GLOBALS['_paused_themes'] ) ) {
  733. return false;
  734. }
  735. if ( get_stylesheet() !== $theme && get_template() !== $theme ) {
  736. return false;
  737. }
  738. return array_key_exists( $theme, $GLOBALS['_paused_themes'] );
  739. }
  740. /**
  741. * Gets the error that was recorded for a paused theme.
  742. *
  743. * @since 5.2.0
  744. *
  745. * @param string $theme Path to the theme directory relative to the themes
  746. * directory.
  747. * @return array|false Array of error information as it was returned by
  748. * `error_get_last()`, or false if none was recorded.
  749. */
  750. function wp_get_theme_error( $theme ) {
  751. if ( ! isset( $GLOBALS['_paused_themes'] ) ) {
  752. return false;
  753. }
  754. if ( ! array_key_exists( $theme, $GLOBALS['_paused_themes'] ) ) {
  755. return false;
  756. }
  757. return $GLOBALS['_paused_themes'][ $theme ];
  758. }
  759. /**
  760. * Tries to resume a single theme.
  761. *
  762. * If a redirect was provided and a functions.php file was found, we first ensure that
  763. * functions.php file does not throw fatal errors anymore.
  764. *
  765. * The way it works is by setting the redirection to the error before trying to
  766. * include the file. If the theme fails, then the redirection will not be overwritten
  767. * with the success message and the theme will not be resumed.
  768. *
  769. * @since 5.2.0
  770. *
  771. * @param string $theme Single theme to resume.
  772. * @param string $redirect Optional. URL to redirect to. Default empty string.
  773. * @return bool|WP_Error True on success, false if `$theme` was not paused,
  774. * `WP_Error` on failure.
  775. */
  776. function resume_theme( $theme, $redirect = '' ) {
  777. list( $extension ) = explode( '/', $theme );
  778. /*
  779. * We'll override this later if the theme could be resumed without
  780. * creating a fatal error.
  781. */
  782. if ( ! empty( $redirect ) ) {
  783. $functions_path = '';
  784. if ( strpos( STYLESHEETPATH, $extension ) ) {
  785. $functions_path = STYLESHEETPATH . '/functions.php';
  786. } elseif ( strpos( TEMPLATEPATH, $extension ) ) {
  787. $functions_path = TEMPLATEPATH . '/functions.php';
  788. }
  789. if ( ! empty( $functions_path ) ) {
  790. wp_redirect(
  791. add_query_arg(
  792. '_error_nonce',
  793. wp_create_nonce( 'theme-resume-error_' . $theme ),
  794. $redirect
  795. )
  796. );
  797. // Load the theme's functions.php to test whether it throws a fatal error.
  798. ob_start();
  799. if ( ! defined( 'WP_SANDBOX_SCRAPING' ) ) {
  800. define( 'WP_SANDBOX_SCRAPING', true );
  801. }
  802. include $functions_path;
  803. ob_clean();
  804. }
  805. }
  806. $result = wp_paused_themes()->delete( $extension );
  807. if ( ! $result ) {
  808. return new WP_Error(
  809. 'could_not_resume_theme',
  810. __( 'Could not resume the theme.' )
  811. );
  812. }
  813. return true;
  814. }
  815. /**
  816. * Renders an admin notice in case some themes have been paused due to errors.
  817. *
  818. * @since 5.2.0
  819. */
  820. function paused_themes_notice() {
  821. if ( 'themes.php' === $GLOBALS['pagenow'] ) {
  822. return;
  823. }
  824. if ( ! current_user_can( 'resume_themes' ) ) {
  825. return;
  826. }
  827. if ( ! isset( $GLOBALS['_paused_themes'] ) || empty( $GLOBALS['_paused_themes'] ) ) {
  828. return;
  829. }
  830. printf(
  831. '<div class="notice notice-error"><p><strong>%s</strong><br>%s</p><p><a href="%s">%s</a></p></div>',
  832. __( 'One or more themes failed to load properly.' ),
  833. __( 'You can find more details and make changes on the Themes screen.' ),
  834. esc_url( admin_url( 'themes.php' ) ),
  835. __( 'Go to the Themes screen' )
  836. );
  837. }