: str_replace(): Passing null to parameter #2 ($replace) of type array|string is deprecated in
[ , $width, $height ] = \wp_get_attachment_image_src( $model->target_post_id, 'full' );
$model->height = $height;
* Enhances the link model with information from its indexable.
* @param SEO_Links $model The link's model.
* @param string $permalink The link's permalink.
* @return SEO_Links The enhanced link model.
protected function enhance_link_from_indexable( $model, $permalink ) {
$target = $this->indexable_repository->find_by_permalink( $permalink );
// If target indexable cannot be found, create one based on the post's post ID.
$post_id = $this->get_post_id( $model->type, $permalink );
if ( $post_id && $post_id !== 0 ) {
$target = $this->indexable_repository->find_by_id_and_type( $post_id, 'post' );
$model->target_indexable_id = $target->id;
if ( $target->object_type === 'post' ) {
$model->target_post_id = $target->object_id;
if ( $model->target_indexable_id ) {
$model->language = $target->language;
$model->region = $target->region;
* Builds the link's permalink.
* @param string $url The url of the link.
* @param array $home_url The home url, as parsed by wp_parse_url.
* @return string The link's permalink.
protected function build_permalink( $url, $home_url ) {
$permalink = $this->get_permalink( $url, $home_url );
if ( $this->url_helper->is_relative( $permalink ) ) {
// Make sure we're checking against the absolute URL, and add a trailing slash if the site has a trailing slash in its permalink settings.
$permalink = $this->url_helper->ensure_absolute_url( \user_trailingslashit( $permalink ) );
* Filters out links that point to the same page with a fragment or query.
* @param SEO_Links $link The link.
* @param array $current_url The url of the page the link is on, as parsed by wp_parse_url.
* @return bool Whether or not the link should be filtered.
protected function filter_link( SEO_Links $link, $current_url ) {
$url = $link->parsed_url;
// Always keep external links.
if ( $link->type === SEO_Links::TYPE_EXTERNAL ) {
// Always keep links with an empty path or pointing to other pages.
if ( isset( $url['path'] ) ) {
return empty( $url['path'] ) || $url['path'] !== $current_url['path'];
// Only keep links to the current page without a fragment or query.
return ( ! isset( $url['fragment'] ) && ! isset( $url['query'] ) );
* Updates the link counts for related indexables.
* @param Indexable $indexable The indexable.
* @param SEO_Links[] $links The link models.
protected function update_related_indexables( $indexable, $links ) {
// Old links were only stored by post id, so remove all old seo links for this post that have no indexable id.
// This can be removed if we ever fully clear all seo links.
if ( $indexable->object_type === 'post' ) {
$this->seo_links_repository->delete_all_by_post_id_where_indexable_id_null( $indexable->object_id );
$updated_indexable_ids = [];
$old_links = $this->seo_links_repository->find_all_by_indexable_id( $indexable->id );
$links_to_remove = $this->links_diff( $old_links, $links );
$links_to_add = $this->links_diff( $links, $old_links );
if ( ! empty( $links_to_remove ) ) {
$this->seo_links_repository->delete_many_by_id( \wp_list_pluck( $links_to_remove, 'id' ) );
if ( ! empty( $links_to_add ) ) {
$this->seo_links_repository->insert_many( $links_to_add );
foreach ( $links_to_add as $link ) {
if ( $link->target_indexable_id ) {
$updated_indexable_ids[] = $link->target_indexable_id;
foreach ( $links_to_remove as $link ) {
if ( $link->target_indexable_id ) {
$updated_indexable_ids[] = $link->target_indexable_id;
$this->update_incoming_links_for_related_indexables( $updated_indexable_ids );
* Creates a diff between two arrays of SEO links, based on urls.
* @param SEO_Links[] $links_a The array to compare.
* @param SEO_Links[] $links_b The array to compare against.
* @return SEO_Links[] Links that are in $links_a, but not in $links_b.
protected function links_diff( $links_a, $links_b ) {
static function ( SEO_Links $link_a, SEO_Links $link_b ) {
return \strcmp( $link_a->url, $link_b->url );
* Returns the number of internal links in an array of link models.
* @param SEO_Links[] $links The link models.
* @return int The number of internal links.
protected function get_internal_link_count( $links ) {
$internal_link_count = 0;
foreach ( $links as $link ) {
if ( $link->type === SEO_Links::TYPE_INTERNAL ) {
return $internal_link_count;
* Returns a cleaned permalink for a given link.
* @param string $link The raw URL.
* @param array $home_url The home URL, as parsed by wp_parse_url.
* @return string The cleaned permalink.
protected function get_permalink( $link, $home_url ) {
// Get rid of the #anchor.
$url_split = \explode( '#', $link );
// Get rid of URL ?query=string.
$url_split = \explode( '?', $link );
// Set the correct URL scheme.
$link = \set_url_scheme( $link, $home_url['scheme'] );
// Add 'www.' if it is absent and should be there.
if ( \strpos( $home_url['host'], 'www.' ) === 0 && \strpos( $link, '://www.' ) === false ) {
$link = \str_replace( '://', '://www.', $link );
// Strip 'www.' if it is present and shouldn't be.
if ( \strpos( $home_url['host'], 'www.' ) !== 0 ) {
$link = \str_replace( '://www.', '://', $link );
* Updates incoming link counts for related indexables.
* @param int[] $related_indexable_ids The IDs of all related indexables.
protected function update_incoming_links_for_related_indexables( $related_indexable_ids ) {
if ( empty( $related_indexable_ids ) ) {
$counts = $this->seo_links_repository->get_incoming_link_counts_for_indexable_ids( $related_indexable_ids );
if ( \wp_cache_supports( 'flush_group' ) ) {
\wp_cache_flush_group( 'orphaned_counts' );
foreach ( $counts as $count ) {
$this->indexable_repository->update_incoming_link_count( $count['target_indexable_id'], $count['incoming'] );