class-wp-theme.php 47 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489
  1. <?php
  2. /**
  3. * WP_Theme Class
  4. *
  5. * @package WordPress
  6. * @subpackage Theme
  7. * @since 3.4.0
  8. */
  9. final class WP_Theme implements ArrayAccess {
  10. /**
  11. * Whether the theme has been marked as updateable.
  12. *
  13. * @since 4.4.0
  14. * @var bool
  15. *
  16. * @see WP_MS_Themes_List_Table
  17. */
  18. public $update = false;
  19. /**
  20. * Headers for style.css files.
  21. *
  22. * @static
  23. * @var array
  24. */
  25. private static $file_headers = array(
  26. 'Name' => 'Theme Name',
  27. 'ThemeURI' => 'Theme URI',
  28. 'Description' => 'Description',
  29. 'Author' => 'Author',
  30. 'AuthorURI' => 'Author URI',
  31. 'Version' => 'Version',
  32. 'Template' => 'Template',
  33. 'Status' => 'Status',
  34. 'Tags' => 'Tags',
  35. 'TextDomain' => 'Text Domain',
  36. 'DomainPath' => 'Domain Path',
  37. );
  38. /**
  39. * Default themes.
  40. *
  41. * @static
  42. * @var array
  43. */
  44. private static $default_themes = array(
  45. 'classic' => 'WordPress Classic',
  46. 'default' => 'WordPress Default',
  47. 'twentyten' => 'Twenty Ten',
  48. 'twentyeleven' => 'Twenty Eleven',
  49. 'twentytwelve' => 'Twenty Twelve',
  50. 'twentythirteen' => 'Twenty Thirteen',
  51. 'twentyfourteen' => 'Twenty Fourteen',
  52. 'twentyfifteen' => 'Twenty Fifteen',
  53. 'twentysixteen' => 'Twenty Sixteen',
  54. 'twentyseventeen' => 'Twenty Seventeen',
  55. );
  56. /**
  57. * Renamed theme tags.
  58. *
  59. * @static
  60. * @var array
  61. */
  62. private static $tag_map = array(
  63. 'fixed-width' => 'fixed-layout',
  64. 'flexible-width' => 'fluid-layout',
  65. );
  66. /**
  67. * Absolute path to the theme root, usually wp-content/themes
  68. *
  69. * @var string
  70. */
  71. private $theme_root;
  72. /**
  73. * Header data from the theme's style.css file.
  74. *
  75. * @var array
  76. */
  77. private $headers = array();
  78. /**
  79. * Header data from the theme's style.css file after being sanitized.
  80. *
  81. * @var array
  82. */
  83. private $headers_sanitized;
  84. /**
  85. * Header name from the theme's style.css after being translated.
  86. *
  87. * Cached due to sorting functions running over the translated name.
  88. *
  89. * @var string
  90. */
  91. private $name_translated;
  92. /**
  93. * Errors encountered when initializing the theme.
  94. *
  95. * @var WP_Error
  96. */
  97. private $errors;
  98. /**
  99. * The directory name of the theme's files, inside the theme root.
  100. *
  101. * In the case of a child theme, this is directory name of the child theme.
  102. * Otherwise, 'stylesheet' is the same as 'template'.
  103. *
  104. * @var string
  105. */
  106. private $stylesheet;
  107. /**
  108. * The directory name of the theme's files, inside the theme root.
  109. *
  110. * In the case of a child theme, this is the directory name of the parent theme.
  111. * Otherwise, 'template' is the same as 'stylesheet'.
  112. *
  113. * @var string
  114. */
  115. private $template;
  116. /**
  117. * A reference to the parent theme, in the case of a child theme.
  118. *
  119. * @var WP_Theme
  120. */
  121. private $parent;
  122. /**
  123. * URL to the theme root, usually an absolute URL to wp-content/themes
  124. *
  125. * @var string
  126. */
  127. private $theme_root_uri;
  128. /**
  129. * Flag for whether the theme's textdomain is loaded.
  130. *
  131. * @var bool
  132. */
  133. private $textdomain_loaded;
  134. /**
  135. * Stores an md5 hash of the theme root, to function as the cache key.
  136. *
  137. * @var string
  138. */
  139. private $cache_hash;
  140. /**
  141. * Flag for whether the themes cache bucket should be persistently cached.
  142. *
  143. * Default is false. Can be set with the {@see 'wp_cache_themes_persistently'} filter.
  144. *
  145. * @static
  146. * @var bool
  147. */
  148. private static $persistently_cache;
  149. /**
  150. * Expiration time for the themes cache bucket.
  151. *
  152. * By default the bucket is not cached, so this value is useless.
  153. *
  154. * @static
  155. * @var bool
  156. */
  157. private static $cache_expiration = 1800;
  158. /**
  159. * Constructor for WP_Theme.
  160. *
  161. * @since 3.4.0
  162. *
  163. * @global array $wp_theme_directories
  164. *
  165. * @param string $theme_dir Directory of the theme within the theme_root.
  166. * @param string $theme_root Theme root.
  167. * @param WP_Error|void $_child If this theme is a parent theme, the child may be passed for validation purposes.
  168. */
  169. public function __construct( $theme_dir, $theme_root, $_child = null ) {
  170. global $wp_theme_directories;
  171. // Initialize caching on first run.
  172. if ( ! isset( self::$persistently_cache ) ) {
  173. /** This action is documented in wp-includes/theme.php */
  174. self::$persistently_cache = apply_filters( 'wp_cache_themes_persistently', false, 'WP_Theme' );
  175. if ( self::$persistently_cache ) {
  176. wp_cache_add_global_groups( 'themes' );
  177. if ( is_int( self::$persistently_cache ) )
  178. self::$cache_expiration = self::$persistently_cache;
  179. } else {
  180. wp_cache_add_non_persistent_groups( 'themes' );
  181. }
  182. }
  183. $this->theme_root = $theme_root;
  184. $this->stylesheet = $theme_dir;
  185. // Correct a situation where the theme is 'some-directory/some-theme' but 'some-directory' was passed in as part of the theme root instead.
  186. if ( ! in_array( $theme_root, (array) $wp_theme_directories ) && in_array( dirname( $theme_root ), (array) $wp_theme_directories ) ) {
  187. $this->stylesheet = basename( $this->theme_root ) . '/' . $this->stylesheet;
  188. $this->theme_root = dirname( $theme_root );
  189. }
  190. $this->cache_hash = md5( $this->theme_root . '/' . $this->stylesheet );
  191. $theme_file = $this->stylesheet . '/style.css';
  192. $cache = $this->cache_get( 'theme' );
  193. if ( is_array( $cache ) ) {
  194. foreach ( array( 'errors', 'headers', 'template' ) as $key ) {
  195. if ( isset( $cache[ $key ] ) )
  196. $this->$key = $cache[ $key ];
  197. }
  198. if ( $this->errors )
  199. return;
  200. if ( isset( $cache['theme_root_template'] ) )
  201. $theme_root_template = $cache['theme_root_template'];
  202. } elseif ( ! file_exists( $this->theme_root . '/' . $theme_file ) ) {
  203. $this->headers['Name'] = $this->stylesheet;
  204. if ( ! file_exists( $this->theme_root . '/' . $this->stylesheet ) )
  205. $this->errors = new WP_Error( 'theme_not_found', sprintf( __( 'The theme directory "%s" does not exist.' ), esc_html( $this->stylesheet ) ) );
  206. else
  207. $this->errors = new WP_Error( 'theme_no_stylesheet', __( 'Stylesheet is missing.' ) );
  208. $this->template = $this->stylesheet;
  209. $this->cache_add( 'theme', array( 'headers' => $this->headers, 'errors' => $this->errors, 'stylesheet' => $this->stylesheet, 'template' => $this->template ) );
  210. if ( ! file_exists( $this->theme_root ) ) // Don't cache this one.
  211. $this->errors->add( 'theme_root_missing', __( 'ERROR: The themes directory is either empty or doesn&#8217;t exist. Please check your installation.' ) );
  212. return;
  213. } elseif ( ! is_readable( $this->theme_root . '/' . $theme_file ) ) {
  214. $this->headers['Name'] = $this->stylesheet;
  215. $this->errors = new WP_Error( 'theme_stylesheet_not_readable', __( 'Stylesheet is not readable.' ) );
  216. $this->template = $this->stylesheet;
  217. $this->cache_add( 'theme', array( 'headers' => $this->headers, 'errors' => $this->errors, 'stylesheet' => $this->stylesheet, 'template' => $this->template ) );
  218. return;
  219. } else {
  220. $this->headers = get_file_data( $this->theme_root . '/' . $theme_file, self::$file_headers, 'theme' );
  221. // Default themes always trump their pretenders.
  222. // Properly identify default themes that are inside a directory within wp-content/themes.
  223. if ( $default_theme_slug = array_search( $this->headers['Name'], self::$default_themes ) ) {
  224. if ( basename( $this->stylesheet ) != $default_theme_slug )
  225. $this->headers['Name'] .= '/' . $this->stylesheet;
  226. }
  227. }
  228. if ( ! $this->template && $this->stylesheet === $this->headers['Template'] ) {
  229. /* translators: %s: Template */
  230. $this->errors = new WP_Error( 'theme_child_invalid', sprintf( __( 'The theme defines itself as its parent theme. Please check the %s header.' ), '<code>Template</code>' ) );
  231. $this->cache_add( 'theme', array( 'headers' => $this->headers, 'errors' => $this->errors, 'stylesheet' => $this->stylesheet ) );
  232. return;
  233. }
  234. // (If template is set from cache [and there are no errors], we know it's good.)
  235. if ( ! $this->template && ! ( $this->template = $this->headers['Template'] ) ) {
  236. $this->template = $this->stylesheet;
  237. if ( ! file_exists( $this->theme_root . '/' . $this->stylesheet . '/index.php' ) ) {
  238. $error_message = sprintf(
  239. /* translators: 1: index.php, 2: Codex URL, 3: style.css */
  240. __( 'Template is missing. Standalone themes need to have a %1$s template file. <a href="%2$s">Child themes</a> need to have a Template header in the %3$s stylesheet.' ),
  241. '<code>index.php</code>',
  242. __( 'https://codex.wordpress.org/Child_Themes' ),
  243. '<code>style.css</code>'
  244. );
  245. $this->errors = new WP_Error( 'theme_no_index', $error_message );
  246. $this->cache_add( 'theme', array( 'headers' => $this->headers, 'errors' => $this->errors, 'stylesheet' => $this->stylesheet, 'template' => $this->template ) );
  247. return;
  248. }
  249. }
  250. // If we got our data from cache, we can assume that 'template' is pointing to the right place.
  251. if ( ! is_array( $cache ) && $this->template != $this->stylesheet && ! file_exists( $this->theme_root . '/' . $this->template . '/index.php' ) ) {
  252. // If we're in a directory of themes inside /themes, look for the parent nearby.
  253. // wp-content/themes/directory-of-themes/*
  254. $parent_dir = dirname( $this->stylesheet );
  255. if ( '.' != $parent_dir && file_exists( $this->theme_root . '/' . $parent_dir . '/' . $this->template . '/index.php' ) ) {
  256. $this->template = $parent_dir . '/' . $this->template;
  257. } elseif ( ( $directories = search_theme_directories() ) && isset( $directories[ $this->template ] ) ) {
  258. // Look for the template in the search_theme_directories() results, in case it is in another theme root.
  259. // We don't look into directories of themes, just the theme root.
  260. $theme_root_template = $directories[ $this->template ]['theme_root'];
  261. } else {
  262. // Parent theme is missing.
  263. $this->errors = new WP_Error( 'theme_no_parent', sprintf( __( 'The parent theme is missing. Please install the "%s" parent theme.' ), esc_html( $this->template ) ) );
  264. $this->cache_add( 'theme', array( 'headers' => $this->headers, 'errors' => $this->errors, 'stylesheet' => $this->stylesheet, 'template' => $this->template ) );
  265. $this->parent = new WP_Theme( $this->template, $this->theme_root, $this );
  266. return;
  267. }
  268. }
  269. // Set the parent, if we're a child theme.
  270. if ( $this->template != $this->stylesheet ) {
  271. // If we are a parent, then there is a problem. Only two generations allowed! Cancel things out.
  272. if ( $_child instanceof WP_Theme && $_child->template == $this->stylesheet ) {
  273. $_child->parent = null;
  274. $_child->errors = new WP_Error( 'theme_parent_invalid', sprintf( __( 'The "%s" theme is not a valid parent theme.' ), esc_html( $_child->template ) ) );
  275. $_child->cache_add( 'theme', array( 'headers' => $_child->headers, 'errors' => $_child->errors, 'stylesheet' => $_child->stylesheet, 'template' => $_child->template ) );
  276. // The two themes actually reference each other with the Template header.
  277. if ( $_child->stylesheet == $this->template ) {
  278. $this->errors = new WP_Error( 'theme_parent_invalid', sprintf( __( 'The "%s" theme is not a valid parent theme.' ), esc_html( $this->template ) ) );
  279. $this->cache_add( 'theme', array( 'headers' => $this->headers, 'errors' => $this->errors, 'stylesheet' => $this->stylesheet, 'template' => $this->template ) );
  280. }
  281. return;
  282. }
  283. // Set the parent. Pass the current instance so we can do the crazy checks above and assess errors.
  284. $this->parent = new WP_Theme( $this->template, isset( $theme_root_template ) ? $theme_root_template : $this->theme_root, $this );
  285. }
  286. // We're good. If we didn't retrieve from cache, set it.
  287. if ( ! is_array( $cache ) ) {
  288. $cache = array( 'headers' => $this->headers, 'errors' => $this->errors, 'stylesheet' => $this->stylesheet, 'template' => $this->template );
  289. // If the parent theme is in another root, we'll want to cache this. Avoids an entire branch of filesystem calls above.
  290. if ( isset( $theme_root_template ) )
  291. $cache['theme_root_template'] = $theme_root_template;
  292. $this->cache_add( 'theme', $cache );
  293. }
  294. }
  295. /**
  296. * When converting the object to a string, the theme name is returned.
  297. *
  298. * @since 3.4.0
  299. *
  300. * @return string Theme name, ready for display (translated)
  301. */
  302. public function __toString() {
  303. return (string) $this->display('Name');
  304. }
  305. /**
  306. * __isset() magic method for properties formerly returned by current_theme_info()
  307. *
  308. * @staticvar array $properties
  309. *
  310. * @since 3.4.0
  311. *
  312. * @param string $offset Property to check if set.
  313. * @return bool Whether the given property is set.
  314. */
  315. public function __isset( $offset ) {
  316. static $properties = array(
  317. 'name', 'title', 'version', 'parent_theme', 'template_dir', 'stylesheet_dir', 'template', 'stylesheet',
  318. 'screenshot', 'description', 'author', 'tags', 'theme_root', 'theme_root_uri',
  319. );
  320. return in_array( $offset, $properties );
  321. }
  322. /**
  323. * __get() magic method for properties formerly returned by current_theme_info()
  324. *
  325. * @since 3.4.0
  326. *
  327. * @param string $offset Property to get.
  328. * @return mixed Property value.
  329. */
  330. public function __get( $offset ) {
  331. switch ( $offset ) {
  332. case 'name' :
  333. case 'title' :
  334. return $this->get('Name');
  335. case 'version' :
  336. return $this->get('Version');
  337. case 'parent_theme' :
  338. return $this->parent() ? $this->parent()->get('Name') : '';
  339. case 'template_dir' :
  340. return $this->get_template_directory();
  341. case 'stylesheet_dir' :
  342. return $this->get_stylesheet_directory();
  343. case 'template' :
  344. return $this->get_template();
  345. case 'stylesheet' :
  346. return $this->get_stylesheet();
  347. case 'screenshot' :
  348. return $this->get_screenshot( 'relative' );
  349. // 'author' and 'description' did not previously return translated data.
  350. case 'description' :
  351. return $this->display('Description');
  352. case 'author' :
  353. return $this->display('Author');
  354. case 'tags' :
  355. return $this->get( 'Tags' );
  356. case 'theme_root' :
  357. return $this->get_theme_root();
  358. case 'theme_root_uri' :
  359. return $this->get_theme_root_uri();
  360. // For cases where the array was converted to an object.
  361. default :
  362. return $this->offsetGet( $offset );
  363. }
  364. }
  365. /**
  366. * Method to implement ArrayAccess for keys formerly returned by get_themes()
  367. *
  368. * @since 3.4.0
  369. *
  370. * @param mixed $offset
  371. * @param mixed $value
  372. */
  373. public function offsetSet( $offset, $value ) {}
  374. /**
  375. * Method to implement ArrayAccess for keys formerly returned by get_themes()
  376. *
  377. * @since 3.4.0
  378. *
  379. * @param mixed $offset
  380. */
  381. public function offsetUnset( $offset ) {}
  382. /**
  383. * Method to implement ArrayAccess for keys formerly returned by get_themes()
  384. *
  385. * @staticvar array $keys
  386. *
  387. * @since 3.4.0
  388. *
  389. * @param mixed $offset
  390. * @return bool
  391. */
  392. public function offsetExists( $offset ) {
  393. static $keys = array(
  394. 'Name', 'Version', 'Status', 'Title', 'Author', 'Author Name', 'Author URI', 'Description',
  395. 'Template', 'Stylesheet', 'Template Files', 'Stylesheet Files', 'Template Dir', 'Stylesheet Dir',
  396. 'Screenshot', 'Tags', 'Theme Root', 'Theme Root URI', 'Parent Theme',
  397. );
  398. return in_array( $offset, $keys );
  399. }
  400. /**
  401. * Method to implement ArrayAccess for keys formerly returned by get_themes().
  402. *
  403. * Author, Author Name, Author URI, and Description did not previously return
  404. * translated data. We are doing so now as it is safe to do. However, as
  405. * Name and Title could have been used as the key for get_themes(), both remain
  406. * untranslated for back compatibility. This means that ['Name'] is not ideal,
  407. * and care should be taken to use `$theme::display( 'Name' )` to get a properly
  408. * translated header.
  409. *
  410. * @since 3.4.0
  411. *
  412. * @param mixed $offset
  413. * @return mixed
  414. */
  415. public function offsetGet( $offset ) {
  416. switch ( $offset ) {
  417. case 'Name' :
  418. case 'Title' :
  419. /*
  420. * See note above about using translated data. get() is not ideal.
  421. * It is only for backward compatibility. Use display().
  422. */
  423. return $this->get('Name');
  424. case 'Author' :
  425. return $this->display( 'Author');
  426. case 'Author Name' :
  427. return $this->display( 'Author', false);
  428. case 'Author URI' :
  429. return $this->display('AuthorURI');
  430. case 'Description' :
  431. return $this->display( 'Description');
  432. case 'Version' :
  433. case 'Status' :
  434. return $this->get( $offset );
  435. case 'Template' :
  436. return $this->get_template();
  437. case 'Stylesheet' :
  438. return $this->get_stylesheet();
  439. case 'Template Files' :
  440. return $this->get_files( 'php', 1, true );
  441. case 'Stylesheet Files' :
  442. return $this->get_files( 'css', 0, false );
  443. case 'Template Dir' :
  444. return $this->get_template_directory();
  445. case 'Stylesheet Dir' :
  446. return $this->get_stylesheet_directory();
  447. case 'Screenshot' :
  448. return $this->get_screenshot( 'relative' );
  449. case 'Tags' :
  450. return $this->get('Tags');
  451. case 'Theme Root' :
  452. return $this->get_theme_root();
  453. case 'Theme Root URI' :
  454. return $this->get_theme_root_uri();
  455. case 'Parent Theme' :
  456. return $this->parent() ? $this->parent()->get('Name') : '';
  457. default :
  458. return null;
  459. }
  460. }
  461. /**
  462. * Returns errors property.
  463. *
  464. * @since 3.4.0
  465. *
  466. * @return WP_Error|false WP_Error if there are errors, or false.
  467. */
  468. public function errors() {
  469. return is_wp_error( $this->errors ) ? $this->errors : false;
  470. }
  471. /**
  472. * Whether the theme exists.
  473. *
  474. * A theme with errors exists. A theme with the error of 'theme_not_found',
  475. * meaning that the theme's directory was not found, does not exist.
  476. *
  477. * @since 3.4.0
  478. *
  479. * @return bool Whether the theme exists.
  480. */
  481. public function exists() {
  482. return ! ( $this->errors() && in_array( 'theme_not_found', $this->errors()->get_error_codes() ) );
  483. }
  484. /**
  485. * Returns reference to the parent theme.
  486. *
  487. * @since 3.4.0
  488. *
  489. * @return WP_Theme|false Parent theme, or false if the current theme is not a child theme.
  490. */
  491. public function parent() {
  492. return isset( $this->parent ) ? $this->parent : false;
  493. }
  494. /**
  495. * Adds theme data to cache.
  496. *
  497. * Cache entries keyed by the theme and the type of data.
  498. *
  499. * @since 3.4.0
  500. *
  501. * @param string $key Type of data to store (theme, screenshot, headers, post_templates)
  502. * @param string $data Data to store
  503. * @return bool Return value from wp_cache_add()
  504. */
  505. private function cache_add( $key, $data ) {
  506. return wp_cache_add( $key . '-' . $this->cache_hash, $data, 'themes', self::$cache_expiration );
  507. }
  508. /**
  509. * Gets theme data from cache.
  510. *
  511. * Cache entries are keyed by the theme and the type of data.
  512. *
  513. * @since 3.4.0
  514. *
  515. * @param string $key Type of data to retrieve (theme, screenshot, headers, post_templates)
  516. * @return mixed Retrieved data
  517. */
  518. private function cache_get( $key ) {
  519. return wp_cache_get( $key . '-' . $this->cache_hash, 'themes' );
  520. }
  521. /**
  522. * Clears the cache for the theme.
  523. *
  524. * @since 3.4.0
  525. */
  526. public function cache_delete() {
  527. foreach ( array( 'theme', 'screenshot', 'headers', 'post_templates' ) as $key )
  528. wp_cache_delete( $key . '-' . $this->cache_hash, 'themes' );
  529. $this->template = $this->textdomain_loaded = $this->theme_root_uri = $this->parent = $this->errors = $this->headers_sanitized = $this->name_translated = null;
  530. $this->headers = array();
  531. $this->__construct( $this->stylesheet, $this->theme_root );
  532. }
  533. /**
  534. * Get a raw, unformatted theme header.
  535. *
  536. * The header is sanitized, but is not translated, and is not marked up for display.
  537. * To get a theme header for display, use the display() method.
  538. *
  539. * Use the get_template() method, not the 'Template' header, for finding the template.
  540. * The 'Template' header is only good for what was written in the style.css, while
  541. * get_template() takes into account where WordPress actually located the theme and
  542. * whether it is actually valid.
  543. *
  544. * @since 3.4.0
  545. *
  546. * @param string $header Theme header. Name, Description, Author, Version, ThemeURI, AuthorURI, Status, Tags.
  547. * @return string|false String on success, false on failure.
  548. */
  549. public function get( $header ) {
  550. if ( ! isset( $this->headers[ $header ] ) )
  551. return false;
  552. if ( ! isset( $this->headers_sanitized ) ) {
  553. $this->headers_sanitized = $this->cache_get( 'headers' );
  554. if ( ! is_array( $this->headers_sanitized ) )
  555. $this->headers_sanitized = array();
  556. }
  557. if ( isset( $this->headers_sanitized[ $header ] ) )
  558. return $this->headers_sanitized[ $header ];
  559. // If themes are a persistent group, sanitize everything and cache it. One cache add is better than many cache sets.
  560. if ( self::$persistently_cache ) {
  561. foreach ( array_keys( $this->headers ) as $_header )
  562. $this->headers_sanitized[ $_header ] = $this->sanitize_header( $_header, $this->headers[ $_header ] );
  563. $this->cache_add( 'headers', $this->headers_sanitized );
  564. } else {
  565. $this->headers_sanitized[ $header ] = $this->sanitize_header( $header, $this->headers[ $header ] );
  566. }
  567. return $this->headers_sanitized[ $header ];
  568. }
  569. /**
  570. * Gets a theme header, formatted and translated for display.
  571. *
  572. * @since 3.4.0
  573. *
  574. * @param string $header Theme header. Name, Description, Author, Version, ThemeURI, AuthorURI, Status, Tags.
  575. * @param bool $markup Optional. Whether to mark up the header. Defaults to true.
  576. * @param bool $translate Optional. Whether to translate the header. Defaults to true.
  577. * @return string|false Processed header, false on failure.
  578. */
  579. public function display( $header, $markup = true, $translate = true ) {
  580. $value = $this->get( $header );
  581. if ( false === $value ) {
  582. return false;
  583. }
  584. if ( $translate && ( empty( $value ) || ! $this->load_textdomain() ) )
  585. $translate = false;
  586. if ( $translate )
  587. $value = $this->translate_header( $header, $value );
  588. if ( $markup )
  589. $value = $this->markup_header( $header, $value, $translate );
  590. return $value;
  591. }
  592. /**
  593. * Sanitize a theme header.
  594. *
  595. * @since 3.4.0
  596. *
  597. * @staticvar array $header_tags
  598. * @staticvar array $header_tags_with_a
  599. *
  600. * @param string $header Theme header. Name, Description, Author, Version, ThemeURI, AuthorURI, Status, Tags.
  601. * @param string $value Value to sanitize.
  602. * @return mixed
  603. */
  604. private function sanitize_header( $header, $value ) {
  605. switch ( $header ) {
  606. case 'Status' :
  607. if ( ! $value ) {
  608. $value = 'publish';
  609. break;
  610. }
  611. // Fall through otherwise.
  612. case 'Name' :
  613. static $header_tags = array(
  614. 'abbr' => array( 'title' => true ),
  615. 'acronym' => array( 'title' => true ),
  616. 'code' => true,
  617. 'em' => true,
  618. 'strong' => true,
  619. );
  620. $value = wp_kses( $value, $header_tags );
  621. break;
  622. case 'Author' :
  623. // There shouldn't be anchor tags in Author, but some themes like to be challenging.
  624. case 'Description' :
  625. static $header_tags_with_a = array(
  626. 'a' => array( 'href' => true, 'title' => true ),
  627. 'abbr' => array( 'title' => true ),
  628. 'acronym' => array( 'title' => true ),
  629. 'code' => true,
  630. 'em' => true,
  631. 'strong' => true,
  632. );
  633. $value = wp_kses( $value, $header_tags_with_a );
  634. break;
  635. case 'ThemeURI' :
  636. case 'AuthorURI' :
  637. $value = esc_url_raw( $value );
  638. break;
  639. case 'Tags' :
  640. $value = array_filter( array_map( 'trim', explode( ',', strip_tags( $value ) ) ) );
  641. break;
  642. case 'Version' :
  643. $value = strip_tags( $value );
  644. break;
  645. }
  646. return $value;
  647. }
  648. /**
  649. * Mark up a theme header.
  650. *
  651. * @since 3.4.0
  652. *
  653. * @staticvar string $comma
  654. *
  655. * @param string $header Theme header. Name, Description, Author, Version, ThemeURI, AuthorURI, Status, Tags.
  656. * @param string $value Value to mark up.
  657. * @param string $translate Whether the header has been translated.
  658. * @return string Value, marked up.
  659. */
  660. private function markup_header( $header, $value, $translate ) {
  661. switch ( $header ) {
  662. case 'Name' :
  663. if ( empty( $value ) ) {
  664. $value = esc_html( $this->get_stylesheet() );
  665. }
  666. break;
  667. case 'Description' :
  668. $value = wptexturize( $value );
  669. break;
  670. case 'Author' :
  671. if ( $this->get('AuthorURI') ) {
  672. $value = sprintf( '<a href="%1$s">%2$s</a>', $this->display( 'AuthorURI', true, $translate ), $value );
  673. } elseif ( ! $value ) {
  674. $value = __( 'Anonymous' );
  675. }
  676. break;
  677. case 'Tags' :
  678. static $comma = null;
  679. if ( ! isset( $comma ) ) {
  680. /* translators: used between list items, there is a space after the comma */
  681. $comma = __( ', ' );
  682. }
  683. $value = implode( $comma, $value );
  684. break;
  685. case 'ThemeURI' :
  686. case 'AuthorURI' :
  687. $value = esc_url( $value );
  688. break;
  689. }
  690. return $value;
  691. }
  692. /**
  693. * Translate a theme header.
  694. *
  695. * @since 3.4.0
  696. *
  697. * @staticvar array $tags_list
  698. *
  699. * @param string $header Theme header. Name, Description, Author, Version, ThemeURI, AuthorURI, Status, Tags.
  700. * @param string $value Value to translate.
  701. * @return string Translated value.
  702. */
  703. private function translate_header( $header, $value ) {
  704. switch ( $header ) {
  705. case 'Name' :
  706. // Cached for sorting reasons.
  707. if ( isset( $this->name_translated ) )
  708. return $this->name_translated;
  709. $this->name_translated = translate( $value, $this->get('TextDomain' ) );
  710. return $this->name_translated;
  711. case 'Tags' :
  712. if ( empty( $value ) || ! function_exists( 'get_theme_feature_list' ) ) {
  713. return $value;
  714. }
  715. static $tags_list;
  716. if ( ! isset( $tags_list ) ) {
  717. $tags_list = array(
  718. // As of 4.6, deprecated tags which are only used to provide translation for older themes.
  719. 'black' => __( 'Black' ), 'blue' => __( 'Blue' ), 'brown' => __( 'Brown' ),
  720. 'gray' => __( 'Gray' ), 'green' => __( 'Green' ), 'orange' => __( 'Orange' ),
  721. 'pink' => __( 'Pink' ), 'purple' => __( 'Purple' ), 'red' => __( 'Red' ),
  722. 'silver' => __( 'Silver' ), 'tan' => __( 'Tan' ), 'white' => __( 'White' ),
  723. 'yellow' => __( 'Yellow' ), 'dark' => __( 'Dark' ), 'light' => __( 'Light' ),
  724. 'fixed-layout' => __( 'Fixed Layout' ), 'fluid-layout' => __( 'Fluid Layout' ),
  725. 'responsive-layout' => __( 'Responsive Layout' ), 'blavatar' => __( 'Blavatar' ),
  726. 'photoblogging' => __( 'Photoblogging' ), 'seasonal' => __( 'Seasonal' ),
  727. );
  728. $feature_list = get_theme_feature_list( false ); // No API
  729. foreach ( $feature_list as $tags ) {
  730. $tags_list += $tags;
  731. }
  732. }
  733. foreach ( $value as &$tag ) {
  734. if ( isset( $tags_list[ $tag ] ) ) {
  735. $tag = $tags_list[ $tag ];
  736. } elseif ( isset( self::$tag_map[ $tag ] ) ) {
  737. $tag = $tags_list[ self::$tag_map[ $tag ] ];
  738. }
  739. }
  740. return $value;
  741. default :
  742. $value = translate( $value, $this->get('TextDomain') );
  743. }
  744. return $value;
  745. }
  746. /**
  747. * The directory name of the theme's "stylesheet" files, inside the theme root.
  748. *
  749. * In the case of a child theme, this is directory name of the child theme.
  750. * Otherwise, get_stylesheet() is the same as get_template().
  751. *
  752. * @since 3.4.0
  753. *
  754. * @return string Stylesheet
  755. */
  756. public function get_stylesheet() {
  757. return $this->stylesheet;
  758. }
  759. /**
  760. * The directory name of the theme's "template" files, inside the theme root.
  761. *
  762. * In the case of a child theme, this is the directory name of the parent theme.
  763. * Otherwise, the get_template() is the same as get_stylesheet().
  764. *
  765. * @since 3.4.0
  766. *
  767. * @return string Template
  768. */
  769. public function get_template() {
  770. return $this->template;
  771. }
  772. /**
  773. * Returns the absolute path to the directory of a theme's "stylesheet" files.
  774. *
  775. * In the case of a child theme, this is the absolute path to the directory
  776. * of the child theme's files.
  777. *
  778. * @since 3.4.0
  779. *
  780. * @return string Absolute path of the stylesheet directory.
  781. */
  782. public function get_stylesheet_directory() {
  783. if ( $this->errors() && in_array( 'theme_root_missing', $this->errors()->get_error_codes() ) )
  784. return '';
  785. return $this->theme_root . '/' . $this->stylesheet;
  786. }
  787. /**
  788. * Returns the absolute path to the directory of a theme's "template" files.
  789. *
  790. * In the case of a child theme, this is the absolute path to the directory
  791. * of the parent theme's files.
  792. *
  793. * @since 3.4.0
  794. *
  795. * @return string Absolute path of the template directory.
  796. */
  797. public function get_template_directory() {
  798. if ( $this->parent() )
  799. $theme_root = $this->parent()->theme_root;
  800. else
  801. $theme_root = $this->theme_root;
  802. return $theme_root . '/' . $this->template;
  803. }
  804. /**
  805. * Returns the URL to the directory of a theme's "stylesheet" files.
  806. *
  807. * In the case of a child theme, this is the URL to the directory of the
  808. * child theme's files.
  809. *
  810. * @since 3.4.0
  811. *
  812. * @return string URL to the stylesheet directory.
  813. */
  814. public function get_stylesheet_directory_uri() {
  815. return $this->get_theme_root_uri() . '/' . str_replace( '%2F', '/', rawurlencode( $this->stylesheet ) );
  816. }
  817. /**
  818. * Returns the URL to the directory of a theme's "template" files.
  819. *
  820. * In the case of a child theme, this is the URL to the directory of the
  821. * parent theme's files.
  822. *
  823. * @since 3.4.0
  824. *
  825. * @return string URL to the template directory.
  826. */
  827. public function get_template_directory_uri() {
  828. if ( $this->parent() )
  829. $theme_root_uri = $this->parent()->get_theme_root_uri();
  830. else
  831. $theme_root_uri = $this->get_theme_root_uri();
  832. return $theme_root_uri . '/' . str_replace( '%2F', '/', rawurlencode( $this->template ) );
  833. }
  834. /**
  835. * The absolute path to the directory of the theme root.
  836. *
  837. * This is typically the absolute path to wp-content/themes.
  838. *
  839. * @since 3.4.0
  840. *
  841. * @return string Theme root.
  842. */
  843. public function get_theme_root() {
  844. return $this->theme_root;
  845. }
  846. /**
  847. * Returns the URL to the directory of the theme root.
  848. *
  849. * This is typically the absolute URL to wp-content/themes. This forms the basis
  850. * for all other URLs returned by WP_Theme, so we pass it to the public function
  851. * get_theme_root_uri() and allow it to run the {@see 'theme_root_uri'} filter.
  852. *
  853. * @since 3.4.0
  854. *
  855. * @return string Theme root URI.
  856. */
  857. public function get_theme_root_uri() {
  858. if ( ! isset( $this->theme_root_uri ) )
  859. $this->theme_root_uri = get_theme_root_uri( $this->stylesheet, $this->theme_root );
  860. return $this->theme_root_uri;
  861. }
  862. /**
  863. * Returns the main screenshot file for the theme.
  864. *
  865. * The main screenshot is called screenshot.png. gif and jpg extensions are also allowed.
  866. *
  867. * Screenshots for a theme must be in the stylesheet directory. (In the case of child
  868. * themes, parent theme screenshots are not inherited.)
  869. *
  870. * @since 3.4.0
  871. *
  872. * @param string $uri Type of URL to return, either 'relative' or an absolute URI. Defaults to absolute URI.
  873. * @return string|false Screenshot file. False if the theme does not have a screenshot.
  874. */
  875. public function get_screenshot( $uri = 'uri' ) {
  876. $screenshot = $this->cache_get( 'screenshot' );
  877. if ( $screenshot ) {
  878. if ( 'relative' == $uri )
  879. return $screenshot;
  880. return $this->get_stylesheet_directory_uri() . '/' . $screenshot;
  881. } elseif ( 0 === $screenshot ) {
  882. return false;
  883. }
  884. foreach ( array( 'png', 'gif', 'jpg', 'jpeg' ) as $ext ) {
  885. if ( file_exists( $this->get_stylesheet_directory() . "/screenshot.$ext" ) ) {
  886. $this->cache_add( 'screenshot', 'screenshot.' . $ext );
  887. if ( 'relative' == $uri )
  888. return 'screenshot.' . $ext;
  889. return $this->get_stylesheet_directory_uri() . '/' . 'screenshot.' . $ext;
  890. }
  891. }
  892. $this->cache_add( 'screenshot', 0 );
  893. return false;
  894. }
  895. /**
  896. * Return files in the theme's directory.
  897. *
  898. * @since 3.4.0
  899. *
  900. * @param mixed $type Optional. Array of extensions to return. Defaults to all files (null).
  901. * @param int $depth Optional. How deep to search for files. Defaults to a flat scan (0 depth). -1 depth is infinite.
  902. * @param bool $search_parent Optional. Whether to return parent files. Defaults to false.
  903. * @return array Array of files, keyed by the path to the file relative to the theme's directory, with the values
  904. * being absolute paths.
  905. */
  906. public function get_files( $type = null, $depth = 0, $search_parent = false ) {
  907. $files = (array) self::scandir( $this->get_stylesheet_directory(), $type, $depth );
  908. if ( $search_parent && $this->parent() ) {
  909. $files += (array) self::scandir( $this->get_template_directory(), $type, $depth );
  910. }
  911. return $files;
  912. }
  913. /**
  914. * Returns the theme's post templates.
  915. *
  916. * @since 4.7.0
  917. *
  918. * @return array Array of page templates, keyed by filename and post type,
  919. * with the value of the translated header name.
  920. */
  921. public function get_post_templates() {
  922. // If you screw up your current theme and we invalidate your parent, most things still work. Let it slide.
  923. if ( $this->errors() && $this->errors()->get_error_codes() !== array( 'theme_parent_invalid' ) ) {
  924. return array();
  925. }
  926. $post_templates = $this->cache_get( 'post_templates' );
  927. if ( ! is_array( $post_templates ) ) {
  928. $post_templates = array();
  929. $files = (array) $this->get_files( 'php', 1, true);
  930. foreach ( $files as $file => $full_path ) {
  931. if ( ! preg_match( '|Template Name:(.*)$|mi', file_get_contents( $full_path ), $header ) ) {
  932. continue;
  933. }
  934. $types = array( 'page' );
  935. if ( preg_match( '|Template Post Type:(.*)$|mi', file_get_contents( $full_path ), $type ) ) {
  936. $types = explode( ',', _cleanup_header_comment( $type[1] ) );
  937. }
  938. foreach ( $types as $type ) {
  939. $type = sanitize_key( $type );
  940. if ( ! isset( $post_templates[ $type ] ) ) {
  941. $post_templates[ $type ] = array();
  942. }
  943. $post_templates[ $type ][ $file ] = _cleanup_header_comment( $header[1] );
  944. }
  945. }
  946. $this->cache_add( 'post_templates', $post_templates );
  947. }
  948. if ( $this->load_textdomain() ) {
  949. foreach ( $post_templates as &$post_type ) {
  950. foreach ( $post_type as &$post_template ) {
  951. $post_template = $this->translate_header( 'Template Name', $post_template );
  952. }
  953. }
  954. }
  955. return $post_templates;
  956. }
  957. /**
  958. * Returns the theme's post templates for a given post type.
  959. *
  960. * @since 3.4.0
  961. * @since 4.7.0 Added the `$post_type` parameter.
  962. *
  963. * @param WP_Post|null $post Optional. The post being edited, provided for context.
  964. * @param string $post_type Optional. Post type to get the templates for. Default 'page'.
  965. * If a post is provided, its post type is used.
  966. * @return array Array of page templates, keyed by filename, with the value of the translated header name.
  967. */
  968. public function get_page_templates( $post = null, $post_type = 'page' ) {
  969. if ( $post ) {
  970. $post_type = get_post_type( $post );
  971. }
  972. $post_templates = $this->get_post_templates();
  973. $post_templates = isset( $post_templates[ $post_type ] ) ? $post_templates[ $post_type ] : array();
  974. /**
  975. * Filters list of page templates for a theme.
  976. *
  977. * @since 4.9.6
  978. *
  979. * @param string[] $post_templates Array of page templates. Keys are filenames,
  980. * values are translated names.
  981. * @param WP_Theme $this The theme object.
  982. * @param WP_Post|null $post The post being edited, provided for context, or null.
  983. * @param string $post_type Post type to get the templates for.
  984. */
  985. $post_templates = (array) apply_filters( 'theme_templates', $post_templates, $this, $post, $post_type );
  986. /**
  987. * Filters list of page templates for a theme.
  988. *
  989. * The dynamic portion of the hook name, `$post_type`, refers to the post type.
  990. *
  991. * @since 3.9.0
  992. * @since 4.4.0 Converted to allow complete control over the `$page_templates` array.
  993. * @since 4.7.0 Added the `$post_type` parameter.
  994. *
  995. * @param array $post_templates Array of page templates. Keys are filenames,
  996. * values are translated names.
  997. * @param WP_Theme $this The theme object.
  998. * @param WP_Post|null $post The post being edited, provided for context, or null.
  999. * @param string $post_type Post type to get the templates for.
  1000. */
  1001. $post_templates = (array) apply_filters( "theme_{$post_type}_templates", $post_templates, $this, $post, $post_type );
  1002. return $post_templates;
  1003. }
  1004. /**
  1005. * Scans a directory for files of a certain extension.
  1006. *
  1007. * @since 3.4.0
  1008. *
  1009. * @static
  1010. *
  1011. * @param string $path Absolute path to search.
  1012. * @param array|string|null $extensions Optional. Array of extensions to find, string of a single extension,
  1013. * or null for all extensions. Default null.
  1014. * @param int $depth Optional. How many levels deep to search for files. Accepts 0, 1+, or
  1015. * -1 (infinite depth). Default 0.
  1016. * @param string $relative_path Optional. The basename of the absolute path. Used to control the
  1017. * returned path for the found files, particularly when this function
  1018. * recurses to lower depths. Default empty.
  1019. * @return array|false Array of files, keyed by the path to the file relative to the `$path` directory prepended
  1020. * with `$relative_path`, with the values being absolute paths. False otherwise.
  1021. */
  1022. private static function scandir( $path, $extensions = null, $depth = 0, $relative_path = '' ) {
  1023. if ( ! is_dir( $path ) ) {
  1024. return false;
  1025. }
  1026. if ( $extensions ) {
  1027. $extensions = (array) $extensions;
  1028. $_extensions = implode( '|', $extensions );
  1029. }
  1030. $relative_path = trailingslashit( $relative_path );
  1031. if ( '/' == $relative_path ) {
  1032. $relative_path = '';
  1033. }
  1034. $results = scandir( $path );
  1035. $files = array();
  1036. /**
  1037. * Filters the array of excluded directories and files while scanning theme folder.
  1038. *
  1039. * @since 4.7.4
  1040. *
  1041. * @param array $exclusions Array of excluded directories and files.
  1042. */
  1043. $exclusions = (array) apply_filters( 'theme_scandir_exclusions', array( 'CVS', 'node_modules', 'vendor', 'bower_components' ) );
  1044. foreach ( $results as $result ) {
  1045. if ( '.' == $result[0] || in_array( $result, $exclusions, true ) ) {
  1046. continue;
  1047. }
  1048. if ( is_dir( $path . '/' . $result ) ) {
  1049. if ( ! $depth ) {
  1050. continue;
  1051. }
  1052. $found = self::scandir( $path . '/' . $result, $extensions, $depth - 1 , $relative_path . $result );
  1053. $files = array_merge_recursive( $files, $found );
  1054. } elseif ( ! $extensions || preg_match( '~\.(' . $_extensions . ')$~', $result ) ) {
  1055. $files[ $relative_path . $result ] = $path . '/' . $result;
  1056. }
  1057. }
  1058. return $files;
  1059. }
  1060. /**
  1061. * Loads the theme's textdomain.
  1062. *
  1063. * Translation files are not inherited from the parent theme. Todo: if this fails for the
  1064. * child theme, it should probably try to load the parent theme's translations.
  1065. *
  1066. * @since 3.4.0
  1067. *
  1068. * @return bool True if the textdomain was successfully loaded or has already been loaded.
  1069. * False if no textdomain was specified in the file headers, or if the domain could not be loaded.
  1070. */
  1071. public function load_textdomain() {
  1072. if ( isset( $this->textdomain_loaded ) )
  1073. return $this->textdomain_loaded;
  1074. $textdomain = $this->get('TextDomain');
  1075. if ( ! $textdomain ) {
  1076. $this->textdomain_loaded = false;
  1077. return false;
  1078. }
  1079. if ( is_textdomain_loaded( $textdomain ) ) {
  1080. $this->textdomain_loaded = true;
  1081. return true;
  1082. }
  1083. $path = $this->get_stylesheet_directory();
  1084. if ( $domainpath = $this->get('DomainPath') )
  1085. $path .= $domainpath;
  1086. else
  1087. $path .= '/languages';
  1088. $this->textdomain_loaded = load_theme_textdomain( $textdomain, $path );
  1089. return $this->textdomain_loaded;
  1090. }
  1091. /**
  1092. * Whether the theme is allowed (multisite only).
  1093. *
  1094. * @since 3.4.0
  1095. *
  1096. * @param string $check Optional. Whether to check only the 'network'-wide settings, the 'site'
  1097. * settings, or 'both'. Defaults to 'both'.
  1098. * @param int $blog_id Optional. Ignored if only network-wide settings are checked. Defaults to current site.
  1099. * @return bool Whether the theme is allowed for the network. Returns true in single-site.
  1100. */
  1101. public function is_allowed( $check = 'both', $blog_id = null ) {
  1102. if ( ! is_multisite() )
  1103. return true;
  1104. if ( 'both' == $check || 'network' == $check ) {
  1105. $allowed = self::get_allowed_on_network();
  1106. if ( ! empty( $allowed[ $this->get_stylesheet() ] ) )
  1107. return true;
  1108. }
  1109. if ( 'both' == $check || 'site' == $check ) {
  1110. $allowed = self::get_allowed_on_site( $blog_id );
  1111. if ( ! empty( $allowed[ $this->get_stylesheet() ] ) )
  1112. return true;
  1113. }
  1114. return false;
  1115. }
  1116. /**
  1117. * Determines the latest WordPress default theme that is installed.
  1118. *
  1119. * This hits the filesystem.
  1120. *
  1121. * @since 4.4.0
  1122. *
  1123. * @return WP_Theme|false Object, or false if no theme is installed, which would be bad.
  1124. */
  1125. public static function get_core_default_theme() {
  1126. foreach ( array_reverse( self::$default_themes ) as $slug => $name ) {
  1127. $theme = wp_get_theme( $slug );
  1128. if ( $theme->exists() ) {
  1129. return $theme;
  1130. }
  1131. }
  1132. return false;
  1133. }
  1134. /**
  1135. * Returns array of stylesheet names of themes allowed on the site or network.
  1136. *
  1137. * @since 3.4.0
  1138. *
  1139. * @static
  1140. *
  1141. * @param int $blog_id Optional. ID of the site. Defaults to the current site.
  1142. * @return array Array of stylesheet names.
  1143. */
  1144. public static function get_allowed( $blog_id = null ) {
  1145. /**
  1146. * Filters the array of themes allowed on the network.
  1147. *
  1148. * Site is provided as context so that a list of network allowed themes can
  1149. * be filtered further.
  1150. *
  1151. * @since 4.5.0
  1152. *
  1153. * @param array $allowed_themes An array of theme stylesheet names.
  1154. * @param int $blog_id ID of the site.
  1155. */
  1156. $network = (array) apply_filters( 'network_allowed_themes', self::get_allowed_on_network(), $blog_id );
  1157. return $network + self::get_allowed_on_site( $blog_id );
  1158. }
  1159. /**
  1160. * Returns array of stylesheet names of themes allowed on the network.
  1161. *
  1162. * @since 3.4.0
  1163. *
  1164. * @static
  1165. *
  1166. * @staticvar array $allowed_themes
  1167. *
  1168. * @return array Array of stylesheet names.
  1169. */
  1170. public static function get_allowed_on_network() {
  1171. static $allowed_themes;
  1172. if ( ! isset( $allowed_themes ) ) {
  1173. $allowed_themes = (array) get_site_option( 'allowedthemes' );
  1174. }
  1175. /**
  1176. * Filters the array of themes allowed on the network.
  1177. *
  1178. * @since MU (3.0.0)
  1179. *
  1180. * @param array $allowed_themes An array of theme stylesheet names.
  1181. */
  1182. $allowed_themes = apply_filters( 'allowed_themes', $allowed_themes );
  1183. return $allowed_themes;
  1184. }
  1185. /**
  1186. * Returns array of stylesheet names of themes allowed on the site.
  1187. *
  1188. * @since 3.4.0
  1189. *
  1190. * @static
  1191. *
  1192. * @staticvar array $allowed_themes
  1193. *
  1194. * @param int $blog_id Optional. ID of the site. Defaults to the current site.
  1195. * @return array Array of stylesheet names.
  1196. */
  1197. public static function get_allowed_on_site( $blog_id = null ) {
  1198. static $allowed_themes = array();
  1199. if ( ! $blog_id || ! is_multisite() )
  1200. $blog_id = get_current_blog_id();
  1201. if ( isset( $allowed_themes[ $blog_id ] ) ) {
  1202. /**
  1203. * Filters the array of themes allowed on the site.
  1204. *
  1205. * @since 4.5.0
  1206. *
  1207. * @param array $allowed_themes An array of theme stylesheet names.
  1208. * @param int $blog_id ID of the site. Defaults to current site.
  1209. */
  1210. return (array) apply_filters( 'site_allowed_themes', $allowed_themes[ $blog_id ], $blog_id );
  1211. }
  1212. $current = $blog_id == get_current_blog_id();
  1213. if ( $current ) {
  1214. $allowed_themes[ $blog_id ] = get_option( 'allowedthemes' );
  1215. } else {
  1216. switch_to_blog( $blog_id );
  1217. $allowed_themes[ $blog_id ] = get_option( 'allowedthemes' );
  1218. restore_current_blog();
  1219. }
  1220. // This is all super old MU back compat joy.
  1221. // 'allowedthemes' keys things by stylesheet. 'allowed_themes' keyed things by name.
  1222. if ( false === $allowed_themes[ $blog_id ] ) {
  1223. if ( $current ) {
  1224. $allowed_themes[ $blog_id ] = get_option( 'allowed_themes' );
  1225. } else {
  1226. switch_to_blog( $blog_id );
  1227. $allowed_themes[ $blog_id ] = get_option( 'allowed_themes' );
  1228. restore_current_blog();
  1229. }
  1230. if ( ! is_array( $allowed_themes[ $blog_id ] ) || empty( $allowed_themes[ $blog_id ] ) ) {
  1231. $allowed_themes[ $blog_id ] = array();
  1232. } else {
  1233. $converted = array();
  1234. $themes = wp_get_themes();
  1235. foreach ( $themes as $stylesheet => $theme_data ) {
  1236. if ( isset( $allowed_themes[ $blog_id ][ $theme_data->get('Name') ] ) )
  1237. $converted[ $stylesheet ] = true;
  1238. }
  1239. $allowed_themes[ $blog_id ] = $converted;
  1240. }
  1241. // Set the option so we never have to go through this pain again.
  1242. if ( is_admin() && $allowed_themes[ $blog_id ] ) {
  1243. if ( $current ) {
  1244. update_option( 'allowedthemes', $allowed_themes[ $blog_id ] );
  1245. delete_option( 'allowed_themes' );
  1246. } else {
  1247. switch_to_blog( $blog_id );
  1248. update_option( 'allowedthemes', $allowed_themes[ $blog_id ] );
  1249. delete_option( 'allowed_themes' );
  1250. restore_current_blog();
  1251. }
  1252. }
  1253. }
  1254. /** This filter is documented in wp-includes/class-wp-theme.php */
  1255. return (array) apply_filters( 'site_allowed_themes', $allowed_themes[ $blog_id ], $blog_id );
  1256. }
  1257. /**
  1258. * Enables a theme for all sites on the current network.
  1259. *
  1260. * @since 4.6.0
  1261. * @static
  1262. *
  1263. * @param string|array $stylesheets Stylesheet name or array of stylesheet names.
  1264. */
  1265. public static function network_enable_theme( $stylesheets ) {
  1266. if ( ! is_multisite() ) {
  1267. return;
  1268. }
  1269. if ( ! is_array( $stylesheets ) ) {
  1270. $stylesheets = array( $stylesheets );
  1271. }
  1272. $allowed_themes = get_site_option( 'allowedthemes' );
  1273. foreach ( $stylesheets as $stylesheet ) {
  1274. $allowed_themes[ $stylesheet ] = true;
  1275. }
  1276. update_site_option( 'allowedthemes', $allowed_themes );
  1277. }
  1278. /**
  1279. * Disables a theme for all sites on the current network.
  1280. *
  1281. * @since 4.6.0
  1282. * @static
  1283. *
  1284. * @param string|array $stylesheets Stylesheet name or array of stylesheet names.
  1285. */
  1286. public static function network_disable_theme( $stylesheets ) {
  1287. if ( ! is_multisite() ) {
  1288. return;
  1289. }
  1290. if ( ! is_array( $stylesheets ) ) {
  1291. $stylesheets = array( $stylesheets );
  1292. }
  1293. $allowed_themes = get_site_option( 'allowedthemes' );
  1294. foreach ( $stylesheets as $stylesheet ) {
  1295. if ( isset( $allowed_themes[ $stylesheet ] ) ) {
  1296. unset( $allowed_themes[ $stylesheet ] );
  1297. }
  1298. }
  1299. update_site_option( 'allowedthemes', $allowed_themes );
  1300. }
  1301. /**
  1302. * Sorts themes by name.
  1303. *
  1304. * @since 3.4.0
  1305. *
  1306. * @static
  1307. *
  1308. * @param array $themes Array of themes to sort (passed by reference).
  1309. */
  1310. public static function sort_by_name( &$themes ) {
  1311. if ( 0 === strpos( get_user_locale(), 'en_' ) ) {
  1312. uasort( $themes, array( 'WP_Theme', '_name_sort' ) );
  1313. } else {
  1314. uasort( $themes, array( 'WP_Theme', '_name_sort_i18n' ) );
  1315. }
  1316. }
  1317. /**
  1318. * Callback function for usort() to naturally sort themes by name.
  1319. *
  1320. * Accesses the Name header directly from the class for maximum speed.
  1321. * Would choke on HTML but we don't care enough to slow it down with strip_tags().
  1322. *
  1323. * @since 3.4.0
  1324. *
  1325. * @static
  1326. *
  1327. * @param string $a First name.
  1328. * @param string $b Second name.
  1329. * @return int Negative if `$a` falls lower in the natural order than `$b`. Zero if they fall equally.
  1330. * Greater than 0 if `$a` falls higher in the natural order than `$b`. Used with usort().
  1331. */
  1332. private static function _name_sort( $a, $b ) {
  1333. return strnatcasecmp( $a->headers['Name'], $b->headers['Name'] );
  1334. }
  1335. /**
  1336. * Name sort (with translation).
  1337. *
  1338. * @since 3.4.0
  1339. *
  1340. * @static
  1341. *
  1342. * @param string $a First name.
  1343. * @param string $b Second name.
  1344. * @return int Negative if `$a` falls lower in the natural order than `$b`. Zero if they fall equally.
  1345. * Greater than 0 if `$a` falls higher in the natural order than `$b`. Used with usort().
  1346. */
  1347. private static function _name_sort_i18n( $a, $b ) {
  1348. // Don't mark up; Do translate.
  1349. return strnatcasecmp( $a->display( 'Name', false, true ), $b->display( 'Name', false, true ) );
  1350. }
  1351. }