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() {
+ ?>
+
+ 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 '';
}
@@ -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
+ )
+ )
+ );
+ }
}
/**