diff --git a/php/class-wp-seo-settings.php b/php/class-wp-seo-settings.php index 8bccc75..d23a2d0 100644 --- a/php/class-wp-seo-settings.php +++ b/php/class-wp-seo-settings.php @@ -37,6 +37,13 @@ class WP_SEO_Settings { */ public $options = array(); + /** + * Storage unit for the current network option values of the plugin. + * + * @var array. + */ + public $network_options = array(); + /** * Taxonomies with archive pages, which can have meta fields set for them. * @@ -66,6 +73,8 @@ class WP_SEO_Settings { const SLUG = 'wp-seo'; + const NETWORK_SLUG = 'wp-seo-network'; + /** * Unused. * @@ -118,6 +127,8 @@ protected function setup() { add_action( 'admin_menu', array( $this, 'add_options_page' ) ); add_action( 'admin_init', array( $this, 'register_settings' ) ); add_action( 'load-settings_page_' . $this::SLUG, array( $this, 'add_help_tab' ) ); + add_action( 'network_admin_menu', array( $this, 'add_network_options_page' ) ); + add_action( 'network_admin_edit_wp-seo-network', array( $this, 'save_network_settings' ) ); } } @@ -185,6 +196,27 @@ public function get_option( $key, $default = null ) { return isset( $this->options[ $key ] ) ? $this->options[ $key ] : $default; } + /** + * Set $network_options with the current network database value. + */ + public function set_network_options() { + $this->network_options = get_site_option( $this::NETWORK_SLUG, array() ); + } + + /** + * Get a network option value. + * + * @param string $key The option key sought. + * @param mixed $default Optional default. + * @return mixed The value, or null on failure. + */ + public function get_network_option( $key, $default = null ) { + if ( empty( $this->network_options ) ) { + $this->set_network_options(); + } + return isset( $this->network_options[ $key ] ) ? $this->network_options[ $key ] : $default; + } + /** * Get the $taxonomies property. * @@ -257,6 +289,74 @@ public function add_options_page() { add_options_page( __( 'WP SEO Settings', 'wp-seo' ), __( 'SEO', 'wp-seo' ), $this->options_capability, $this::SLUG, array( $this, 'view_settings_page' ) ); } + /** + * Register the plugin network options page. + */ + public function add_network_options_page() { + add_submenu_page( 'settings.php', __( 'WP SEO Settings', 'wp-seo' ), __( 'SEO', 'wp-seo' ), 'manage_network_options', $this::SLUG, array( $this, 'view_network_settings_page' ) ); + } + + /** + * Render the network settings page for robots.txt network prefix/suffix fields. + */ + public function view_network_settings_page() { + ?> +
+

+ +

+ +
+ + $this::NETWORK_SLUG ) ) ); + } + do_accordion_sections( $this::NETWORK_SLUG, 'advanced', null ); + } else { + do_settings_sections( $this::NETWORK_SLUG ); + } + ?> + +
+
+ network_options ) ) { + $this->set_network_options(); + } + + $this->network_options['robots_txt_network_prefix'] = isset( $in['robots_txt_network_prefix'] ) && is_string( $in['robots_txt_network_prefix'] ) ? sanitize_text_field( $in['robots_txt_network_prefix'] ) : ''; + $this->network_options['robots_txt_network_suffix'] = isset( $in['robots_txt_network_suffix'] ) && is_string( $in['robots_txt_network_suffix'] ) ? sanitize_text_field( $in['robots_txt_network_suffix'] ) : ''; + + update_site_option( $this::NETWORK_SLUG, $this->network_options ); + + wp_safe_redirect( add_query_arg( array( 'page' => $this::SLUG, 'updated' => 'true' ), network_admin_url( 'settings.php' ) ) ); + exit; + } + /** * Add tabs to the help menu on the plugin options page. */ @@ -328,6 +428,18 @@ public function register_settings() { add_settings_section( 'arbitrary', __( 'Other Meta Tags', 'wp-seo' ), false, $this::SLUG ); add_settings_field( 'arbitrary_tags', __( 'Tags', 'wp-seo' ), array( $this, 'field' ), $this::SLUG, 'arbitrary', array( 'type' => 'repeatable', 'field' => 'arbitrary_tags', 'repeat' => array( 'name' => __( 'Name', 'lin' ), 'content' => __( 'Content', 'lin' ) ) ) ); + + add_settings_section( 'robots', __( 'Robots.txt', 'wp-seo' ), false, $this::SLUG ); + add_settings_field( 'robots_example', __( 'Robots.txt Example', 'wp-seo' ), array( $this, 'example_robots_txt' ), $this::SLUG, 'robots' ); + add_settings_field( 'robots_txt_prefix', __( 'Add to start of Robots.txt', 'wp-seo' ), array( $this, 'field' ), $this::SLUG, 'robots', array( 'type' => 'textarea', 'field' => 'robots_txt_prefix' ) ); + add_settings_field( 'robots_txt_suffix', __( 'Add to end of Robots.txt', 'wp-seo' ), array( $this, 'field' ), $this::SLUG, 'robots', array( 'type' => 'textarea', 'field' => 'robots_txt_suffix' ) ); + + if ( is_network_admin() ) { + add_settings_section( 'robots', __( 'Robots.txt', 'wp-seo' ), false, $this::NETWORK_SLUG ); + add_settings_field( 'robots_example', __( 'Robots.txt Example', 'wp-seo' ), array( $this, 'example_robots_txt' ), $this::NETWORK_SLUG, 'robots' ); + add_settings_field( 'robots_txt_network_prefix', __( 'Add to start of Robots.txt (Network)', 'wp-seo' ), array( $this, 'field_network' ), $this::NETWORK_SLUG, 'robots', array( 'type' => 'textarea', 'field' => 'robots_txt_network_prefix' ) ); + add_settings_field( 'robots_txt_network_suffix', __( 'Add to end of Robots.txt (Network)', 'wp-seo' ), array( $this, 'field_network' ), $this::NETWORK_SLUG, 'robots', array( 'type' => 'textarea', 'field' => 'robots_txt_network_suffix' ) ); + } } /** @@ -358,6 +470,27 @@ public function example_url( $text, $url = false ) { echo '

'; } + /** + * Display the compiled Robots.txt contents in an uneditable field. + */ + public function example_robots_txt() { + ob_start(); + /* Error suppression is used here because `do_robots` calls `header` which + * throws a warning due to headers already having been sent. There is no + * way around this, without recreating the function and maintaining our + * own version of it entirely. At least as of WordPress 6.9.4. + */ + @do_robots(); + $robots = ob_get_clean(); + $this->render_textarea([ + 'field' => 'robots_txt', + 'disabled' => true, + 'rows' => 10, + ], + $robots + ); + } + /** * Display an example URL for individual posts. * @@ -448,6 +581,34 @@ public function example_404_page() { * } */ public function field( $args ) { + $this->render_field( $args, array( $this, 'get_option' ) ); + } + + /** + * Display a network settings field. + * + * @param array $args Field arguments. @see render_field(). + */ + public function field_network( $args ) { + $this->render_field( $args, array( $this, 'get_network_option' ) ); + } + + /** + * Render a settings field using the provided getter callable. + * + * Shared implementation for field() and field_network(). Callers pass the + * appropriate getter (get_option or get_network_option) so lazy-loading and + * value lookup are handled by the getter regardless of storage source. + * + * @param array $args { + * Field arguments. + * + * @type string $field The option key to pass to $getter. + * @type string $type Optional field type. Defaults to 'text'. + * } + * @param callable $getter Callable that accepts ( $key, $default ) and returns the value. + */ + protected function render_field( $args, callable $getter ) { if ( empty( $args['field'] ) ) { return; } @@ -456,7 +617,7 @@ public function field( $args ) { $args['type'] = 'text'; } - $value = ! empty( $this->options[ $args['field'] ] ) ? $this->options[ $args['field'] ] : ''; + $value = call_user_func( $getter, $args['field'], '' ); switch ( $args['type'] ) { case 'textarea' : @@ -524,11 +685,12 @@ public function render_textarea( $args, $value ) { ) ); printf( - '', + '', esc_attr( $this::SLUG ), esc_attr( $args['field'] ), esc_attr( $args['rows'] ), esc_attr( $args['cols'] ), + ( ! empty( $args['disabled'] ) ? ' disabled' : '' ), esc_textarea( $value ) ); } @@ -682,9 +844,9 @@ public function view_settings_page() { if ( apply_filters( 'wp_seo_use_settings_accordions', true ) ) { global $wp_settings_sections; foreach ( (array) $wp_settings_sections[ $this::SLUG ] as $section ) { - add_meta_box( $section['id'], $section['title'], array( $this, 'settings_meta_box' ), 'wp-seo', 'advanced', 'default', $section ); + add_meta_box( $section['id'], $section['title'], array( $this, 'settings_meta_box' ), $this::SLUG, 'advanced', 'default', array_merge( $section, array( 'slug' => $this::SLUG ) ) ); } - do_accordion_sections( 'wp-seo', 'advanced', null ); + do_accordion_sections( $this::SLUG, 'advanced', null ); } else { do_settings_sections( $this::SLUG ); } @@ -698,23 +860,27 @@ public function view_settings_page() { /** * Render a section's fields as a meta box. * - * @param mixed $object Unused. Data passed from do_accordion_sections(). - * @param array $box { + * @param mixed $_object Unused. Data passed from do_accordion_sections(). + * @param array $box { * An array of meta box arguments. * - * @type string $id @see add_meta_box(). - * @type string $title @see add_meta_box(). - * @type callback $callback @see add_meta_box(). - * @type array $args @see add_meta_box(), add_settings_section(). + * @type string $id @see add_meta_box(). + * @type string $title @see add_meta_box(). + * @type callback $callback @see add_meta_box(). + * @type array $args @see add_meta_box(), add_settings_section(). + * @type string $slug The settings page slug to pass to do_settings_fields(). + * Defaults to SLUG. * } */ - public function settings_meta_box( $object, $box ) { + public function settings_meta_box( $_object, $box ) { if ( is_callable( $box['args']['callback'] ) ) { call_user_func( $box['args']['callback'], $box['args'] ); } + $slug = isset( $box['args']['slug'] ) ? $box['args']['slug'] : $this::SLUG; + echo ''; - do_settings_fields( $this::SLUG, $box['args']['id'] ); + do_settings_fields( $slug, $box['args']['id'] ); echo '
'; } @@ -760,6 +926,10 @@ public function sanitize_options( $in ) { $sanitize_as_text_field[] = 'search_title'; $sanitize_as_text_field[] = '404_title'; + // Robots.txt fields. + $sanitize_as_text_field[] = 'robots_txt_prefix'; + $sanitize_as_text_field[] = 'robots_txt_suffix'; + foreach ( $sanitize_as_text_field as $field ) { $out[ $field ] = isset( $in[ $field ] ) && is_string( $in[ $field ] ) ? sanitize_text_field( $in[ $field ] ) : null; } diff --git a/php/class-wp-seo.php b/php/class-wp-seo.php index 1231f56..52f77fb 100644 --- a/php/class-wp-seo.php +++ b/php/class-wp-seo.php @@ -90,6 +90,7 @@ protected function setup() { add_filter( 'pre_get_document_title', array( $this, 'pre_get_document_title' ), 20 ); add_filter( 'wp_title', array( $this, 'wp_title' ), 20, 2 ); add_filter( 'wp_head', array( $this, 'wp_head' ), 5 ); + add_filter( 'robots_txt', array( $this, 'robots_txt' ) ); } /** @@ -616,6 +617,55 @@ public function wp_head() { } } + + /** + * Build the prefix and suffix for the Robots.txt file, and return it. + * + * @param string $robots The robots.txt file contents. + * @return string + */ + public function robots_txt( string $robots ): string { + /** + * Filters the network-level Robots.txt Prefix value for WP SEO. + * + * @param string $prefix The robots.txt network prefix, added in the WP SEO network settings page. + */ + $robots_network_prefix = apply_filters( 'wp_seo_robots_txt_network_prefix', WP_SEO_Settings()->get_network_option( 'robots_txt_network_prefix', '' ) ); + + /** + * Filters the network-level Robots.txt Suffix value for WP SEO. + * + * @param string $suffix The robots.txt network suffix, added in the WP SEO network settings page. + */ + $robots_network_suffix = apply_filters( 'wp_seo_robots_txt_network_suffix', WP_SEO_Settings()->get_network_option( 'robots_txt_network_suffix', '' ) ); + + /** + * Filters the Robots.txt Prefix value for WP SEO. + * + * @param string $prefix The robots.txt prefix, added in the WP SEO settings page. + */ + $robots_prefix = apply_filters( 'wp_seo_robots_txt_prefix', WP_SEO_Settings()->get_option( 'robots_txt_prefix', '' ) ); + + /** + * Filters the Robots.txt Suffix value for WP SEO. + * + * @param string $suffix The robots.txt suffix, added in the WP SEO settings page. + */ + $robots_suffix = apply_filters( 'wp_seo_robots_txt_suffix', WP_SEO_Settings()->get_option( 'robots_txt_suffix', '' ) ); + + return implode( + PHP_EOL, + array_filter( + array( + $robots_network_prefix, + $robots_prefix, + $robots, + $robots_suffix, + $robots_network_suffix + ) + ) + ); + } } /**