class-admin.php 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496
  1. <?php
  2. /**
  3. * WPSEO plugin file.
  4. *
  5. * @package WPSEO\Admin
  6. */
  7. /**
  8. * Class that holds most of the admin functionality for Yoast SEO.
  9. */
  10. class WPSEO_Admin {
  11. /**
  12. * The page identifier used in WordPress to register the admin page.
  13. *
  14. * !DO NOT CHANGE THIS!
  15. *
  16. * @var string
  17. */
  18. const PAGE_IDENTIFIER = 'wpseo_dashboard';
  19. /**
  20. * Array of classes that add admin functionality.
  21. *
  22. * @var array
  23. */
  24. protected $admin_features;
  25. /**
  26. * Class constructor.
  27. */
  28. public function __construct() {
  29. $integrations = [];
  30. global $pagenow;
  31. $wpseo_menu = new WPSEO_Menu();
  32. $wpseo_menu->register_hooks();
  33. if ( is_multisite() ) {
  34. WPSEO_Options::maybe_set_multisite_defaults( false );
  35. }
  36. if ( WPSEO_Options::get( 'stripcategorybase' ) === true ) {
  37. add_action( 'created_category', [ $this, 'schedule_rewrite_flush' ] );
  38. add_action( 'edited_category', [ $this, 'schedule_rewrite_flush' ] );
  39. add_action( 'delete_category', [ $this, 'schedule_rewrite_flush' ] );
  40. }
  41. if ( WPSEO_Options::get( 'disable-attachment' ) === true ) {
  42. add_filter( 'wpseo_accessible_post_types', [ 'WPSEO_Post_Type', 'filter_attachment_post_type' ] );
  43. }
  44. if ( filter_input( INPUT_GET, 'page' ) === 'wpseo_tools' && filter_input( INPUT_GET, 'tool' ) === null ) {
  45. new WPSEO_Recalculate_Scores();
  46. }
  47. add_filter( 'plugin_action_links_' . WPSEO_BASENAME, [ $this, 'add_action_link' ], 10, 2 );
  48. add_action( 'admin_enqueue_scripts', [ $this, 'config_page_scripts' ] );
  49. add_action( 'admin_enqueue_scripts', [ $this, 'enqueue_global_style' ] );
  50. add_filter( 'user_contactmethods', [ $this, 'update_contactmethods' ], 10, 1 );
  51. add_action( 'after_switch_theme', [ $this, 'switch_theme' ] );
  52. add_action( 'switch_theme', [ $this, 'switch_theme' ] );
  53. add_filter( 'set-screen-option', [ $this, 'save_bulk_edit_options' ], 10, 3 );
  54. add_action( 'admin_init', [ 'WPSEO_Plugin_Conflict', 'hook_check_for_plugin_conflicts' ], 10, 1 );
  55. add_action( 'admin_init', [ $this, 'map_manage_options_cap' ] );
  56. WPSEO_Sitemaps_Cache::register_clear_on_option_update( 'wpseo' );
  57. WPSEO_Sitemaps_Cache::register_clear_on_option_update( 'home' );
  58. if ( WPSEO_Utils::is_yoast_seo_page() ) {
  59. add_action( 'admin_enqueue_scripts', [ $this, 'enqueue_assets' ] );
  60. }
  61. if ( WPSEO_Utils::is_api_available() ) {
  62. $configuration = new WPSEO_Configuration_Page();
  63. $configuration->set_hooks();
  64. $configuration->catch_configuration_request();
  65. }
  66. $this->set_upsell_notice();
  67. $this->initialize_cornerstone_content();
  68. if ( WPSEO_Utils::is_plugin_network_active() ) {
  69. $integrations[] = new Yoast_Network_Admin();
  70. }
  71. $this->admin_features = [
  72. 'dashboard_widget' => new Yoast_Dashboard_Widget(),
  73. ];
  74. if ( WPSEO_Metabox::is_post_overview( $pagenow ) || WPSEO_Metabox::is_post_edit( $pagenow ) ) {
  75. $this->admin_features['primary_category'] = new WPSEO_Primary_Term_Admin();
  76. }
  77. $integrations[] = new WPSEO_Yoast_Columns();
  78. $integrations[] = new WPSEO_License_Page_Manager();
  79. $integrations[] = new WPSEO_Statistic_Integration();
  80. $integrations[] = new WPSEO_Capability_Manager_Integration( WPSEO_Capability_Manager_Factory::get() );
  81. $integrations[] = new WPSEO_Admin_Media_Purge_Notification();
  82. $integrations[] = new WPSEO_Admin_Gutenberg_Compatibility_Notification();
  83. $integrations[] = new WPSEO_Expose_Shortlinks();
  84. $integrations[] = new WPSEO_MyYoast_Proxy();
  85. $integrations[] = new WPSEO_MyYoast_Route();
  86. $integrations[] = new WPSEO_Schema_Person_Upgrade_Notification();
  87. $integrations[] = new WPSEO_Tracking( 'https://tracking.yoast.com/stats', ( WEEK_IN_SECONDS * 2 ) );
  88. $integrations[] = new WPSEO_Admin_Settings_Changed_Listener();
  89. $integrations[] = $this->get_helpscout_beacon();
  90. $integrations = array_merge(
  91. $integrations,
  92. $this->get_admin_features(),
  93. $this->initialize_seo_links(),
  94. $this->initialize_cornerstone_content()
  95. );
  96. foreach ( $integrations as $integration ) {
  97. $integration->register_hooks();
  98. }
  99. }
  100. /**
  101. * Schedules a rewrite flush to happen at shutdown.
  102. */
  103. public function schedule_rewrite_flush() {
  104. add_action( 'shutdown', 'flush_rewrite_rules' );
  105. }
  106. /**
  107. * Returns all the classes for the admin features.
  108. *
  109. * @return array
  110. */
  111. public function get_admin_features() {
  112. return $this->admin_features;
  113. }
  114. /**
  115. * Register assets needed on admin pages.
  116. */
  117. public function enqueue_assets() {
  118. if ( 'wpseo_licenses' === filter_input( INPUT_GET, 'page' ) ) {
  119. $asset_manager = new WPSEO_Admin_Asset_Manager();
  120. $asset_manager->enqueue_style( 'extensions' );
  121. }
  122. }
  123. /**
  124. * Returns the manage_options capability.
  125. *
  126. * @return string The capability to use.
  127. */
  128. public function get_manage_options_cap() {
  129. /**
  130. * Filter: 'wpseo_manage_options_capability' - Allow changing the capability users need to view the settings pages.
  131. *
  132. * @api string unsigned The capability.
  133. */
  134. return apply_filters( 'wpseo_manage_options_capability', 'wpseo_manage_options' );
  135. }
  136. /**
  137. * Maps the manage_options cap on saving an options page to wpseo_manage_options.
  138. */
  139. public function map_manage_options_cap() {
  140. $option_page = ! empty( $_POST['option_page'] ) ? $_POST['option_page'] : ''; // WPCS: CSRF ok.
  141. if ( strpos( $option_page, 'yoast_wpseo' ) === 0 ) {
  142. add_filter( 'option_page_capability_' . $option_page, [ $this, 'get_manage_options_cap' ] );
  143. }
  144. }
  145. /**
  146. * Adds the ability to choose how many posts are displayed per page
  147. * on the bulk edit pages.
  148. */
  149. public function bulk_edit_options() {
  150. $option = 'per_page';
  151. $args = [
  152. 'label' => __( 'Posts', 'wordpress-seo' ),
  153. 'default' => 10,
  154. 'option' => 'wpseo_posts_per_page',
  155. ];
  156. add_screen_option( $option, $args );
  157. }
  158. /**
  159. * Saves the posts per page limit for bulk edit pages.
  160. *
  161. * @param int $status Status value to pass through.
  162. * @param string $option Option name.
  163. * @param int $value Count value to check.
  164. *
  165. * @return int
  166. */
  167. public function save_bulk_edit_options( $status, $option, $value ) {
  168. if ( 'wpseo_posts_per_page' === $option && ( $value > 0 && $value < 1000 ) ) {
  169. return $value;
  170. }
  171. return $status;
  172. }
  173. /**
  174. * Adds links to Premium Support and FAQ under the plugin in the plugin overview page.
  175. *
  176. * @staticvar string $this_plugin Holds the directory & filename for the plugin.
  177. *
  178. * @param array $links Array of links for the plugins, adapted when the current plugin is found.
  179. * @param string $file The filename for the current plugin, which the filter loops through.
  180. *
  181. * @return array $links
  182. */
  183. public function add_action_link( $links, $file ) {
  184. if ( WPSEO_BASENAME === $file && WPSEO_Capability_Utils::current_user_can( 'wpseo_manage_options' ) ) {
  185. $settings_link = '<a href="' . esc_url( admin_url( 'admin.php?page=' . self::PAGE_IDENTIFIER ) ) . '">' . __( 'Settings', 'wordpress-seo' ) . '</a>';
  186. array_unshift( $links, $settings_link );
  187. }
  188. $addon_manager = new WPSEO_Addon_Manager();
  189. if ( WPSEO_Utils::is_yoast_seo_premium() && $addon_manager->has_valid_subscription( WPSEO_Addon_Manager::PREMIUM_SLUG ) ) {
  190. return $links;
  191. }
  192. // Add link to premium support landing page.
  193. $premium_link = '<a style="font-weight: bold;" href="' . esc_url( WPSEO_Shortlinker::get( 'https://yoa.st/1yb' ) ) . '" target="_blank">' . __( 'Premium Support', 'wordpress-seo' ) . '</a>';
  194. array_unshift( $links, $premium_link );
  195. // Add link to docs.
  196. $faq_link = '<a href="' . esc_url( WPSEO_Shortlinker::get( 'https://yoa.st/1yc' ) ) . '" target="_blank">' . __( 'FAQ', 'wordpress-seo' ) . '</a>';
  197. array_unshift( $links, $faq_link );
  198. return $links;
  199. }
  200. /**
  201. * Enqueues the (tiny) global JS needed for the plugin.
  202. */
  203. public function config_page_scripts() {
  204. $asset_manager = new WPSEO_Admin_Asset_Manager();
  205. $asset_manager->enqueue_script( 'admin-global-script' );
  206. wp_localize_script( WPSEO_Admin_Asset_Manager::PREFIX . 'admin-global-script', 'wpseoAdminGlobalL10n', $this->localize_admin_global_script() );
  207. }
  208. /**
  209. * Enqueues the (tiny) global stylesheet needed for the plugin.
  210. */
  211. public function enqueue_global_style() {
  212. $asset_manager = new WPSEO_Admin_Asset_Manager();
  213. $asset_manager->enqueue_style( 'admin-global' );
  214. }
  215. /**
  216. * Filter the $contactmethods array and add a set of social profiles.
  217. *
  218. * These are used with the Facebook author, rel="author" and Twitter cards implementation.
  219. *
  220. * @link https://developers.google.com/search/docs/data-types/social-profile
  221. *
  222. * @param array $contactmethods Currently set contactmethods.
  223. *
  224. * @return array $contactmethods with added contactmethods.
  225. */
  226. public function update_contactmethods( $contactmethods ) {
  227. $contactmethods['facebook'] = __( 'Facebook profile URL', 'wordpress-seo' );
  228. $contactmethods['instagram'] = __( 'Instagram profile URL', 'wordpress-seo' );
  229. $contactmethods['linkedin'] = __( 'LinkedIn profile URL', 'wordpress-seo' );
  230. $contactmethods['myspace'] = __( 'MySpace profile URL', 'wordpress-seo' );
  231. $contactmethods['pinterest'] = __( 'Pinterest profile URL', 'wordpress-seo' );
  232. $contactmethods['soundcloud'] = __( 'SoundCloud profile URL', 'wordpress-seo' );
  233. $contactmethods['tumblr'] = __( 'Tumblr profile URL', 'wordpress-seo' );
  234. $contactmethods['twitter'] = __( 'Twitter username (without @)', 'wordpress-seo' );
  235. $contactmethods['youtube'] = __( 'YouTube profile URL', 'wordpress-seo' );
  236. $contactmethods['wikipedia'] = __( 'Wikipedia page about you', 'wordpress-seo' ) . '<br/><small>' . __( '(if one exists)', 'wordpress-seo' ) . '</small>';
  237. return $contactmethods;
  238. }
  239. /**
  240. * Log the updated timestamp for user profiles when theme is changed.
  241. */
  242. public function switch_theme() {
  243. $users = get_users( [ 'who' => 'authors' ] );
  244. if ( is_array( $users ) && $users !== [] ) {
  245. foreach ( $users as $user ) {
  246. update_user_meta( $user->ID, '_yoast_wpseo_profile_updated', time() );
  247. }
  248. }
  249. }
  250. /**
  251. * Localization for the dismiss urls.
  252. *
  253. * @return array
  254. */
  255. private function localize_admin_global_script() {
  256. return [
  257. /* translators: %1$s: '%%term_title%%' variable used in titles and meta's template that's not compatible with the given template, %2$s: expands to 'HelpScout beacon' */
  258. 'variable_warning' => sprintf(
  259. __( 'Warning: the variable %1$s cannot be used in this template. See the %2$s for more info.', 'wordpress-seo' ),
  260. '<code>%s</code>',
  261. 'HelpScout beacon'
  262. ),
  263. 'dismiss_about_url' => $this->get_dismiss_url( 'wpseo-dismiss-about' ),
  264. 'dismiss_tagline_url' => $this->get_dismiss_url( 'wpseo-dismiss-tagline-notice' ),
  265. /* translators: %s: expends to Yoast SEO */
  266. 'help_video_iframe_title' => sprintf( __( '%s video tutorial', 'wordpress-seo' ), 'Yoast SEO' ),
  267. 'scrollable_table_hint' => __( 'Scroll to see the table content.', 'wordpress-seo' ),
  268. ];
  269. }
  270. /**
  271. * Extending the current page URL with two params to be able to ignore the notice.
  272. *
  273. * @param string $dismiss_param The param used to dismiss the notification.
  274. *
  275. * @return string
  276. */
  277. private function get_dismiss_url( $dismiss_param ) {
  278. $arr_params = [
  279. $dismiss_param => '1',
  280. 'nonce' => wp_create_nonce( $dismiss_param ),
  281. ];
  282. return esc_url( add_query_arg( $arr_params ) );
  283. }
  284. /**
  285. * Sets the upsell notice.
  286. */
  287. protected function set_upsell_notice() {
  288. $upsell = new WPSEO_Product_Upsell_Notice();
  289. $upsell->dismiss_notice_listener();
  290. $upsell->initialize();
  291. }
  292. /**
  293. * Whether we are on the admin dashboard page.
  294. *
  295. * @returns bool
  296. */
  297. protected function on_dashboard_page() {
  298. return 'index.php' === $GLOBALS['pagenow'];
  299. }
  300. /**
  301. * Loads the cornerstone filter.
  302. *
  303. * @return WPSEO_WordPress_Integration[] The integrations to initialize.
  304. */
  305. protected function initialize_cornerstone_content() {
  306. if ( ! WPSEO_Options::get( 'enable_cornerstone_content' ) ) {
  307. return [];
  308. }
  309. return [
  310. 'cornerstone_filter' => new WPSEO_Cornerstone_Filter(),
  311. ];
  312. }
  313. /**
  314. * Initializes the seo link watcher.
  315. *
  316. * @returns WPSEO_WordPress_Integration[]
  317. */
  318. protected function initialize_seo_links() {
  319. $integrations = [];
  320. $link_table_compatibility_notifier = new WPSEO_Link_Compatibility_Notifier();
  321. $link_table_accessible_notifier = new WPSEO_Link_Table_Accessible_Notifier();
  322. if ( ! WPSEO_Options::get( 'enable_text_link_counter' ) ) {
  323. $link_table_compatibility_notifier->remove_notification();
  324. return $integrations;
  325. }
  326. $integrations[] = new WPSEO_Link_Cleanup_Transient();
  327. // Only use the link module for PHP 5.3 and higher and show a notice when version is wrong.
  328. if ( version_compare( phpversion(), '5.3', '<' ) ) {
  329. $link_table_compatibility_notifier->add_notification();
  330. return $integrations;
  331. }
  332. $link_table_compatibility_notifier->remove_notification();
  333. // When the table doesn't exists, just add the notification and return early.
  334. if ( ! WPSEO_Link_Table_Accessible::is_accessible() ) {
  335. WPSEO_Link_Table_Accessible::cleanup();
  336. }
  337. if ( ! WPSEO_Meta_Table_Accessible::is_accessible() ) {
  338. WPSEO_Meta_Table_Accessible::cleanup();
  339. }
  340. if ( ! WPSEO_Link_Table_Accessible::is_accessible() || ! WPSEO_Meta_Table_Accessible::is_accessible() ) {
  341. $link_table_accessible_notifier->add_notification();
  342. return $integrations;
  343. }
  344. $link_table_accessible_notifier->remove_notification();
  345. $integrations[] = new WPSEO_Link_Columns( new WPSEO_Meta_Storage() );
  346. $integrations[] = new WPSEO_Link_Reindex_Dashboard();
  347. $integrations[] = new WPSEO_Link_Notifier();
  348. // Adds a filter to exclude the attachments from the link count.
  349. add_filter( 'wpseo_link_count_post_types', [ 'WPSEO_Post_Type', 'filter_attachment_post_type' ] );
  350. return $integrations;
  351. }
  352. /**
  353. * Retrieves an instance of the HelpScout beacon class for Yoast SEO.
  354. *
  355. * @return WPSEO_HelpScout The instance of the HelpScout beacon.
  356. */
  357. private function get_helpscout_beacon() {
  358. $helpscout_settings = [
  359. 'beacon_id' => '2496aba6-0292-489c-8f5d-1c0fba417c2f',
  360. 'pages' => [
  361. 'wpseo_dashboard',
  362. 'wpseo_titles',
  363. 'wpseo_search_console',
  364. 'wpseo_social',
  365. 'wpseo_tools',
  366. 'wpseo_licenses',
  367. ],
  368. 'products' => [],
  369. 'ask_consent' => true,
  370. ];
  371. /**
  372. * Filter: 'wpseo_helpscout_beacon_settings' - Allows overriding the HelpScout beacon settings.
  373. *
  374. * @api string - The helpscout beacons settings.
  375. */
  376. $helpscout_settings = apply_filters( 'wpseo_helpscout_beacon_settings', $helpscout_settings );
  377. return new WPSEO_HelpScout(
  378. $helpscout_settings['beacon_id'],
  379. $helpscout_settings['pages'],
  380. $helpscout_settings['products'],
  381. $helpscout_settings['ask_consent']
  382. );
  383. }
  384. /* ********************* DEPRECATED METHODS ********************* */
  385. /**
  386. * Cleans stopwords out of the slug, if the slug hasn't been set yet.
  387. *
  388. * @deprecated 7.0
  389. * @codeCoverageIgnore
  390. *
  391. * @return void
  392. */
  393. public function remove_stopwords_from_slug() {
  394. _deprecated_function( __METHOD__, 'WPSEO 7.0' );
  395. }
  396. /**
  397. * Filter the stopwords from the slug.
  398. *
  399. * @deprecated 7.0
  400. * @codeCoverageIgnore
  401. *
  402. * @return void
  403. */
  404. public function filter_stopwords_from_slug() {
  405. _deprecated_function( __METHOD__, 'WPSEO 7.0' );
  406. }
  407. /**
  408. * Initializes WHIP to show a notice for outdated PHP versions.
  409. *
  410. * @deprecated 8.1
  411. * @codeCoverageIgnore
  412. *
  413. * @return void
  414. */
  415. public function check_php_version() {
  416. // Intentionally left empty.
  417. }
  418. }