: str_replace(): Passing null to parameter #2 ($replace) of type array|string is deprecated in
* @package WPSEO\Internals\Options
use Yoast\WP\SEO\Config\Schema_Types;
class WPSEO_Option_Titles extends WPSEO_Option {
public $option_name = 'wpseo_titles';
* Array of defaults for the option.
* Shouldn't be requested directly, use $this->get_defaults();
* {@internal Note: Some of the default values are added via the translate_defaults() method.}}
'forcerewritetitle' => false,
'separator' => 'sc-dash',
'title-home-wpseo' => '%%sitename%% %%page%% %%sep%% %%sitedesc%%', // Text field.
'title-author-wpseo' => '', // Text field.
'title-archive-wpseo' => '%%date%% %%page%% %%sep%% %%sitename%%', // Text field.
'title-search-wpseo' => '', // Text field.
'title-404-wpseo' => '', // Text field.
'social-title-author-wpseo' => '%%name%%', // Text field.
'social-title-archive-wpseo' => '%%date%%', // Text field.
'social-description-author-wpseo' => '', // Text area.
'social-description-archive-wpseo' => '', // Text area.
'social-image-url-author-wpseo' => '', // Hidden input field.
'social-image-url-archive-wpseo' => '', // Hidden input field.
'social-image-id-author-wpseo' => 0, // Hidden input field.
'social-image-id-archive-wpseo' => 0, // Hidden input field.
'metadesc-home-wpseo' => '', // Text area.
'metadesc-author-wpseo' => '', // Text area.
'metadesc-archive-wpseo' => '', // Text area.
'rssbefore' => '', // Text area.
'rssafter' => '', // Text area.
'noindex-author-wpseo' => false,
'noindex-author-noposts-wpseo' => true,
'noindex-archive-wpseo' => true,
'disable-author' => false,
'disable-post_format' => false,
'disable-attachment' => true,
'breadcrumbs-404crumb' => '', // Text field.
'breadcrumbs-display-blog-page' => true,
'breadcrumbs-boldlast' => false,
'breadcrumbs-archiveprefix' => '', // Text field.
'breadcrumbs-enable' => true,
'breadcrumbs-home' => '', // Text field.
'breadcrumbs-prefix' => '', // Text field.
'breadcrumbs-searchprefix' => '', // Text field.
'breadcrumbs-sep' => 'ยป', // Text field.
'alternate_website_name' => '',
'company_logo_meta' => false,
'person_logo_meta' => false,
'company_alternate_name' => '',
'company_or_person' => 'company',
'company_or_person_user_id' => false,
'stripcategorybase' => false,
'open_graph_frontpage_title' => '%%sitename%%', // Text field.
'open_graph_frontpage_desc' => '', // Text field.
'open_graph_frontpage_image' => '', // Text field.
'open_graph_frontpage_image_id' => 0,
'publishing_principles_id' => 0,
'ownership_funding_info_id' => 0,
'actionable_feedback_policy_id' => 0,
'corrections_policy_id' => 0,
'diversity_policy_id' => 0,
'diversity_staffing_report_id' => 0,
'org-founding-date' => '',
'org-number-employees' => '',
* Uses enrich_defaults to add more along the lines of:
* - 'title-' . $pt->name => ''; // Text field.
* - 'metadesc-' . $pt->name => ''; // Text field.
* - 'noindex-' . $pt->name => false;
* - 'display-metabox-pt-' . $pt->name => false;
* - 'title-ptarchive-' . $pt->name => ''; // Text field.
* - 'metadesc-ptarchive-' . $pt->name => ''; // Text field.
* - 'bctitle-ptarchive-' . $pt->name => ''; // Text field.
* - 'noindex-ptarchive-' . $pt->name => false;
* - 'title-tax-' . $tax->name => '''; // Text field.
* - 'metadesc-tax-' . $tax->name => ''; // Text field.
* - 'noindex-tax-' . $tax->name => false;
* - 'display-metabox-tax-' . $tax->name => false;
* - 'schema-page-type-' . $pt->name => 'WebPage';
* - 'schema-article-type-' . $pt->name => 'Article';
* Used for "caching" during pageload.
protected $enriched_defaults = null;
* Array of variable option name patterns for the option.
protected $variable_array_key_patterns = [
* Array of sub-options which should not be overloaded with multi-site defaults.
* Add the actions and filters for the option.
* @todo [JRF => testers] Check if the extra actions below would run into problems if an option
* is updated early on and if so, change the call to schedule these for a later action on add/update
* instead of running them straight away.
protected function __construct() {
add_action( 'update_option_' . $this->option_name, [ 'WPSEO_Utils', 'clear_cache' ] );
add_action( 'init', [ $this, 'end_of_init' ], 999 );
add_action( 'registered_post_type', [ $this, 'invalidate_enrich_defaults_cache' ] );
add_action( 'unregistered_post_type', [ $this, 'invalidate_enrich_defaults_cache' ] );
add_action( 'registered_taxonomy', [ $this, 'invalidate_enrich_defaults_cache' ] );
add_action( 'unregistered_taxonomy', [ $this, 'invalidate_enrich_defaults_cache' ] );
add_filter( 'admin_title', [ 'Yoast_Input_Validation', 'add_yoast_admin_document_title_errors' ] );
* Make sure we can recognize the right action for the double cleaning.
public function end_of_init() {
do_action( 'wpseo_double_clean_titles' );
* Get the singleton instance of this class.
public static function get_instance() {
if ( ! ( self::$instance instanceof self ) ) {
self::$instance = new self();
* Get the available separator options.
public function get_separator_options() {
$separators = wp_list_pluck( self::get_separator_option_list(), 'option' );
* Allow altering the array with separator options.
* @param array $separator_options Array with the separator options.
$filtered_separators = apply_filters( 'wpseo_separator_options', $separators );
if ( is_array( $filtered_separators ) && $filtered_separators !== [] ) {
$separators = array_merge( $separators, $filtered_separators );
* Get the available separator options aria-labels.
* @return string[] Array with the separator options aria-labels.
public function get_separator_options_for_display() {
$separators = $this->get_separator_options();
$separator_list = self::get_separator_option_list();
foreach ( $separators as $key => $label ) {
$aria_label = ( $separator_list[ $key ]['label'] ?? '' );
$separator_options[ $key ] = [
'aria_label' => $aria_label,
return $separator_options;
* Translate strings used in the option defaults.
public function translate_defaults() {
/* translators: 1: Author name; 2: Site name. */
$this->defaults['title-author-wpseo'] = sprintf( __( '%1$s, Author at %2$s', 'wordpress-seo' ), '%%name%%', '%%sitename%%' ) . ' %%page%% ';
/* translators: %s expands to the search phrase. */
$this->defaults['title-search-wpseo'] = sprintf( __( 'You searched for %s', 'wordpress-seo' ), '%%searchphrase%%' ) . ' %%page%% %%sep%% %%sitename%%';
$this->defaults['title-404-wpseo'] = __( 'Page not found', 'wordpress-seo' ) . ' %%sep%% %%sitename%%';
/* translators: 1: link to post; 2: link to blog. */
$this->defaults['rssafter'] = sprintf( __( 'The post %1$s appeared first on %2$s.', 'wordpress-seo' ), '%%POSTLINK%%', '%%BLOGLINK%%' );
$this->defaults['breadcrumbs-404crumb'] = __( 'Error 404: Page not found', 'wordpress-seo' );
$this->defaults['breadcrumbs-archiveprefix'] = __( 'Archives for', 'wordpress-seo' );
$this->defaults['breadcrumbs-home'] = __( 'Home', 'wordpress-seo' );
$this->defaults['breadcrumbs-searchprefix'] = __( 'You searched for', 'wordpress-seo' );
* Add dynamically created default options based on available post types and taxonomies.
public function enrich_defaults() {
$enriched_defaults = $this->enriched_defaults;
if ( $enriched_defaults !== null ) {
$this->defaults += $enriched_defaults;
* Retrieve all the relevant post type and taxonomy arrays.
* WPSEO_Post_Type::get_accessible_post_types() should *not* be used here.
* These are the defaults and can be prepared for any public post type.
$post_type_objects = get_post_types( [ 'public' => true ], 'objects' );
if ( $post_type_objects ) {
/* translators: %s expands to the name of a post type (plural). */
$archive = sprintf( __( '%s Archive', 'wordpress-seo' ), '%%pt_plural%%' );
foreach ( $post_type_objects as $pt ) {
$enriched_defaults[ 'title-' . $pt->name ] = '%%title%% %%page%% %%sep%% %%sitename%%'; // Text field.
$enriched_defaults[ 'metadesc-' . $pt->name ] = ''; // Text area.
$enriched_defaults[ 'noindex-' . $pt->name ] = false;
$enriched_defaults[ 'display-metabox-pt-' . $pt->name ] = true;
$enriched_defaults[ 'post_types-' . $pt->name . '-maintax' ] = 0; // Select box.
$enriched_defaults[ 'schema-page-type-' . $pt->name ] = 'WebPage';
$enriched_defaults[ 'schema-article-type-' . $pt->name ] = ( $pt->name === 'post' ) ? 'Article' : 'None';
if ( $pt->name !== 'attachment' ) {
$enriched_defaults[ 'social-title-' . $pt->name ] = '%%title%%'; // Text field.
$enriched_defaults[ 'social-description-' . $pt->name ] = ''; // Text area.
$enriched_defaults[ 'social-image-url-' . $pt->name ] = ''; // Hidden input field.
$enriched_defaults[ 'social-image-id-' . $pt->name ] = 0; // Hidden input field.
// Custom post types that have archives.
if ( ! $pt->_builtin && WPSEO_Post_Type::has_archive( $pt ) ) {
$enriched_defaults[ 'title-ptarchive-' . $pt->name ] = $archive . ' %%page%% %%sep%% %%sitename%%'; // Text field.
$enriched_defaults[ 'metadesc-ptarchive-' . $pt->name ] = ''; // Text area.
$enriched_defaults[ 'bctitle-ptarchive-' . $pt->name ] = ''; // Text field.
$enriched_defaults[ 'noindex-ptarchive-' . $pt->name ] = false;
$enriched_defaults[ 'social-title-ptarchive-' . $pt->name ] = $archive; // Text field.
$enriched_defaults[ 'social-description-ptarchive-' . $pt->name ] = ''; // Text area.
$enriched_defaults[ 'social-image-url-ptarchive-' . $pt->name ] = ''; // Hidden input field.
$enriched_defaults[ 'social-image-id-ptarchive-' . $pt->name ] = 0; // Hidden input field.
$taxonomy_objects = get_taxonomies( [ 'public' => true ], 'object' );
if ( $taxonomy_objects ) {
/* translators: %s expands to the variable used for term title. */
$archives = sprintf( __( '%s Archives', 'wordpress-seo' ), '%%term_title%%' );
foreach ( $taxonomy_objects as $tax ) {
$enriched_defaults[ 'title-tax-' . $tax->name ] = $archives . ' %%page%% %%sep%% %%sitename%%'; // Text field.
$enriched_defaults[ 'metadesc-tax-' . $tax->name ] = ''; // Text area.
$enriched_defaults[ 'display-metabox-tax-' . $tax->name ] = true;
$enriched_defaults[ 'noindex-tax-' . $tax->name ] = ( $tax->name === 'post_format' );
$enriched_defaults[ 'social-title-tax-' . $tax->name ] = $archives; // Text field.
$enriched_defaults[ 'social-description-tax-' . $tax->name ] = ''; // Text area.
$enriched_defaults[ 'social-image-url-tax-' . $tax->name ] = ''; // Hidden input field.
$enriched_defaults[ 'social-image-id-tax-' . $tax->name ] = 0; // Hidden input field.
$enriched_defaults[ 'taxonomy-' . $tax->name . '-ptparent' ] = 0; // Select box;.
$this->enriched_defaults = $enriched_defaults;
$this->defaults += $enriched_defaults;
* Invalidates enrich_defaults() cache.
* - (un)registered_post_type
* - (un)registered_taxonomy
public function invalidate_enrich_defaults_cache() {
$this->enriched_defaults = null;
* @param string[] $dirty New value for the option.
* @param string[] $clean Clean value for the option, normally the defaults.
* @param string[] $old Old value of the option.
* @return string[] Validated clean value for the option to be saved to the database.
protected function validate_option( $dirty, $clean, $old ) {
$allowed_post_types = $this->get_allowed_post_types();
foreach ( $clean as $key => $value ) {
$switch_key = $this->get_switch_key( $key );
// Only ever set programmatically, so no reason for intense validation.
case 'company_logo_meta':
if ( isset( $dirty[ $key ] ) ) {
$clean[ $key ] = $dirty[ $key ];
/* Breadcrumbs text fields. */
case 'breadcrumbs-404crumb':
case 'breadcrumbs-archiveprefix':
case 'breadcrumbs-prefix':
case 'breadcrumbs-searchprefix':
if ( isset( $dirty[ $key ] ) ) {
$clean[ $key ] = wp_kses_post( $dirty[ $key ] );
* 'title-home-wpseo', 'title-author-wpseo', 'title-archive-wpseo', // phpcs:ignore Squiz.PHP.CommentedOutCode.Found -- This isn't commented out code.
* 'title-search-wpseo', 'title-404-wpseo'
* 'title-ptarchive-' . $pt->name
* 'title-tax-' . $tax->name
* 'social-title-' . $pt->name
* 'social-title-ptarchive-' . $pt->name
* 'social-title-tax-' . $tax->name
* 'social-title-author-wpseo', 'social-title-archive-wpseo'
* 'open_graph_frontpage_title'
case 'alternate_website_name':
case 'open_graph_frontpage_title':
if ( isset( $dirty[ $key ] ) ) {
$clean[ $key ] = WPSEO_Utils::sanitize_text_field( $dirty[ $key ] );
case 'company_or_person':
if ( isset( $dirty[ $key ] ) ) {
if ( in_array( $dirty[ $key ], [ 'company', 'person' ], true ) ) {
$clean[ $key ] = $dirty[ $key ];
$defaults = $this->get_defaults();
$clean[ $key ] = $defaults['company_or_person'];
* 'company_logo', 'person_logo' // phpcs:ignore Squiz.PHP.CommentedOutCode.Found -- This isn't commented out code.
case 'open_graph_frontpage_image':
// When a logo changes, we need to ditch the caches we have for it.
unset( $clean[ $switch_key . '_id' ] );
unset( $clean[ $switch_key . '_meta' ] );
$this->validate_url( $key, $dirty, $old, $clean );
* 'social-image-url-' . $pt->name
* 'social-image-url-ptarchive-' . $pt->name
* 'social-image-url-tax-' . $tax->name
* 'social-image-url-author-wpseo', 'social-image-url-archive-wpseo'
case 'social-image-url-':
$this->validate_url( $key, $dirty, $old, $clean );
* 'metadesc-home-wpseo', 'metadesc-author-wpseo', 'metadesc-archive-wpseo'
* 'metadesc-' . $pt->name
* 'metadesc-ptarchive-' . $pt->name
* 'metadesc-tax-' . $tax->name
* 'bctitle-ptarchive-' . $pt->name
* 'social-description-' . $pt->name
* 'social-description-ptarchive-' . $pt->name
* 'social-description-tax-' . $tax->name
* 'social-description-author-wpseo', 'social-description-archive-wpseo'
* 'open_graph_frontpage_desc'
case 'bctitle-ptarchive-':
case 'company_alternate_name':
case 'social-description-':
case 'open_graph_frontpage_desc':