: str_replace(): Passing null to parameter #2 ($replace) of type array|string is deprecated in
$wpseo_social = get_option( 'wpseo_social' );
$wpseo_titles = get_option( 'wpseo_titles' );
// Reset to the correct default value.
$copied_options['open_graph_frontpage_title'] = '%%sitename%%';
'og_frontpage_title' => 'open_graph_frontpage_title',
'og_frontpage_desc' => 'open_graph_frontpage_desc',
'og_frontpage_image' => 'open_graph_frontpage_image',
'og_frontpage_image_id' => 'open_graph_frontpage_image_id',
foreach ( $options as $social_option => $titles_option ) {
if ( ! empty( $wpseo_social[ $social_option ] ) ) {
$copied_options[ $titles_option ] = $wpseo_social[ $social_option ];
$wpseo_titles = array_merge( $wpseo_titles, $copied_options );
update_option( 'wpseo_titles', $wpseo_titles );
* Reset the social options with the correct default values.
public function reset_og_settings_to_default_values() {
$wpseo_titles = get_option( 'wpseo_titles' );
$updated_options['social-title-author-wpseo'] = '%%name%%';
$updated_options['social-title-archive-wpseo'] = '%%date%%';
/* translators: %s expands to the name of a post type (plural). */
$post_type_archive_default = sprintf( __( '%s Archive', 'wordpress-seo' ), '%%pt_plural%%' );
/* translators: %s expands to the variable used for term title. */
$term_archive_default = sprintf( __( '%s Archives', 'wordpress-seo' ), '%%term_title%%' );
$post_type_objects = get_post_types( [ 'public' => true ], 'objects' );
if ( $post_type_objects ) {
foreach ( $post_type_objects as $pt ) {
if ( isset( $wpseo_titles[ 'social-title-' . $pt->name ] ) ) {
$updated_options[ 'social-title-' . $pt->name ] = '%%title%%';
if ( isset( $wpseo_titles[ 'social-title-ptarchive-' . $pt->name ] ) ) {
$updated_options[ 'social-title-ptarchive-' . $pt->name ] = $post_type_archive_default;
$taxonomy_objects = get_taxonomies( [ 'public' => true ], 'object' );
if ( $taxonomy_objects ) {
foreach ( $taxonomy_objects as $tax ) {
if ( isset( $wpseo_titles[ 'social-title-tax-' . $tax->name ] ) ) {
$updated_options[ 'social-title-tax-' . $tax->name ] = $term_archive_default;
$wpseo_titles = array_merge( $wpseo_titles, $updated_options );
update_option( 'wpseo_titles', $wpseo_titles );
* Removes all indexables for posts that are not publicly viewable.
* This method should be called after init, because post_types can still be registered.
public function remove_indexable_rows_for_non_public_post_types() {
// If migrations haven't been completed successfully the following may give false errors. So suppress them.
$show_errors = $wpdb->show_errors;
$wpdb->show_errors = false;
$indexable_table = Model::get_table_name( 'Indexable' );
$included_post_types = YoastSEO()->helpers->post_type->get_indexable_post_types();
if ( empty( $included_post_types ) ) {
// phpcs:disable WordPress.DB.DirectDatabaseQuery.NoCaching -- Reason: No relevant caches.
// phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery -- Reason: Most performant way.
[ $indexable_table, 'object_type', 'object_sub_type' ]
// phpcs:disable WordPress.DB.DirectDatabaseQuery.NoCaching -- Reason: No relevant caches.
// phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery -- Reason: Most performant way.
AND %i NOT IN ( " . implode( ', ', array_fill( 0, count( $included_post_types ), '%s' ) ) . ' )',
array_merge( [ $indexable_table, 'object_type', 'object_sub_type', 'object_sub_type' ], $included_post_types )
$wpdb->show_errors = $show_errors;
* Removes all indexables for terms that are not publicly viewable.
* This method should be called after init, because taxonomies can still be registered.
public function remove_indexable_rows_for_non_public_taxonomies() {
// If migrations haven't been completed successfully the following may give false errors. So suppress them.
$show_errors = $wpdb->show_errors;
$wpdb->show_errors = false;
$indexable_table = Model::get_table_name( 'Indexable' );
$included_taxonomies = YoastSEO()->helpers->taxonomy->get_indexable_taxonomies();
if ( empty( $included_taxonomies ) ) {
// phpcs:disable WordPress.DB.DirectDatabaseQuery.NoCaching -- Reason: No relevant caches.
// phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery -- Reason: Most performant way.
[ $indexable_table, 'object_type', 'object_sub_type' ]
// phpcs:disable WordPress.DB.DirectDatabaseQuery.NoCaching -- Reason: No relevant caches.
// phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery -- Reason: Most performant way.
AND %i NOT IN ( " . implode( ', ', array_fill( 0, count( $included_taxonomies ), '%s' ) ) . ' )',
array_merge( [ $indexable_table, 'object_type', 'object_sub_type', 'object_sub_type' ], $included_taxonomies )
$wpdb->show_errors = $show_errors;
* De-duplicates indexables that have more than one "unindexed" rows for the same object. Keeps the newest indexable.
protected function deduplicate_unindexed_indexable_rows() {
// If migrations haven't been completed successfully the following may give false errors. So suppress them.
$show_errors = $wpdb->show_errors;
$wpdb->show_errors = false;
// phpcs:disable WordPress.DB.DirectDatabaseQuery.NoCaching -- Reason: No relevant caches.
// phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery -- Reason: Most performant way.
$duplicates = $wpdb->get_results(
post_status = 'unindexed'
AND object_type IN ( 'term', 'post', 'user' )
[ Model::get_table_name( 'Indexable' ) ]
if ( empty( $duplicates ) ) {
$wpdb->show_errors = $show_errors;
// Users, terms and posts may share the same object_id. So delete them in separate, more performant, queries.
$this->get_indexable_deduplication_query_for_type( 'post', $duplicates, $wpdb ),
$this->get_indexable_deduplication_query_for_type( 'term', $duplicates, $wpdb ),
$this->get_indexable_deduplication_query_for_type( 'user', $duplicates, $wpdb ),
foreach ( $delete_queries as $delete_query ) {
if ( ! empty( $delete_query ) ) {
// phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery -- Reason: Most performant way.
// phpcs:disable WordPress.DB.DirectDatabaseQuery.NoCaching -- Reason: No relevant caches.
// phpcs:disable WordPress.DB.PreparedSQL.NotPrepared -- Reason: Is it prepared already.
$wpdb->query( $delete_query );
$wpdb->show_errors = $show_errors;
* Cleans up "unindexed" indexable rows when appropriate, aka when there's no object ID even though it should.
protected function clean_unindexed_indexable_rows_with_no_object_id() {
// If migrations haven't been completed successfully the following may give false errors. So suppress them.
$show_errors = $wpdb->show_errors;
$wpdb->show_errors = false;
// phpcs:disable WordPress.DB.DirectDatabaseQuery.NoCaching -- Reason: No relevant caches.
// phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery -- Reason: Most performant way.
AND %i NOT IN ( 'home-page', 'date-archive', 'post-type-archive', 'system-page' )
[ Model::get_table_name( 'Indexable' ), 'post_status', 'object_type', 'object_id' ]
$wpdb->show_errors = $show_errors;
* Removes all user indexable rows when the author archive is disabled.
protected function remove_indexable_rows_for_disabled_authors_archive() {
if ( ! YoastSEO()->helpers->author_archive->are_disabled() ) {
// If migrations haven't been completed successfully the following may give false errors. So suppress them.
$show_errors = $wpdb->show_errors;
$wpdb->show_errors = false;
// phpcs:disable WordPress.DB.DirectDatabaseQuery.NoCaching -- Reason: No relevant caches.
// phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery -- Reason: Most performant way.
"DELETE FROM %i WHERE %i = 'user'",
[ Model::get_table_name( 'Indexable' ), 'object_type' ]
$wpdb->show_errors = $show_errors;
* Creates a query for de-duplicating indexables for a particular type.
* @param string $object_type The object type to deduplicate.
* @param string|array<array<int,int,string>> $duplicates The result of the duplicate query.
* @param wpdb $wpdb The wpdb object.
* @return string The query that removes all but one duplicate for each object of the object type.
protected function get_indexable_deduplication_query_for_type( $object_type, $duplicates, $wpdb ) {
$filtered_duplicates = array_filter(
static function ( $duplicate ) use ( $object_type ) {
return $duplicate['object_type'] === $object_type;
if ( empty( $filtered_duplicates ) ) {
$object_ids = wp_list_pluck( $filtered_duplicates, 'object_id' );
$newest_indexable_ids = wp_list_pluck( $filtered_duplicates, 'newest_id' );
$replacements = array_merge( [ Model::get_table_name( 'Indexable' ), 'object_id' ], array_values( $object_ids ), array_values( $newest_indexable_ids ) );
$replacements[] = $object_type;
// phpcs:disable WordPress.DB.DirectDatabaseQuery.NoCaching -- Reason: No relevant caches.
// phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery -- Reason: Most performant way.
%i IN ( ' . implode( ', ', array_fill( 0, count( $filtered_duplicates ), '%d' ) ) . ' )
AND id NOT IN ( ' . implode( ', ', array_fill( 0, count( $filtered_duplicates ), '%d' ) ) . ' )
* Removes the settings' introduction modal data for users.
public function delete_user_introduction_meta() {
delete_metadata( 'user', 0, '_yoast_settings_introduction', '', true );