class.jetpack-sync-module-themes.php 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616
  1. <?php
  2. class Jetpack_Sync_Module_Themes extends Jetpack_Sync_Module {
  3. function name() {
  4. return 'themes';
  5. }
  6. public function init_listeners( $callable ) {
  7. add_action( 'switch_theme', array( $this, 'sync_theme_support' ), 10, 3 );
  8. add_action( 'jetpack_sync_current_theme_support', $callable, 10, 2 );
  9. add_action( 'upgrader_process_complete', array( $this, 'check_upgrader'), 10, 2 );
  10. add_action( 'jetpack_installed_theme', $callable, 10, 2 );
  11. add_action( 'jetpack_updated_themes', $callable, 10, 2 );
  12. add_action( 'delete_site_transient_update_themes', array( $this, 'detect_theme_deletion') );
  13. add_action( 'jetpack_deleted_theme', $callable, 10, 2 );
  14. add_filter( 'wp_redirect', array( $this, 'detect_theme_edit' ) );
  15. add_action( 'jetpack_edited_theme', $callable, 10, 2 );
  16. add_action( 'wp_ajax_edit-theme-plugin-file', array( $this, 'theme_edit_ajax' ), 0 );
  17. add_action( 'update_site_option_allowedthemes', array( $this, 'sync_network_allowed_themes_change' ), 10, 4 );
  18. add_action( 'jetpack_network_disabled_themes', $callable, 10, 2 );
  19. add_action( 'jetpack_network_enabled_themes', $callable, 10, 2 );
  20. // Sidebar updates.
  21. add_action( 'update_option_sidebars_widgets', array( $this, 'sync_sidebar_widgets_actions' ), 10, 2 );
  22. add_action( 'jetpack_widget_added', $callable, 10, 4 );
  23. add_action( 'jetpack_widget_removed', $callable, 10, 4 );
  24. add_action( 'jetpack_widget_moved_to_inactive', $callable, 10, 2 );
  25. add_action( 'jetpack_cleared_inactive_widgets', $callable );
  26. add_action( 'jetpack_widget_reordered', $callable, 10, 2 );
  27. add_filter( 'widget_update_callback', array( $this, 'sync_widget_edit' ), 10, 4 );
  28. add_action( 'jetpack_widget_edited', $callable );
  29. }
  30. public function sync_widget_edit( $instance, $new_instance, $old_instance, $widget_object ) {
  31. if ( empty( $old_instance ) ) {
  32. return $instance;
  33. }
  34. // Don't trigger sync action if this is an ajax request, because Customizer makes them during preview before saving changes
  35. if ( defined( 'DOING_AJAX' ) && DOING_AJAX && isset( $_POST['customized'] ) ) {
  36. return $instance;
  37. }
  38. $widget = array(
  39. 'name' => $widget_object->name,
  40. 'id' => $widget_object->id,
  41. 'title' => isset( $new_instance['title'] ) ? $new_instance['title'] : '',
  42. );
  43. /**
  44. * Trigger action to alert $callable sync listener that a widget was edited
  45. *
  46. * @since 5.0.0
  47. *
  48. * @param string $widget_name , Name of edited widget
  49. */
  50. do_action( 'jetpack_widget_edited', $widget );
  51. return $instance;
  52. }
  53. public function sync_network_allowed_themes_change( $option, $value, $old_value, $network_id ) {
  54. $all_enabled_theme_slugs = array_keys( $value );
  55. if ( count( $old_value ) > count( $value ) ) {
  56. //Suppress jetpack_network_disabled_themes sync action when theme is deleted
  57. $delete_theme_call = $this->get_delete_theme_call();
  58. if ( ! empty( $delete_theme_call ) ) {
  59. return;
  60. }
  61. $newly_disabled_theme_names = array_keys( array_diff_key( $old_value, $value ) );
  62. $newly_disabled_themes = $this->get_theme_details_for_slugs( $newly_disabled_theme_names );
  63. /**
  64. * Trigger action to alert $callable sync listener that network themes were disabled
  65. *
  66. * @since 5.0.0
  67. *
  68. * @param mixed $newly_disabled_themes, Array of info about network disabled themes
  69. * @param mixed $all_enabled_theme_slugs, Array of slugs of all enabled themes
  70. */
  71. do_action( 'jetpack_network_disabled_themes', $newly_disabled_themes, $all_enabled_theme_slugs );
  72. return;
  73. }
  74. $newly_enabled_theme_names = array_keys( array_diff_key( $value, $old_value ) );
  75. $newly_enabled_themes = $this->get_theme_details_for_slugs( $newly_enabled_theme_names );
  76. /**
  77. * Trigger action to alert $callable sync listener that network themes were enabled
  78. *
  79. * @since 5.0.0
  80. *
  81. * @param mixed $newly_enabled_themes , Array of info about network enabled themes
  82. * @param mixed $all_enabled_theme_slugs, Array of slugs of all enabled themes
  83. */
  84. do_action( 'jetpack_network_enabled_themes', $newly_enabled_themes, $all_enabled_theme_slugs );
  85. }
  86. private function get_theme_details_for_slugs( $theme_slugs ) {
  87. $theme_data = array();
  88. foreach ( $theme_slugs as $slug ) {
  89. $theme = wp_get_theme( $slug );
  90. $theme_data[ $slug ] = array(
  91. 'name' => $theme->get( 'Name' ),
  92. 'version' => $theme->get( 'Version' ),
  93. 'uri' => $theme->get( 'ThemeURI' ),
  94. 'slug' => $slug,
  95. );
  96. }
  97. return $theme_data;
  98. }
  99. public function detect_theme_edit( $redirect_url ) {
  100. $url = wp_parse_url( admin_url( $redirect_url ) );
  101. $theme_editor_url = wp_parse_url( admin_url( 'theme-editor.php' ) );
  102. if ( $theme_editor_url['path'] !== $url['path'] ) {
  103. return $redirect_url;
  104. }
  105. $query_params = array();
  106. wp_parse_str( $url['query'], $query_params );
  107. if (
  108. ! isset( $_POST['newcontent'] ) ||
  109. ! isset( $query_params['file'] ) ||
  110. ! isset( $query_params['theme'] ) ||
  111. ! isset( $query_params['updated'] )
  112. ) {
  113. return $redirect_url;
  114. }
  115. $theme = wp_get_theme( $query_params['theme'] );
  116. $theme_data = array(
  117. 'name' => $theme->get('Name'),
  118. 'version' => $theme->get('Version'),
  119. 'uri' => $theme->get( 'ThemeURI' ),
  120. );
  121. /**
  122. * Trigger action to alert $callable sync listener that a theme was edited
  123. *
  124. * @since 5.0.0
  125. *
  126. * @param string $query_params['theme'], Slug of edited theme
  127. * @param string $theme_data, Information about edited them
  128. */
  129. do_action( 'jetpack_edited_theme', $query_params['theme'], $theme_data );
  130. return $redirect_url;
  131. }
  132. public function theme_edit_ajax() {
  133. $args = wp_unslash( $_POST );
  134. if ( empty( $args['theme'] ) ) {
  135. return;
  136. }
  137. if ( empty( $args['file'] ) ) {
  138. return;
  139. }
  140. $file = $args['file'];
  141. if ( 0 !== validate_file( $file ) ) {
  142. return;
  143. }
  144. if ( ! isset( $args['newcontent'] ) ) {
  145. return;
  146. }
  147. if ( ! isset( $args['nonce'] ) ) {
  148. return;
  149. }
  150. $stylesheet = $args['theme'];
  151. if ( 0 !== validate_file( $stylesheet ) ) {
  152. return;
  153. }
  154. if ( ! current_user_can( 'edit_themes' ) ) {
  155. return;
  156. }
  157. $theme = wp_get_theme( $stylesheet );
  158. if ( ! $theme->exists() ) {
  159. return;
  160. }
  161. $real_file = $theme->get_stylesheet_directory() . '/' . $file;
  162. if ( ! wp_verify_nonce( $args['nonce'], 'edit-theme_' . $real_file . $stylesheet ) ) {
  163. return;
  164. }
  165. if ( $theme->errors() && 'theme_no_stylesheet' === $theme->errors()->get_error_code() ) {
  166. return;
  167. }
  168. $editable_extensions = wp_get_theme_file_editable_extensions( $theme );
  169. $allowed_files = array();
  170. foreach ( $editable_extensions as $type ) {
  171. switch ( $type ) {
  172. case 'php':
  173. $allowed_files = array_merge( $allowed_files, $theme->get_files( 'php', -1 ) );
  174. break;
  175. case 'css':
  176. $style_files = $theme->get_files( 'css', -1 );
  177. $allowed_files['style.css'] = $style_files['style.css'];
  178. $allowed_files = array_merge( $allowed_files, $style_files );
  179. break;
  180. default:
  181. $allowed_files = array_merge( $allowed_files, $theme->get_files( $type, -1 ) );
  182. break;
  183. }
  184. }
  185. if ( 0 !== validate_file( $real_file, $allowed_files ) ) {
  186. return;
  187. }
  188. // Ensure file is real.
  189. if ( ! is_file( $real_file ) ) {
  190. return;
  191. }
  192. // Ensure file extension is allowed.
  193. $extension = null;
  194. if ( preg_match( '/\.([^.]+)$/', $real_file, $matches ) ) {
  195. $extension = strtolower( $matches[1] );
  196. if ( ! in_array( $extension, $editable_extensions, true ) ) {
  197. return;
  198. }
  199. }
  200. if ( ! is_writeable( $real_file ) ) {
  201. return;
  202. }
  203. $file_pointer = fopen( $real_file, 'w+' );
  204. if ( false === $file_pointer ) {
  205. return;
  206. }
  207. fclose( $file_pointer );
  208. $theme_data = array(
  209. 'name' => $theme->get('Name'),
  210. 'version' => $theme->get('Version'),
  211. 'uri' => $theme->get( 'ThemeURI' ),
  212. );
  213. /**
  214. * This action is documented already in this file
  215. */
  216. do_action( 'jetpack_edited_theme', $stylesheet, $theme_data );
  217. }
  218. public function detect_theme_deletion() {
  219. $delete_theme_call = $this->get_delete_theme_call();
  220. if ( empty( $delete_theme_call ) ) {
  221. return;
  222. }
  223. $slug = $delete_theme_call['args'][0];
  224. $theme = wp_get_theme( $slug );
  225. $theme_data = array(
  226. 'name' => $theme->get('Name'),
  227. 'version' => $theme->get('Version'),
  228. 'uri' => $theme->get( 'ThemeURI' ),
  229. 'slug' => $slug,
  230. );
  231. /**
  232. * Signals to the sync listener that a theme was deleted and a sync action
  233. * reflecting the deletion and theme slug should be sent
  234. *
  235. * @since 5.0.0
  236. *
  237. * @param string $slug Theme slug
  238. * @param array $theme_data Theme info Since 5.3
  239. */
  240. do_action( 'jetpack_deleted_theme', $slug, $theme_data );
  241. }
  242. public function check_upgrader( $upgrader, $details ) {
  243. if ( ! isset( $details['type'] ) ||
  244. 'theme' !== $details['type'] ||
  245. is_wp_error( $upgrader->skin->result ) ||
  246. ! method_exists( $upgrader, 'theme_info' )
  247. ) {
  248. return;
  249. }
  250. if ( 'install' === $details['action'] ) {
  251. $theme = $upgrader->theme_info();
  252. if ( ! $theme instanceof WP_Theme ) {
  253. return;
  254. }
  255. $theme_info = array(
  256. 'name' => $theme->get( 'Name' ),
  257. 'version' => $theme->get( 'Version' ),
  258. 'uri' => $theme->get( 'ThemeURI' ),
  259. );
  260. /**
  261. * Signals to the sync listener that a theme was installed and a sync action
  262. * reflecting the installation and the theme info should be sent
  263. *
  264. * @since 4.9.0
  265. *
  266. * @param string $theme->theme_root Text domain of the theme
  267. * @param mixed $theme_info Array of abbreviated theme info
  268. */
  269. do_action( 'jetpack_installed_theme', $theme->stylesheet, $theme_info );
  270. }
  271. if ( 'update' === $details['action'] ) {
  272. $themes = array();
  273. if ( empty( $details['themes'] ) && isset ( $details['theme'] ) ) {
  274. $details['themes'] = array( $details['theme'] );
  275. }
  276. foreach ( $details['themes'] as $theme_slug ) {
  277. $theme = wp_get_theme( $theme_slug );
  278. if ( ! $theme instanceof WP_Theme ) {
  279. continue;
  280. }
  281. $themes[ $theme_slug ] = array(
  282. 'name' => $theme->get( 'Name' ),
  283. 'version' => $theme->get( 'Version' ),
  284. 'uri' => $theme->get( 'ThemeURI' ),
  285. 'stylesheet' => $theme->stylesheet,
  286. );
  287. }
  288. if ( empty( $themes ) ) {
  289. return;
  290. }
  291. /**
  292. * Signals to the sync listener that one or more themes was updated and a sync action
  293. * reflecting the update and the theme info should be sent
  294. *
  295. * @since 6.2.0
  296. *
  297. * @param mixed $themes Array of abbreviated theme info
  298. */
  299. do_action( 'jetpack_updated_themes', $themes );
  300. }
  301. }
  302. public function init_full_sync_listeners( $callable ) {
  303. add_action( 'jetpack_full_sync_theme_data', $callable );
  304. }
  305. public function sync_theme_support( $new_name, $new_theme, $old_theme = null ) {
  306. // Previous theme support got added in WP 4.5
  307. $previous_theme = false;
  308. if ( $old_theme instanceof WP_Theme ) {
  309. $previous_theme = $this->get_theme_support_info( $old_theme );
  310. }
  311. /**
  312. * Fires when the client needs to sync theme support info
  313. * Only sends theme support attributes whitelisted in Jetpack_Sync_Defaults::$default_theme_support_whitelist
  314. *
  315. * @since 4.2.0
  316. *
  317. * @param array the theme support array
  318. * @param array the previous theme since Jetpack 6.5.0
  319. */
  320. do_action( 'jetpack_sync_current_theme_support' , $this->get_theme_support_info(), $previous_theme );
  321. }
  322. public function enqueue_full_sync_actions( $config, $max_items_to_enqueue, $state ) {
  323. /**
  324. * Tells the client to sync all theme data to the server
  325. *
  326. * @since 4.2.0
  327. *
  328. * @param boolean Whether to expand theme data (should always be true)
  329. */
  330. do_action( 'jetpack_full_sync_theme_data', true );
  331. // The number of actions enqueued, and next module state (true == done)
  332. return array( 1, true );
  333. }
  334. public function estimate_full_sync_actions( $config ) {
  335. return 1;
  336. }
  337. public function init_before_send() {
  338. add_filter( 'jetpack_sync_before_send_jetpack_full_sync_theme_data', array( $this, 'expand_theme_data' ) );
  339. }
  340. function get_full_sync_actions() {
  341. return array( 'jetpack_full_sync_theme_data' );
  342. }
  343. function expand_theme_data() {
  344. return array( $this->get_theme_support_info() );
  345. }
  346. function get_widget_name( $widget_id ) {
  347. global $wp_registered_widgets;
  348. return ( isset( $wp_registered_widgets[ $widget_id ] ) ? $wp_registered_widgets[ $widget_id ]['name'] : null );
  349. }
  350. function get_sidebar_name( $sidebar_id ) {
  351. global $wp_registered_sidebars;
  352. return ( isset( $wp_registered_sidebars[ $sidebar_id ] ) ? $wp_registered_sidebars[ $sidebar_id ]['name'] : null );
  353. }
  354. function sync_add_widgets_to_sidebar( $new_widgets, $old_widgets, $sidebar ) {
  355. $added_widgets = array_diff( $new_widgets, $old_widgets );
  356. if ( empty( $added_widgets ) ) {
  357. return array();
  358. }
  359. $moved_to_sidebar = array();
  360. $sidebar_name = $this->get_sidebar_name( $sidebar );
  361. //Don't sync jetpack_widget_added if theme was switched
  362. if ( $this->is_theme_switch() ) {
  363. return array();
  364. }
  365. foreach ( $added_widgets as $added_widget ) {
  366. $moved_to_sidebar[] = $added_widget;
  367. $added_widget_name = $this->get_widget_name( $added_widget );
  368. /**
  369. * Helps Sync log that a widget got added
  370. *
  371. * @since 4.9.0
  372. *
  373. * @param string $sidebar, Sidebar id got changed
  374. * @param string $added_widget, Widget id got added
  375. * @param string $sidebar_name, Sidebar id got changed Since 5.0.0
  376. * @param string $added_widget_name, Widget id got added Since 5.0.0
  377. *
  378. */
  379. do_action( 'jetpack_widget_added', $sidebar, $added_widget, $sidebar_name, $added_widget_name );
  380. }
  381. return $moved_to_sidebar;
  382. }
  383. function sync_remove_widgets_from_sidebar( $new_widgets, $old_widgets, $sidebar, $inactive_widgets ) {
  384. $removed_widgets = array_diff( $old_widgets, $new_widgets );
  385. if ( empty( $removed_widgets ) ) {
  386. return array();
  387. }
  388. $moved_to_inactive = array();
  389. $sidebar_name = $this->get_sidebar_name( $sidebar );
  390. foreach( $removed_widgets as $removed_widget ) {
  391. // Lets check if we didn't move the widget to in_active_widgets
  392. if ( isset( $inactive_widgets ) && ! in_array( $removed_widget, $inactive_widgets ) ) {
  393. $removed_widget_name = $this->get_widget_name( $removed_widget );
  394. /**
  395. * Helps Sync log that a widgte got removed
  396. *
  397. * @since 4.9.0
  398. *
  399. * @param string $sidebar, Sidebar id got changed
  400. * @param string $removed_widget, Widget id got removed
  401. * @param string $sidebar_name, Name of the sidebar that changed Since 5.0.0
  402. * @param string $removed_widget_name, Name of the widget that got removed Since 5.0.0
  403. */
  404. do_action( 'jetpack_widget_removed', $sidebar, $removed_widget, $sidebar_name, $removed_widget_name );
  405. } else {
  406. $moved_to_inactive[] = $removed_widget;
  407. }
  408. }
  409. return $moved_to_inactive;
  410. }
  411. function sync_widgets_reordered( $new_widgets, $old_widgets, $sidebar ) {
  412. $added_widgets = array_diff( $new_widgets, $old_widgets );
  413. if ( ! empty( $added_widgets ) ) {
  414. return;
  415. }
  416. $removed_widgets = array_diff( $old_widgets, $new_widgets );
  417. if ( ! empty( $removed_widgets ) ) {
  418. return;
  419. }
  420. if ( serialize( $old_widgets ) !== serialize( $new_widgets ) ) {
  421. $sidebar_name = $this->get_sidebar_name( $sidebar );
  422. /**
  423. * Helps Sync log that a sidebar id got reordered
  424. *
  425. * @since 4.9.0
  426. *
  427. * @param string $sidebar, Sidebar id got changed
  428. * @param string $sidebar_name, Name of the sidebar that changed Since 5.0.0
  429. */
  430. do_action( 'jetpack_widget_reordered', $sidebar, $sidebar_name );
  431. }
  432. }
  433. function sync_sidebar_widgets_actions( $old_value, $new_value ) {
  434. // Don't really know how to deal with different array_values yet.
  435. if (
  436. ( isset( $old_value['array_version'] ) && $old_value['array_version'] !== 3 ) ||
  437. ( isset( $new_value['array_version'] ) && $new_value['array_version'] !== 3 )
  438. ) {
  439. return;
  440. }
  441. $moved_to_inactive_ids = array();
  442. $moved_to_sidebar = array();
  443. foreach ( $new_value as $sidebar => $new_widgets ) {
  444. if ( in_array( $sidebar, array( 'array_version', 'wp_inactive_widgets' ) ) ) {
  445. continue;
  446. }
  447. $old_widgets = isset( $old_value[ $sidebar ] )
  448. ? $old_value[ $sidebar ]
  449. : array();
  450. if ( ! is_array( $new_widgets ) ) {
  451. $new_widgets = array();
  452. }
  453. $moved_to_inactive_recently = $this->sync_remove_widgets_from_sidebar( $new_widgets, $old_widgets, $sidebar, $new_value['wp_inactive_widgets'] );
  454. $moved_to_inactive_ids = array_merge( $moved_to_inactive_ids, $moved_to_inactive_recently );
  455. $moved_to_sidebar_recently = $this->sync_add_widgets_to_sidebar( $new_widgets, $old_widgets, $sidebar );
  456. $moved_to_sidebar = array_merge( $moved_to_sidebar, $moved_to_sidebar_recently );
  457. $this->sync_widgets_reordered( $new_widgets, $old_widgets, $sidebar );
  458. }
  459. //Don't sync either jetpack_widget_moved_to_inactive or jetpack_cleared_inactive_widgets if theme was switched
  460. if ( $this->is_theme_switch() ) {
  461. return;
  462. }
  463. // Treat inactive sidebar a bit differently
  464. if ( ! empty( $moved_to_inactive_ids ) ) {
  465. $moved_to_inactive_name = array_map( array( $this, 'get_widget_name' ), $moved_to_inactive_ids );
  466. /**
  467. * Helps Sync log that a widgets IDs got moved to in active
  468. *
  469. * @since 4.9.0
  470. *
  471. * @param array $moved_to_inactive_ids, Array of widgets id that moved to inactive id got changed
  472. * @param array $moved_to_inactive_names, Array of widgets names that moved to inactive id got changed Since 5.0.0
  473. */
  474. do_action( 'jetpack_widget_moved_to_inactive', $moved_to_inactive_ids, $moved_to_inactive_name );
  475. } elseif ( empty( $moved_to_sidebar ) &&
  476. empty( $new_value['wp_inactive_widgets']) &&
  477. ! empty( $old_value['wp_inactive_widgets'] ) ) {
  478. /**
  479. * Helps Sync log that a got cleared from inactive.
  480. *
  481. * @since 4.9.0
  482. */
  483. do_action( 'jetpack_cleared_inactive_widgets' );
  484. }
  485. }
  486. /**
  487. * @param null $theme or the theme object
  488. *
  489. * @return array
  490. */
  491. private function get_theme_support_info( $theme = null ) {
  492. global $_wp_theme_features;
  493. $theme_support = array();
  494. // We are trying to get the current theme info.
  495. if ( $theme === null ) {
  496. $theme = wp_get_theme();
  497. foreach ( Jetpack_Sync_Defaults::$default_theme_support_whitelist as $theme_feature ) {
  498. $has_support = current_theme_supports( $theme_feature );
  499. if ( $has_support ) {
  500. $theme_support[ $theme_feature ] = $_wp_theme_features[ $theme_feature ];
  501. }
  502. }
  503. }
  504. $theme_support['name'] = $theme->get('Name');
  505. $theme_support['version'] = $theme->get('Version');
  506. $theme_support['slug'] = $theme->get_stylesheet();
  507. $theme_support['uri'] = $theme->get('ThemeURI');
  508. return $theme_support;
  509. }
  510. private function get_delete_theme_call() {
  511. $backtrace = debug_backtrace();
  512. $delete_theme_call = null;
  513. foreach ( $backtrace as $call ) {
  514. if ( isset( $call['function'] ) && 'delete_theme' === $call['function'] ) {
  515. $delete_theme_call = $call;
  516. break;
  517. }
  518. }
  519. return $delete_theme_call;
  520. }
  521. private function is_theme_switch() {
  522. return did_action( 'after_switch_theme' );
  523. }
  524. }