Initial commit: Atomaste website
This commit is contained in:
@@ -0,0 +1,909 @@
|
||||
<?php
|
||||
namespace AIOSEO\Plugin\Common\Sitemap;
|
||||
|
||||
// Exit if accessed directly.
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit;
|
||||
}
|
||||
|
||||
use AIOSEO\Plugin\Common\Integrations\BuddyPress as BuddyPressIntegration;
|
||||
|
||||
/**
|
||||
* Determines which content should be included in the sitemap.
|
||||
*
|
||||
* @since 4.0.0
|
||||
*/
|
||||
class Content {
|
||||
/**
|
||||
* Returns the entries for the requested sitemap.
|
||||
*
|
||||
* @since 4.0.0
|
||||
*
|
||||
* @return array The sitemap entries.
|
||||
*/
|
||||
public function get() {
|
||||
if ( ! in_array( aioseo()->sitemap->type, [ 'general', 'rss' ], true ) || ! $this->isEnabled() ) {
|
||||
return [];
|
||||
}
|
||||
|
||||
if ( 'rss' === aioseo()->sitemap->type ) {
|
||||
return $this->rss();
|
||||
}
|
||||
|
||||
if ( 'general' !== aioseo()->sitemap->type ) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$indexesEnabled = aioseo()->options->sitemap->general->indexes;
|
||||
if ( ! $indexesEnabled ) {
|
||||
if ( 'root' === aioseo()->sitemap->indexName ) {
|
||||
// If indexes are disabled, throw all entries together into one big file.
|
||||
return $this->nonIndexed();
|
||||
}
|
||||
|
||||
return [];
|
||||
}
|
||||
|
||||
if ( 'root' === aioseo()->sitemap->indexName ) {
|
||||
return aioseo()->sitemap->root->indexes();
|
||||
}
|
||||
|
||||
// Check if requested index has a dedicated method.
|
||||
$methodName = aioseo()->helpers->dashesToCamelCase( aioseo()->sitemap->indexName );
|
||||
if ( method_exists( $this, $methodName ) ) {
|
||||
return $this->$methodName();
|
||||
}
|
||||
|
||||
// Check if requested index is a registered post type.
|
||||
if ( in_array( aioseo()->sitemap->indexName, aioseo()->sitemap->helpers->includedPostTypes(), true ) ) {
|
||||
return $this->posts( aioseo()->sitemap->indexName );
|
||||
}
|
||||
|
||||
// Check if requested index is a registered taxonomy.
|
||||
if (
|
||||
in_array( aioseo()->sitemap->indexName, aioseo()->sitemap->helpers->includedTaxonomies(), true ) &&
|
||||
'product_attributes' !== aioseo()->sitemap->indexName
|
||||
) {
|
||||
return $this->terms( aioseo()->sitemap->indexName );
|
||||
}
|
||||
|
||||
if (
|
||||
aioseo()->helpers->isWooCommerceActive() &&
|
||||
in_array( aioseo()->sitemap->indexName, aioseo()->sitemap->helpers->includedTaxonomies(), true ) &&
|
||||
'product_attributes' === aioseo()->sitemap->indexName
|
||||
) {
|
||||
return $this->productAttributes();
|
||||
}
|
||||
|
||||
return [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the total entries number for the requested sitemap.
|
||||
*
|
||||
* @since 4.1.5
|
||||
*
|
||||
* @return int The total entries number.
|
||||
*/
|
||||
public function getTotal() {
|
||||
if ( ! in_array( aioseo()->sitemap->type, [ 'general', 'rss' ], true ) || ! $this->isEnabled() ) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
if ( 'rss' === aioseo()->sitemap->type ) {
|
||||
return count( $this->rss() );
|
||||
}
|
||||
|
||||
if ( 'general' !== aioseo()->sitemap->type ) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
$indexesEnabled = aioseo()->options->sitemap->general->indexes;
|
||||
if ( ! $indexesEnabled ) {
|
||||
if ( 'root' === aioseo()->sitemap->indexName ) {
|
||||
// If indexes are disabled, throw all entries together into one big file.
|
||||
return count( $this->nonIndexed() );
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
if ( 'root' === aioseo()->sitemap->indexName ) {
|
||||
return count( aioseo()->sitemap->root->indexes() );
|
||||
}
|
||||
|
||||
// Check if requested index has a dedicated method.
|
||||
$methodName = aioseo()->helpers->dashesToCamelCase( aioseo()->sitemap->indexName );
|
||||
if ( method_exists( $this, $methodName ) ) {
|
||||
$res = $this->$methodName();
|
||||
|
||||
return ! empty( $res ) ? count( $res ) : 0;
|
||||
}
|
||||
|
||||
// Check if requested index is a registered post type.
|
||||
if ( in_array( aioseo()->sitemap->indexName, aioseo()->sitemap->helpers->includedPostTypes(), true ) ) {
|
||||
return aioseo()->sitemap->query->posts( aioseo()->sitemap->indexName, [ 'count' => true ] );
|
||||
}
|
||||
|
||||
// Check if requested index is a registered taxonomy.
|
||||
if ( in_array( aioseo()->sitemap->indexName, aioseo()->sitemap->helpers->includedTaxonomies(), true ) ) {
|
||||
return aioseo()->sitemap->query->terms( aioseo()->sitemap->indexName, [ 'count' => true ] );
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the requested sitemap is enabled.
|
||||
*
|
||||
* @since 4.0.0
|
||||
*
|
||||
* @return boolean Whether the sitemap is enabled.
|
||||
*/
|
||||
public function isEnabled() {
|
||||
$options = aioseo()->options->noConflict();
|
||||
if ( ! $options->sitemap->{aioseo()->sitemap->type}->enable ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if ( $options->sitemap->{aioseo()->sitemap->type}->postTypes->all ) {
|
||||
return true;
|
||||
}
|
||||
|
||||
$included = aioseo()->sitemap->helpers->includedPostTypes();
|
||||
|
||||
return ! empty( $included );
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns all sitemap entries if indexing is disabled.
|
||||
*
|
||||
* @since 4.0.0
|
||||
*
|
||||
* @return array $entries The sitemap entries.
|
||||
*/
|
||||
private function nonIndexed() {
|
||||
$additional = $this->addl();
|
||||
$postTypes = aioseo()->sitemap->helpers->includedPostTypes();
|
||||
$isStaticHomepage = 'page' === get_option( 'show_on_front' );
|
||||
$blogPageEntry = [];
|
||||
$homePageEntry = ! $isStaticHomepage ? [ array_shift( $additional ) ] : [];
|
||||
$entries = array_merge( $additional, $this->author(), $this->date(), $this->postArchive() );
|
||||
|
||||
if ( $postTypes ) {
|
||||
foreach ( $postTypes as $postType ) {
|
||||
$postTypeEntries = $this->posts( $postType );
|
||||
|
||||
// If we don't have a static homepage, it's business as usual.
|
||||
if ( ! $isStaticHomepage ) {
|
||||
$entries = array_merge( $entries, $postTypeEntries );
|
||||
continue;
|
||||
}
|
||||
|
||||
$homePageId = (int) get_option( 'page_on_front' );
|
||||
$blogPageId = (int) get_option( 'page_for_posts' );
|
||||
|
||||
if ( 'post' === $postType && $blogPageId ) {
|
||||
$blogPageEntry[] = array_shift( $postTypeEntries );
|
||||
}
|
||||
|
||||
if ( 'page' === $postType && $homePageId ) {
|
||||
$homePageEntry[] = array_shift( $postTypeEntries );
|
||||
}
|
||||
|
||||
$entries = array_merge( $entries, $postTypeEntries );
|
||||
}
|
||||
}
|
||||
|
||||
$taxonomies = aioseo()->sitemap->helpers->includedTaxonomies();
|
||||
if ( $taxonomies ) {
|
||||
foreach ( $taxonomies as $taxonomy ) {
|
||||
$entries = array_merge( $entries, $this->terms( $taxonomy ) );
|
||||
}
|
||||
}
|
||||
|
||||
// Sort first by priority, then by last modified date.
|
||||
usort( $entries, function ( $a, $b ) {
|
||||
// If the priorities are equal, sort by last modified date.
|
||||
if ( $a['priority'] === $b['priority'] ) {
|
||||
return $a['lastmod'] > $b['lastmod'] ? -1 : 1;
|
||||
}
|
||||
|
||||
return $a['priority'] > $b['priority'] ? -1 : 1;
|
||||
} );
|
||||
|
||||
// Merge the arrays with the home page always first.
|
||||
return array_merge( $homePageEntry, $blogPageEntry, $entries );
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns all post entries for a given post type.
|
||||
*
|
||||
* @since 4.0.0
|
||||
*
|
||||
* @param string $postType The name of the post type.
|
||||
* @param array $additionalArgs Any additional arguments for the post query.
|
||||
* @return array The sitemap entries.
|
||||
*/
|
||||
public function posts( $postType, $additionalArgs = [] ) {
|
||||
$posts = aioseo()->sitemap->query->posts( $postType, $additionalArgs );
|
||||
if ( ! $posts ) {
|
||||
return [];
|
||||
}
|
||||
|
||||
// Return if we're determining the root indexes.
|
||||
if ( ! empty( $additionalArgs['root'] ) && $additionalArgs['root'] ) {
|
||||
return $posts;
|
||||
}
|
||||
|
||||
$entries = [];
|
||||
$isStaticHomepage = 'page' === get_option( 'show_on_front' );
|
||||
$homePageId = (int) get_option( 'page_on_front' );
|
||||
$excludeImages = aioseo()->sitemap->helpers->excludeImages();
|
||||
foreach ( $posts as $post ) {
|
||||
$entry = [
|
||||
'loc' => get_permalink( $post->ID ),
|
||||
'lastmod' => aioseo()->helpers->dateTimeToIso8601( $this->getLastModified( $post ) ),
|
||||
'changefreq' => aioseo()->sitemap->priority->frequency( 'postTypes', $post, $postType ),
|
||||
'priority' => aioseo()->sitemap->priority->priority( 'postTypes', $post, $postType ),
|
||||
];
|
||||
|
||||
if ( ! $excludeImages ) {
|
||||
$entry['images'] = ! empty( $post->images ) ? json_decode( $post->images ) : [];
|
||||
}
|
||||
|
||||
// Override priority/frequency for static homepage.
|
||||
if ( $isStaticHomepage && ( $homePageId === $post->ID || aioseo()->helpers->wpmlIsHomePage( $post->ID ) ) ) {
|
||||
$entry['loc'] = aioseo()->helpers->maybeRemoveTrailingSlash( aioseo()->helpers->wpmlHomeUrl( $post->ID ) ?: $entry['loc'] );
|
||||
$entry['changefreq'] = aioseo()->sitemap->priority->frequency( 'homePage' );
|
||||
$entry['priority'] = aioseo()->sitemap->priority->priority( 'homePage' );
|
||||
}
|
||||
|
||||
$entries[] = apply_filters( 'aioseo_sitemap_post', $entry, $post->ID, $postType, 'post' );
|
||||
}
|
||||
|
||||
// We can't remove the post type here because other plugins rely on it.
|
||||
return apply_filters( 'aioseo_sitemap_posts', $entries, $postType );
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns all post archive entries.
|
||||
*
|
||||
* @since 4.0.0
|
||||
*
|
||||
* @return array $entries The sitemap entries.
|
||||
*/
|
||||
private function postArchive() {
|
||||
$entries = [];
|
||||
foreach ( aioseo()->sitemap->helpers->includedPostTypes( true ) as $postType ) {
|
||||
if (
|
||||
aioseo()->dynamicOptions->noConflict()->searchAppearance->archives->has( $postType ) &&
|
||||
! aioseo()->dynamicOptions->searchAppearance->archives->$postType->advanced->robotsMeta->default &&
|
||||
aioseo()->dynamicOptions->searchAppearance->archives->$postType->advanced->robotsMeta->noindex
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$post = aioseo()->core->db
|
||||
->start( aioseo()->core->db->db->posts . ' as p', true )
|
||||
->select( 'p.ID' )
|
||||
->where( 'p.post_status', 'publish' )
|
||||
->where( 'p.post_type', $postType )
|
||||
->limit( 1 )
|
||||
->run()
|
||||
->result();
|
||||
|
||||
if ( ! $post ) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$url = get_post_type_archive_link( $postType );
|
||||
if ( $url ) {
|
||||
$entries[] = [
|
||||
'loc' => $url,
|
||||
'lastmod' => aioseo()->sitemap->helpers->lastModifiedPostTime( $postType ),
|
||||
'changefreq' => aioseo()->sitemap->priority->frequency( 'archive' ),
|
||||
'priority' => aioseo()->sitemap->priority->priority( 'archive' ),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
return apply_filters( 'aioseo_sitemap_post_archives', $entries );
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns all term entries for a given taxonomy.
|
||||
*
|
||||
* @since 4.0.0
|
||||
*
|
||||
* @param string $taxonomy The name of the taxonomy.
|
||||
* @param array $additionalArgs Any additional arguments for the term query.
|
||||
* @return array The sitemap entries.
|
||||
*/
|
||||
public function terms( $taxonomy, $additionalArgs = [] ) {
|
||||
$terms = aioseo()->sitemap->query->terms( $taxonomy, $additionalArgs );
|
||||
if ( ! $terms ) {
|
||||
return [];
|
||||
}
|
||||
|
||||
// Get all registered post types for the taxonomy.
|
||||
$postTypes = [];
|
||||
foreach ( get_post_types() as $postType ) {
|
||||
$taxonomies = get_object_taxonomies( $postType );
|
||||
foreach ( $taxonomies as $name ) {
|
||||
if ( $taxonomy === $name ) {
|
||||
$postTypes[] = $postType;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Return if we're determining the root indexes.
|
||||
if ( ! empty( $additionalArgs['root'] ) && $additionalArgs['root'] ) {
|
||||
return $terms;
|
||||
}
|
||||
|
||||
$entries = [];
|
||||
foreach ( $terms as $term ) {
|
||||
$entry = [
|
||||
'loc' => get_term_link( $term->term_id ),
|
||||
'lastmod' => $this->getTermLastModified( $term ),
|
||||
'changefreq' => aioseo()->sitemap->priority->frequency( 'taxonomies', $term, $taxonomy ),
|
||||
'priority' => aioseo()->sitemap->priority->priority( 'taxonomies', $term, $taxonomy ),
|
||||
'images' => aioseo()->sitemap->image->term( $term )
|
||||
];
|
||||
|
||||
$entries[] = apply_filters( 'aioseo_sitemap_term', $entry, $term->term_id, $term->taxonomy, 'term' );
|
||||
}
|
||||
|
||||
return apply_filters( 'aioseo_sitemap_terms', $entries );
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the last modified date for a given term.
|
||||
*
|
||||
* @since 4.0.0
|
||||
*
|
||||
* @param int|object $term The term data object.
|
||||
* @return string The lastmod timestamp.
|
||||
*/
|
||||
public function getTermLastModified( $term ) {
|
||||
$termRelationshipsTable = aioseo()->core->db->db->prefix . 'term_relationships';
|
||||
$termTaxonomyTable = aioseo()->core->db->db->prefix . 'term_taxonomy';
|
||||
|
||||
// If the term is an ID, get the term object.
|
||||
if ( is_numeric( $term ) ) {
|
||||
$term = aioseo()->helpers->getTerm( $term );
|
||||
}
|
||||
|
||||
// First, check the count of the term. If it's 0, then we're dealing with a parent term that does not have
|
||||
// posts assigned to it. In this case, we need to get the last modified date of all its children.
|
||||
if ( empty( $term->count ) ) {
|
||||
$lastModified = aioseo()->core->db
|
||||
->start( aioseo()->core->db->db->posts . ' as p', true )
|
||||
->select( 'MAX(`p`.`post_modified_gmt`) as last_modified' )
|
||||
->where( 'p.post_status', 'publish' )
|
||||
->whereRaw( "
|
||||
( `p`.`ID` IN
|
||||
(
|
||||
SELECT CONVERT(`tr`.`object_id`, unsigned)
|
||||
FROM `$termRelationshipsTable` as tr
|
||||
JOIN `$termTaxonomyTable` as tt ON `tr`.`term_taxonomy_id` = `tt`.`term_taxonomy_id`
|
||||
WHERE `tt`.`term_id` IN
|
||||
(
|
||||
SELECT `tt`.`term_id`
|
||||
FROM `$termTaxonomyTable` as tt
|
||||
WHERE `tt`.`parent` = '{$term->term_id}'
|
||||
)
|
||||
)
|
||||
)" )
|
||||
->run()
|
||||
->result();
|
||||
} else {
|
||||
$lastModified = aioseo()->core->db
|
||||
->start( aioseo()->core->db->db->posts . ' as p', true )
|
||||
->select( 'MAX(`p`.`post_modified_gmt`) as last_modified' )
|
||||
->where( 'p.post_status', 'publish' )
|
||||
->whereRaw( "
|
||||
( `p`.`ID` IN
|
||||
(
|
||||
SELECT CONVERT(`tr`.`object_id`, unsigned)
|
||||
FROM `$termRelationshipsTable` as tr
|
||||
JOIN `$termTaxonomyTable` as tt ON `tr`.`term_taxonomy_id` = `tt`.`term_taxonomy_id`
|
||||
WHERE `tt`.`term_id` = '{$term->term_id}'
|
||||
)
|
||||
)" )
|
||||
->run()
|
||||
->result();
|
||||
}
|
||||
|
||||
$lastModified = $lastModified[0]->last_modified ?? '';
|
||||
|
||||
return aioseo()->helpers->dateTimeToIso8601( $lastModified );
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns all additional pages.
|
||||
*
|
||||
* @since 4.0.0
|
||||
*
|
||||
* @param bool $shouldChunk Whether the entries should be chuncked. Is set to false when the static sitemap is generated.
|
||||
* @return array The sitemap entries.
|
||||
*/
|
||||
public function addl( $shouldChunk = true ) {
|
||||
$additionalPages = [];
|
||||
if ( aioseo()->options->sitemap->general->additionalPages->enable ) {
|
||||
$additionalPages = array_map( 'json_decode', aioseo()->options->sitemap->general->additionalPages->pages );
|
||||
$additionalPages = array_filter( $additionalPages, function( $additionalPage ) {
|
||||
return ! empty( $additionalPage->url );
|
||||
} );
|
||||
}
|
||||
|
||||
$entries = [];
|
||||
foreach ( $additionalPages as $additionalPage ) {
|
||||
$entries[] = [
|
||||
'loc' => $additionalPage->url,
|
||||
'lastmod' => aioseo()->sitemap->helpers->lastModifiedAdditionalPage( $additionalPage ),
|
||||
'changefreq' => $additionalPage->frequency->value,
|
||||
'priority' => $additionalPage->priority->value,
|
||||
'isTimezone' => true
|
||||
];
|
||||
}
|
||||
|
||||
$postTypes = aioseo()->sitemap->helpers->includedPostTypes();
|
||||
$shouldIncludeHomepage = 'posts' === get_option( 'show_on_front' ) || ! in_array( 'page', $postTypes, true );
|
||||
if ( $shouldIncludeHomepage ) {
|
||||
$frontPageId = (int) get_option( 'page_on_front' );
|
||||
$frontPageUrl = aioseo()->helpers->localizedUrl( '/' );
|
||||
$post = aioseo()->helpers->getPost( $frontPageId );
|
||||
|
||||
$homepageEntry = [
|
||||
'loc' => aioseo()->helpers->maybeRemoveTrailingSlash( $frontPageUrl ),
|
||||
'lastmod' => $post ? aioseo()->helpers->dateTimeToIso8601( $this->getLastModified( $post ) ) : aioseo()->sitemap->helpers->lastModifiedPostTime(),
|
||||
'changefreq' => aioseo()->sitemap->priority->frequency( 'homePage' ),
|
||||
'priority' => aioseo()->sitemap->priority->priority( 'homePage' )
|
||||
];
|
||||
|
||||
$translatedHomepages = aioseo()->helpers->wpmlHomePages();
|
||||
foreach ( $translatedHomepages as $languageCode => $translatedHomepage ) {
|
||||
if ( untrailingslashit( $translatedHomepage['url'] ) !== untrailingslashit( $homepageEntry['loc'] ) ) {
|
||||
$homepageEntry['languages'][] = [
|
||||
'language' => $languageCode,
|
||||
'location' => $translatedHomepage['url']
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
// Add homepage to the first position.
|
||||
array_unshift( $entries, $homepageEntry );
|
||||
}
|
||||
|
||||
if ( aioseo()->options->sitemap->general->additionalPages->enable ) {
|
||||
$entries = apply_filters( 'aioseo_sitemap_additional_pages', $entries );
|
||||
}
|
||||
|
||||
if ( empty( $entries ) ) {
|
||||
return [];
|
||||
}
|
||||
|
||||
if ( aioseo()->options->sitemap->general->indexes && $shouldChunk ) {
|
||||
$entries = aioseo()->sitemap->helpers->chunkEntries( $entries );
|
||||
$entries = $entries[ aioseo()->sitemap->pageNumber ] ?? [];
|
||||
}
|
||||
|
||||
return $entries;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns all author archive entries.
|
||||
*
|
||||
* @since 4.0.0
|
||||
*
|
||||
* @return array The sitemap entries.
|
||||
*/
|
||||
public function author() {
|
||||
if (
|
||||
! aioseo()->sitemap->helpers->lastModifiedPost() ||
|
||||
! aioseo()->options->sitemap->general->author ||
|
||||
! aioseo()->options->searchAppearance->archives->author->show ||
|
||||
(
|
||||
! aioseo()->options->searchAppearance->archives->author->advanced->robotsMeta->default &&
|
||||
aioseo()->options->searchAppearance->archives->author->advanced->robotsMeta->noindex
|
||||
) ||
|
||||
(
|
||||
aioseo()->options->searchAppearance->archives->author->advanced->robotsMeta->default &&
|
||||
(
|
||||
! aioseo()->options->searchAppearance->advanced->globalRobotsMeta->default &&
|
||||
aioseo()->options->searchAppearance->advanced->globalRobotsMeta->noindex
|
||||
)
|
||||
)
|
||||
) {
|
||||
return [];
|
||||
}
|
||||
|
||||
// Allow users to filter the authors in case their sites use a membership plugin or have custom code that affect the authors on their site.
|
||||
// e.g. there might be additional roles/conditions that need to be checked here.
|
||||
$authors = apply_filters( 'aioseo_sitemap_authors', [] );
|
||||
if ( empty( $authors ) ) {
|
||||
$usersTableName = aioseo()->core->db->db->users; // We get the table name from WPDB since multisites share the same table.
|
||||
$authors = aioseo()->core->db->start( "$usersTableName as u", true )
|
||||
->select( 'u.ID as ID, u.user_nicename as nicename, MAX(p.post_modified_gmt) as lastModified' )
|
||||
->join( 'posts as p', 'u.ID = p.post_author' )
|
||||
->where( 'p.post_status', 'publish' )
|
||||
->whereIn( 'p.post_type', aioseo()->sitemap->helpers->getAuthorPostTypes() )
|
||||
->groupBy( 'u.ID' )
|
||||
->orderBy( 'lastModified DESC' )
|
||||
->limit( aioseo()->sitemap->linksPerIndex, aioseo()->sitemap->pageNumber * aioseo()->sitemap->linksPerIndex )
|
||||
->run()
|
||||
->result();
|
||||
}
|
||||
|
||||
if ( empty( $authors ) ) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$entries = [];
|
||||
foreach ( $authors as $authorData ) {
|
||||
$nicename = $authorData->nicename ? $authorData->nicename : null;
|
||||
$entries[] = [
|
||||
'loc' => ! empty( $authorData->authorUrl ) ? $authorData->authorUrl : get_author_posts_url( $authorData->ID, $nicename ),
|
||||
'lastmod' => aioseo()->helpers->dateTimeToIso8601( $authorData->lastModified ),
|
||||
'changefreq' => aioseo()->sitemap->priority->frequency( 'author' ),
|
||||
'priority' => aioseo()->sitemap->priority->priority( 'author' )
|
||||
];
|
||||
}
|
||||
|
||||
return apply_filters( 'aioseo_sitemap_author_archives', $entries );
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns all data archive entries.
|
||||
*
|
||||
* @since 4.0.0
|
||||
*
|
||||
* @return array The sitemap entries.
|
||||
*/
|
||||
public function date() {
|
||||
if (
|
||||
! aioseo()->sitemap->helpers->lastModifiedPost() ||
|
||||
! aioseo()->options->sitemap->general->date ||
|
||||
! aioseo()->options->searchAppearance->archives->date->show ||
|
||||
(
|
||||
! aioseo()->options->searchAppearance->archives->date->advanced->robotsMeta->default &&
|
||||
aioseo()->options->searchAppearance->archives->date->advanced->robotsMeta->noindex
|
||||
) ||
|
||||
(
|
||||
aioseo()->options->searchAppearance->archives->date->advanced->robotsMeta->default &&
|
||||
(
|
||||
! aioseo()->options->searchAppearance->advanced->globalRobotsMeta->default &&
|
||||
aioseo()->options->searchAppearance->advanced->globalRobotsMeta->noindex
|
||||
)
|
||||
)
|
||||
) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$postsTable = aioseo()->core->db->db->posts;
|
||||
$dates = aioseo()->core->db->execute(
|
||||
"SELECT
|
||||
YEAR(post_date) AS `year`,
|
||||
MONTH(post_date) AS `month`,
|
||||
post_date_gmt,
|
||||
post_modified_gmt
|
||||
FROM {$postsTable}
|
||||
WHERE post_type = 'post' AND post_status = 'publish'
|
||||
GROUP BY
|
||||
YEAR(post_date),
|
||||
MONTH(post_date)
|
||||
ORDER BY post_date ASC
|
||||
LIMIT 50000",
|
||||
true
|
||||
)->result();
|
||||
|
||||
if ( empty( $dates ) ) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$entries = [];
|
||||
$year = '';
|
||||
foreach ( $dates as $date ) {
|
||||
$entry = [
|
||||
'lastmod' => aioseo()->helpers->dateTimeToIso8601( $this->getLastModified( $date ) ),
|
||||
'changefreq' => aioseo()->sitemap->priority->frequency( 'date' ),
|
||||
'priority' => aioseo()->sitemap->priority->priority( 'date' ),
|
||||
];
|
||||
|
||||
// Include each year only once.
|
||||
if ( $year !== $date->year ) {
|
||||
$year = $date->year;
|
||||
$entry['loc'] = get_year_link( $date->year );
|
||||
$entries[] = $entry;
|
||||
}
|
||||
$entry['loc'] = get_month_link( $date->year, $date->month );
|
||||
$entries[] = $entry;
|
||||
}
|
||||
|
||||
return apply_filters( 'aioseo_sitemap_date_archives', $entries );
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns all entries for the RSS Sitemap.
|
||||
*
|
||||
* @since 4.0.0
|
||||
*
|
||||
* @return array The sitemap entries.
|
||||
*/
|
||||
public function rss() {
|
||||
$posts = aioseo()->sitemap->query->posts(
|
||||
aioseo()->sitemap->helpers->includedPostTypes(),
|
||||
[ 'orderBy' => '`p`.`post_modified_gmt` DESC' ]
|
||||
);
|
||||
|
||||
if ( ! count( $posts ) ) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$entries = [];
|
||||
foreach ( $posts as $post ) {
|
||||
$entry = [
|
||||
'guid' => get_permalink( $post->ID ),
|
||||
'title' => get_the_title( $post ),
|
||||
'description' => get_post_field( 'post_excerpt', $post->ID ),
|
||||
'pubDate' => aioseo()->helpers->dateTimeToRfc822( $this->getLastModified( $post ) )
|
||||
];
|
||||
|
||||
$entries[] = apply_filters( 'aioseo_sitemap_post_rss', $entry, $post->ID, $post->post_type, 'post' );
|
||||
}
|
||||
|
||||
usort( $entries, function( $a, $b ) {
|
||||
return $a['pubDate'] < $b['pubDate'] ? 1 : 0;
|
||||
});
|
||||
|
||||
return apply_filters( 'aioseo_sitemap_rss', $entries );
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the last modified date for a given post.
|
||||
*
|
||||
* @since 4.6.3
|
||||
*
|
||||
* @param object $post The post object.
|
||||
*
|
||||
* @return string The last modified date.
|
||||
*/
|
||||
public function getLastModified( $post ) {
|
||||
$publishDate = $post->post_date_gmt;
|
||||
$lastModifiedDate = $post->post_modified_gmt;
|
||||
|
||||
// Get the date which is the latest.
|
||||
return $lastModifiedDate > $publishDate ? $lastModifiedDate : $publishDate;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns all entries for the BuddyPress Activity Sitemap.
|
||||
* This method is automagically called from {@see get()} if the current index name equals to 'bp-activity'
|
||||
*
|
||||
* @since 4.7.6
|
||||
*
|
||||
* @return array The sitemap entries.
|
||||
*/
|
||||
public function bpActivity() {
|
||||
$entries = [];
|
||||
if ( ! in_array( aioseo()->sitemap->indexName, aioseo()->sitemap->helpers->includedPostTypes(), true ) ) {
|
||||
return $entries;
|
||||
}
|
||||
|
||||
$postType = 'bp-activity';
|
||||
$query = aioseo()->core->db
|
||||
->start( 'bp_activity as a' )
|
||||
->select( '`a`.`id`, `a`.`date_recorded`' )
|
||||
->whereRaw( "a.is_spam = 0 AND a.hide_sitewide = 0 AND a.type NOT IN ('activity_comment', 'last_activity')" )
|
||||
->limit( aioseo()->sitemap->linksPerIndex, aioseo()->sitemap->offset )
|
||||
->orderBy( '`a`.`date_recorded` DESC' );
|
||||
|
||||
$items = $query->run()
|
||||
->result();
|
||||
|
||||
foreach ( $items as $item ) {
|
||||
$entry = [
|
||||
'loc' => BuddyPressIntegration::getComponentSingleUrl( 'activity', $item->id ),
|
||||
'lastmod' => aioseo()->helpers->dateTimeToIso8601( $item->date_recorded ),
|
||||
'changefreq' => aioseo()->sitemap->priority->frequency( 'postTypes', false, $postType ),
|
||||
'priority' => aioseo()->sitemap->priority->priority( 'postTypes', false, $postType ),
|
||||
];
|
||||
|
||||
$entries[] = apply_filters( 'aioseo_sitemap_post', $entry, $item->id, $postType );
|
||||
}
|
||||
|
||||
$archiveUrl = BuddyPressIntegration::getComponentArchiveUrl( 'activity' );
|
||||
if (
|
||||
aioseo()->helpers->isUrl( $archiveUrl ) &&
|
||||
! in_array( $postType, aioseo()->helpers->getNoindexedObjects( 'archives' ), true )
|
||||
) {
|
||||
$lastMod = ! empty( $items[0] ) ? $items[0]->date_recorded : current_time( 'mysql' );
|
||||
$entry = [
|
||||
'loc' => $archiveUrl,
|
||||
'lastmod' => $lastMod,
|
||||
'changefreq' => aioseo()->sitemap->priority->frequency( 'postTypes', false, $postType ),
|
||||
'priority' => aioseo()->sitemap->priority->priority( 'postTypes', false, $postType ),
|
||||
];
|
||||
|
||||
array_unshift( $entries, $entry );
|
||||
}
|
||||
|
||||
return apply_filters( 'aioseo_sitemap_posts', $entries, $postType );
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns all entries for the BuddyPress Group Sitemap.
|
||||
* This method is automagically called from {@see get()} if the current index name equals to 'bp-group'
|
||||
*
|
||||
* @since 4.7.6
|
||||
*
|
||||
* @return array The sitemap entries.
|
||||
*/
|
||||
public function bpGroup() {
|
||||
$entries = [];
|
||||
if ( ! in_array( aioseo()->sitemap->indexName, aioseo()->sitemap->helpers->includedPostTypes(), true ) ) {
|
||||
return $entries;
|
||||
}
|
||||
|
||||
$postType = 'bp-group';
|
||||
$query = aioseo()->core->db
|
||||
->start( 'bp_groups as g' )
|
||||
->select( '`g`.`id`, `g`.`date_created`, `gm`.`meta_value` as date_modified' )
|
||||
->leftJoin( 'bp_groups_groupmeta as gm', 'g.id = gm.group_id' )
|
||||
->whereRaw( "g.status = 'public' AND gm.meta_key = 'last_activity'" )
|
||||
->limit( aioseo()->sitemap->linksPerIndex, aioseo()->sitemap->offset )
|
||||
->orderBy( '`gm`.`meta_value` DESC, `g`.`date_created` DESC' );
|
||||
|
||||
$items = $query->run()
|
||||
->result();
|
||||
|
||||
foreach ( $items as $item ) {
|
||||
$lastMod = $item->date_modified ?: $item->date_created;
|
||||
$entry = [
|
||||
'loc' => BuddyPressIntegration::getComponentSingleUrl( 'group', BuddyPressIntegration::callFunc( 'bp_get_group_by', 'id', $item->id ) ),
|
||||
'lastmod' => aioseo()->helpers->dateTimeToIso8601( $lastMod ),
|
||||
'changefreq' => aioseo()->sitemap->priority->frequency( 'postTypes', false, $postType ),
|
||||
'priority' => aioseo()->sitemap->priority->priority( 'postTypes', false, $postType ),
|
||||
];
|
||||
|
||||
$entries[] = apply_filters( 'aioseo_sitemap_post', $entry, $item->id, $postType );
|
||||
}
|
||||
|
||||
$archiveUrl = BuddyPressIntegration::getComponentArchiveUrl( 'group' );
|
||||
if (
|
||||
aioseo()->helpers->isUrl( $archiveUrl ) &&
|
||||
! in_array( $postType, aioseo()->helpers->getNoindexedObjects( 'archives' ), true )
|
||||
) {
|
||||
$lastMod = ! empty( $items[0] ) ? $items[0]->date_modified : current_time( 'mysql' );
|
||||
$entry = [
|
||||
'loc' => $archiveUrl,
|
||||
'lastmod' => $lastMod,
|
||||
'changefreq' => aioseo()->sitemap->priority->frequency( 'postTypes', false, $postType ),
|
||||
'priority' => aioseo()->sitemap->priority->priority( 'postTypes', false, $postType ),
|
||||
];
|
||||
|
||||
array_unshift( $entries, $entry );
|
||||
}
|
||||
|
||||
return apply_filters( 'aioseo_sitemap_posts', $entries, $postType );
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns all entries for the BuddyPress Member Sitemap.
|
||||
* This method is automagically called from {@see get()} if the current index name equals to 'bp-member'
|
||||
*
|
||||
* @since 4.7.6
|
||||
*
|
||||
* @return array The sitemap entries.
|
||||
*/
|
||||
public function bpMember() {
|
||||
$entries = [];
|
||||
if ( ! in_array( aioseo()->sitemap->indexName, aioseo()->sitemap->helpers->includedPostTypes(), true ) ) {
|
||||
return $entries;
|
||||
}
|
||||
|
||||
$postType = 'bp-member';
|
||||
$query = aioseo()->core->db
|
||||
->start( 'bp_activity as a' )
|
||||
->select( '`a`.`user_id` as id, `a`.`date_recorded`' )
|
||||
->whereRaw( "a.component = 'members' AND a.type = 'last_activity'" )
|
||||
->limit( aioseo()->sitemap->linksPerIndex, aioseo()->sitemap->offset )
|
||||
->orderBy( '`a`.`date_recorded` DESC' );
|
||||
|
||||
$items = $query->run()
|
||||
->result();
|
||||
|
||||
foreach ( $items as $item ) {
|
||||
$entry = [
|
||||
'loc' => BuddyPressIntegration::getComponentSingleUrl( 'member', $item->id ),
|
||||
'lastmod' => aioseo()->helpers->dateTimeToIso8601( $item->date_recorded ),
|
||||
'changefreq' => aioseo()->sitemap->priority->frequency( 'postTypes', false, $postType ),
|
||||
'priority' => aioseo()->sitemap->priority->priority( 'postTypes', false, $postType ),
|
||||
];
|
||||
|
||||
$entries[] = apply_filters( 'aioseo_sitemap_post', $entry, $item->id, $postType );
|
||||
}
|
||||
|
||||
$archiveUrl = BuddyPressIntegration::getComponentArchiveUrl( 'member' );
|
||||
if (
|
||||
aioseo()->helpers->isUrl( $archiveUrl ) &&
|
||||
! in_array( $postType, aioseo()->helpers->getNoindexedObjects( 'archives' ), true )
|
||||
) {
|
||||
$lastMod = ! empty( $items[0] ) ? $items[0]->date_recorded : current_time( 'mysql' );
|
||||
$entry = [
|
||||
'loc' => $archiveUrl,
|
||||
'lastmod' => $lastMod,
|
||||
'changefreq' => aioseo()->sitemap->priority->frequency( 'postTypes', false, $postType ),
|
||||
'priority' => aioseo()->sitemap->priority->priority( 'postTypes', false, $postType ),
|
||||
];
|
||||
|
||||
array_unshift( $entries, $entry );
|
||||
}
|
||||
|
||||
return apply_filters( 'aioseo_sitemap_posts', $entries, $postType );
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns all entries for the WooCommerce Product Attributes sitemap.
|
||||
* Note: This sitemap does not support pagination.
|
||||
*
|
||||
* @since 4.7.8
|
||||
*
|
||||
* @param bool $count Whether to return the count of the entries. This is used to determine the indexes.
|
||||
* @return array The sitemap entries.
|
||||
*/
|
||||
public function productAttributes( $count = false ) {
|
||||
$aioseoTermsTable = aioseo()->core->db->prefix . 'aioseo_terms';
|
||||
$wcAttributeTaxonomiesTable = aioseo()->core->db->prefix . 'woocommerce_attribute_taxonomies';
|
||||
$termTaxonomyTable = aioseo()->core->db->prefix . 'term_taxonomy';
|
||||
|
||||
$selectClause = 'COUNT(*) as childProductAttributes';
|
||||
if ( ! $count ) {
|
||||
$selectClause = aioseo()->pro ? 'tt.term_id, at.frequency, at.priority' : 'tt.term_id';
|
||||
}
|
||||
|
||||
$joinClause = aioseo()->pro ? "LEFT JOIN {$aioseoTermsTable} AS at ON tt.term_id = at.term_id" : '';
|
||||
$whereClause = aioseo()->pro ? 'AND (at.robots_noindex IS NULL OR at.robots_noindex = 0)' : '';
|
||||
$limitClause = $count ? '' : 'LIMIT 50000';
|
||||
|
||||
$result = aioseo()->core->db->execute(
|
||||
"SELECT {$selectClause}
|
||||
FROM {$termTaxonomyTable} AS tt
|
||||
JOIN {$wcAttributeTaxonomiesTable} AS wat ON tt.taxonomy = CONCAT('pa_', wat.attribute_name)
|
||||
{$joinClause}
|
||||
WHERE wat.attribute_public = 1
|
||||
{$whereClause}
|
||||
AND tt.count > 0
|
||||
{$limitClause};",
|
||||
true
|
||||
)->result();
|
||||
|
||||
if ( $count ) {
|
||||
return ! empty( $result[0]->childProductAttributes ) ? (int) $result[0]->childProductAttributes : 0;
|
||||
}
|
||||
|
||||
if ( empty( $result ) ) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$entries = [];
|
||||
foreach ( $result as $term ) {
|
||||
$term = (object) $term;
|
||||
$termId = (int) $term->term_id;
|
||||
|
||||
$entry = [
|
||||
'loc' => get_term_link( $termId ),
|
||||
'lastmod' => $this->getTermLastModified( $termId ),
|
||||
'changefreq' => aioseo()->sitemap->priority->frequency( 'taxonomies', $term, 'product_attributes' ),
|
||||
'priority' => aioseo()->sitemap->priority->priority( 'taxonomies', $term, 'product_attributes' ),
|
||||
'images' => aioseo()->sitemap->image->term( $term )
|
||||
];
|
||||
|
||||
$entries[] = apply_filters( 'aioseo_sitemap_product_attributes', $entry, $termId );
|
||||
}
|
||||
|
||||
return $entries;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,287 @@
|
||||
<?php
|
||||
namespace AIOSEO\Plugin\Common\Sitemap;
|
||||
|
||||
// Exit if accessed directly.
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit;
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles the static sitemap.
|
||||
*
|
||||
* @since 4.0.0
|
||||
*/
|
||||
class File {
|
||||
/**
|
||||
* Whether the static files have already been updated during the current request.
|
||||
*
|
||||
* We keep track of this so that setting changes to do not trigger the regeneration multiple times.
|
||||
*
|
||||
* @since 4.0.0
|
||||
*
|
||||
* @var boolean
|
||||
*/
|
||||
private static $isUpdated = false;
|
||||
|
||||
/**
|
||||
* Generates the static sitemap files.
|
||||
*
|
||||
* @since 4.0.0
|
||||
*
|
||||
* @param boolean $force Whether or not to force it through.
|
||||
* @return void
|
||||
*/
|
||||
public function generate( $force = false ) {
|
||||
aioseo()->addons->doAddonFunction( 'file', 'generate', [ $force ] );
|
||||
|
||||
// Exit if static sitemap generation isn't enabled.
|
||||
if (
|
||||
! $force &&
|
||||
(
|
||||
self::$isUpdated ||
|
||||
! aioseo()->options->sitemap->general->enable ||
|
||||
! aioseo()->options->sitemap->general->advancedSettings->enable ||
|
||||
! in_array( 'staticSitemap', aioseo()->internalOptions->internal->deprecatedOptions, true ) ||
|
||||
aioseo()->options->deprecated->sitemap->general->advancedSettings->dynamic
|
||||
)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
$files = [];
|
||||
self::$isUpdated = true;
|
||||
// We need to set these values here as setContext() doesn't run.
|
||||
// Subsequently, we need to manually reset the index name below for each query we run.
|
||||
// Also, since we need to chunck the entries manually, we cannot limit any queries and need to reset the amount of allowed URLs per index.
|
||||
aioseo()->sitemap->offset = 0;
|
||||
aioseo()->sitemap->type = 'general';
|
||||
$sitemapName = aioseo()->sitemap->helpers->filename();
|
||||
aioseo()->sitemap->indexes = aioseo()->options->sitemap->general->indexes;
|
||||
aioseo()->sitemap->linksPerIndex = PHP_INT_MAX;
|
||||
aioseo()->sitemap->isStatic = true;
|
||||
|
||||
$additionalPages = [];
|
||||
if ( aioseo()->options->sitemap->general->additionalPages->enable ) {
|
||||
foreach ( aioseo()->options->sitemap->general->additionalPages->pages as $additionalPage ) {
|
||||
$additionalPage = json_decode( $additionalPage );
|
||||
if ( empty( $additionalPage->url ) ) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Decode Additional Page Url to properly show Unicode Characters.
|
||||
$additionalPages[] = $additionalPage;
|
||||
}
|
||||
}
|
||||
|
||||
$postTypes = aioseo()->sitemap->helpers->includedPostTypes();
|
||||
$additionalPages = apply_filters( 'aioseo_sitemap_additional_pages', $additionalPages );
|
||||
|
||||
if (
|
||||
'posts' === get_option( 'show_on_front' ) ||
|
||||
count( $additionalPages ) ||
|
||||
! in_array( 'page', $postTypes, true )
|
||||
) {
|
||||
$entries = aioseo()->sitemap->content->addl( false );
|
||||
$filename = "addl-$sitemapName.xml";
|
||||
$files[ $filename ] = [
|
||||
'total' => count( $entries ),
|
||||
'entries' => $entries
|
||||
];
|
||||
}
|
||||
|
||||
if (
|
||||
aioseo()->sitemap->helpers->lastModifiedPost() &&
|
||||
aioseo()->options->sitemap->general->author
|
||||
) {
|
||||
$entries = aioseo()->sitemap->content->author();
|
||||
$filename = "author-$sitemapName.xml";
|
||||
$files[ $filename ] = [
|
||||
'total' => count( $entries ),
|
||||
'entries' => $entries
|
||||
];
|
||||
}
|
||||
|
||||
if (
|
||||
aioseo()->sitemap->helpers->lastModifiedPost() &&
|
||||
aioseo()->options->sitemap->general->date
|
||||
) {
|
||||
$entries = aioseo()->sitemap->content->date();
|
||||
$filename = "date-$sitemapName.xml";
|
||||
$files[ $filename ] = [
|
||||
'total' => count( $entries ),
|
||||
'entries' => $entries
|
||||
];
|
||||
}
|
||||
|
||||
$postTypes = aioseo()->sitemap->helpers->includedPostTypes();
|
||||
if ( $postTypes ) {
|
||||
foreach ( $postTypes as $postType ) {
|
||||
aioseo()->sitemap->indexName = $postType;
|
||||
|
||||
$posts = aioseo()->sitemap->content->posts( $postType );
|
||||
if ( ! $posts ) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$total = aioseo()->sitemap->query->posts( $postType, [ 'count' => true ] );
|
||||
|
||||
// We need to temporarily reset the linksPerIndex count here so that we can properly chunk.
|
||||
aioseo()->sitemap->linksPerIndex = aioseo()->options->sitemap->general->linksPerIndex;
|
||||
$chunks = aioseo()->sitemap->helpers->chunkEntries( $posts );
|
||||
aioseo()->sitemap->linksPerIndex = PHP_INT_MAX;
|
||||
|
||||
if ( 1 === count( $chunks ) ) {
|
||||
$filename = "$postType-$sitemapName.xml";
|
||||
$files[ $filename ] = [
|
||||
'total' => $total,
|
||||
'entries' => $chunks[0]
|
||||
];
|
||||
} else {
|
||||
for ( $i = 1; $i <= count( $chunks ); $i++ ) {
|
||||
$filename = "$postType-$sitemapName$i.xml";
|
||||
$files[ $filename ] = [
|
||||
'total' => $total,
|
||||
'entries' => $chunks[ $i - 1 ]
|
||||
];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$taxonomies = aioseo()->sitemap->helpers->includedTaxonomies();
|
||||
if ( $taxonomies ) {
|
||||
foreach ( $taxonomies as $taxonomy ) {
|
||||
aioseo()->sitemap->indexName = $taxonomy;
|
||||
|
||||
$terms = aioseo()->sitemap->content->terms( $taxonomy );
|
||||
if ( ! $terms ) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$total = aioseo()->sitemap->query->terms( $taxonomy, [ 'count' => true ] );
|
||||
|
||||
// We need to temporarily reset the linksPerIndex count here so that we can properly chunk.
|
||||
aioseo()->sitemap->linksPerIndex = aioseo()->options->sitemap->general->linksPerIndex;
|
||||
$chunks = aioseo()->sitemap->helpers->chunkEntries( $terms );
|
||||
aioseo()->sitemap->linksPerIndex = PHP_INT_MAX;
|
||||
|
||||
if ( 1 === count( $chunks ) ) {
|
||||
$filename = "$taxonomy-$sitemapName.xml";
|
||||
$files[ $filename ] = [
|
||||
'total' => $total,
|
||||
'entries' => $chunks[0]
|
||||
];
|
||||
} else {
|
||||
for ( $i = 1; $i <= count( $chunks ); $i++ ) {
|
||||
$filename = "$taxonomy-$sitemapName$i.xml";
|
||||
$files[ $filename ] = [
|
||||
'total' => $total,
|
||||
'entries' => $chunks[ $i - 1 ]
|
||||
];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
$this->writeSitemaps( $files );
|
||||
}
|
||||
|
||||
/**
|
||||
* Writes all sitemap files.
|
||||
*
|
||||
* @since 4.0.0
|
||||
*
|
||||
* @param array $files The sitemap files.
|
||||
* @return void
|
||||
*/
|
||||
public function writeSitemaps( $files ) {
|
||||
$sitemapName = aioseo()->sitemap->helpers->filename();
|
||||
if ( aioseo()->sitemap->indexes ) {
|
||||
$indexes = [];
|
||||
foreach ( $files as $filename => $data ) {
|
||||
if ( empty( $data['entries'] ) ) {
|
||||
continue;
|
||||
}
|
||||
$indexes[] = [
|
||||
'loc' => trailingslashit( home_url() ) . $filename,
|
||||
'lastmod' => array_values( $data['entries'] )[0]['lastmod'],
|
||||
'count' => count( $data['entries'] )
|
||||
];
|
||||
}
|
||||
$files[ "$sitemapName.xml" ] = [
|
||||
'total' => 0,
|
||||
'entries' => $indexes,
|
||||
];
|
||||
foreach ( $files as $filename => $data ) {
|
||||
$this->writeSitemap( $filename, $data['entries'], $data['total'] );
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$content = [];
|
||||
foreach ( $files as $filename => $data ) {
|
||||
foreach ( $data['entries'] as $entry ) {
|
||||
$content[] = $entry;
|
||||
}
|
||||
}
|
||||
$this->writeSitemap( "$sitemapName.xml", $content, count( $content ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Writes a given sitemap file to the root dir.
|
||||
*
|
||||
* Helper function for writeSitemaps().
|
||||
*
|
||||
* @since 4.0.0
|
||||
*
|
||||
* @param string $filename The name of the file.
|
||||
* @param array $entries The sitemap entries for the file.
|
||||
* @return void
|
||||
*/
|
||||
protected function writeSitemap( $filename, $entries, $total = 0 ) {
|
||||
$sitemapName = aioseo()->sitemap->helpers->filename();
|
||||
aioseo()->sitemap->indexName = $filename;
|
||||
if ( "$sitemapName.xml" === $filename && aioseo()->sitemap->indexes ) {
|
||||
// Set index name to root so that we use the right output template.
|
||||
aioseo()->sitemap->indexName = 'root';
|
||||
}
|
||||
|
||||
aioseo()->sitemap->xsl->saveXslData( $filename, $entries, $total );
|
||||
|
||||
ob_start();
|
||||
aioseo()->sitemap->output->output( $entries );
|
||||
aioseo()->addons->doAddonFunction( 'output', 'output', [ $entries, $total ] );
|
||||
$content = ob_get_clean();
|
||||
|
||||
$fs = aioseo()->core->fs;
|
||||
$file = ABSPATH . sanitize_file_name( $filename );
|
||||
$fileExists = $fs->exists( $file );
|
||||
if ( ! $fileExists || $fs->isWritable( $file ) ) {
|
||||
$fs->putContents( $file, $content );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Return an array of sitemap files.
|
||||
*
|
||||
* @since 4.0.0
|
||||
*
|
||||
* @return array An array of files.
|
||||
*/
|
||||
public function files() {
|
||||
require_once ABSPATH . 'wp-admin/includes/file.php';
|
||||
$files = list_files( get_home_path(), 1 );
|
||||
if ( ! count( $files ) ) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$sitemapFiles = [];
|
||||
foreach ( $files as $filename ) {
|
||||
if ( preg_match( '#.*sitemap.*#', (string) $filename ) ) {
|
||||
$sitemapFiles[] = $filename;
|
||||
}
|
||||
}
|
||||
|
||||
return $sitemapFiles;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,638 @@
|
||||
<?php
|
||||
namespace AIOSEO\Plugin\Common\Sitemap;
|
||||
|
||||
// Exit if accessed directly.
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit;
|
||||
}
|
||||
|
||||
/**
|
||||
* Contains general helper methods specific to the sitemap.
|
||||
*
|
||||
* @since 4.0.0
|
||||
*/
|
||||
class Helpers {
|
||||
/**
|
||||
* Used to track the performance of the sitemap.
|
||||
*
|
||||
* @since 4.0.0
|
||||
*
|
||||
* @var array
|
||||
* $memory The peak memory that is required to generate the sitemap.
|
||||
* $time The time that is required to generate the sitemap.
|
||||
*/
|
||||
private $performance;
|
||||
|
||||
/**
|
||||
* Returns the sitemap filename.
|
||||
*
|
||||
* @since 4.0.0
|
||||
*
|
||||
* @param string $type The sitemap type. We pass it in when we need to get the filename for a specific sitemap outside of the context of the sitemap.
|
||||
* @return string The sitemap filename.
|
||||
*/
|
||||
public function filename( $type = '' ) {
|
||||
if ( ! $type ) {
|
||||
$type = isset( aioseo()->sitemap->type ) ? aioseo()->sitemap->type : 'general';
|
||||
}
|
||||
|
||||
return apply_filters( 'aioseo_sitemap_filename', aioseo()->options->sitemap->$type->filename );
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the last modified post.
|
||||
*
|
||||
* @since 4.0.0
|
||||
*
|
||||
* @param array $additionalArgs Any additional arguments for the post query.
|
||||
* @return mixed WP_Post object or false.
|
||||
*/
|
||||
public function lastModifiedPost( $additionalArgs = [] ) {
|
||||
$args = [
|
||||
'post_status' => 'publish',
|
||||
'posts_per_page' => 1,
|
||||
'orderby ' => 'modified',
|
||||
'order' => 'ASC'
|
||||
];
|
||||
|
||||
if ( $additionalArgs ) {
|
||||
foreach ( $additionalArgs as $k => $v ) {
|
||||
$args[ $k ] = $v;
|
||||
}
|
||||
}
|
||||
|
||||
$query = ( new \WP_Query( $args ) );
|
||||
if ( ! $query->post_count ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return $query->posts[0];
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the timestamp of the last modified post.
|
||||
*
|
||||
* @since 4.0.0
|
||||
*
|
||||
* @param array $postTypes The relevant post types.
|
||||
* @param array $additionalArgs Any additional arguments for the post query.
|
||||
* @return string Formatted date string (ISO 8601).
|
||||
*/
|
||||
public function lastModifiedPostTime( $postTypes = [ 'post', 'page' ], $additionalArgs = [] ) {
|
||||
if ( is_array( $postTypes ) ) {
|
||||
$postTypes = implode( "', '", $postTypes );
|
||||
}
|
||||
|
||||
$query = aioseo()->core->db
|
||||
->start( aioseo()->core->db->db->posts . ' as p', true )
|
||||
->select( 'MAX(`p`.`post_modified_gmt`) as last_modified' )
|
||||
->where( 'p.post_status', 'publish' )
|
||||
->whereRaw( "( `p`.`post_type` IN ( '$postTypes' ) )" );
|
||||
|
||||
if ( isset( $additionalArgs['author'] ) ) {
|
||||
$query->where( 'p.post_author', $additionalArgs['author'] );
|
||||
}
|
||||
|
||||
$lastModified = $query->run()
|
||||
->result();
|
||||
|
||||
return ! empty( $lastModified[0]->last_modified )
|
||||
? aioseo()->helpers->dateTimeToIso8601( $lastModified[0]->last_modified )
|
||||
: '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the timestamp of the last modified additional page.
|
||||
*
|
||||
* @since 4.0.0
|
||||
*
|
||||
* @return string Formatted date string (ISO 8601).
|
||||
*/
|
||||
public function lastModifiedAdditionalPagesTime() {
|
||||
$pages = [];
|
||||
if ( 'posts' === get_option( 'show_on_front' ) || ! in_array( 'page', $this->includedPostTypes(), true ) ) {
|
||||
$frontPageId = (int) get_option( 'page_on_front' );
|
||||
$post = aioseo()->helpers->getPost( $frontPageId );
|
||||
$pages[] = $post ? strtotime( $post->post_modified_gmt ) : strtotime( aioseo()->sitemap->helpers->lastModifiedPostTime() );
|
||||
}
|
||||
|
||||
foreach ( aioseo()->options->sitemap->general->additionalPages->pages as $page ) {
|
||||
$additionalPage = json_decode( $page );
|
||||
if ( empty( $additionalPage->url ) ) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$pages[] = strtotime( $additionalPage->lastModified );
|
||||
}
|
||||
|
||||
if ( empty( $pages ) ) {
|
||||
$additionalPages = apply_filters( 'aioseo_sitemap_additional_pages', [] );
|
||||
if ( empty( $additionalPages ) ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$lastModified = 0;
|
||||
$timestamp = time();
|
||||
foreach ( $additionalPages as $page ) {
|
||||
if ( empty( $page['lastmod'] ) ) {
|
||||
continue;
|
||||
}
|
||||
$timestamp = strtotime( $page['lastmod'] );
|
||||
if ( ! $timestamp ) {
|
||||
continue;
|
||||
}
|
||||
if ( $lastModified < $timestamp ) {
|
||||
$lastModified = $timestamp;
|
||||
}
|
||||
}
|
||||
|
||||
return 0 !== $lastModified ? aioseo()->helpers->dateTimeToIso8601( gmdate( 'Y-m-d H:i:s', $timestamp ) ) : false;
|
||||
}
|
||||
|
||||
return aioseo()->helpers->dateTimeToIso8601( gmdate( 'Y-m-d H:i:s', max( $pages ) ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Formats a given image URL for usage in the sitemap.
|
||||
*
|
||||
* @since 4.0.0
|
||||
*
|
||||
* @param string $url The URL.
|
||||
* @return string The formatted URL.
|
||||
*/
|
||||
public function formatUrl( $url ) {
|
||||
// Remove URL parameters.
|
||||
$url = strtok( $url, '?' );
|
||||
$url = htmlspecialchars( $url, ENT_COMPAT, 'UTF-8', false );
|
||||
|
||||
return aioseo()->helpers->makeUrlAbsolute( $url );
|
||||
}
|
||||
|
||||
/**
|
||||
* Logs the performance of the sitemap for debugging purposes.
|
||||
*
|
||||
* @since 4.0.0
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function logPerformance() {
|
||||
// Start logging the performance.
|
||||
if ( ! $this->performance ) {
|
||||
$this->performance['time'] = microtime( true );
|
||||
$this->performance['memory'] = ( memory_get_peak_usage( true ) / 1024 ) / 1024;
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// Stop logging the performance.
|
||||
$time = microtime( true ) - $this->performance['time'];
|
||||
$memory = $this->performance['memory'];
|
||||
$type = aioseo()->sitemap->type;
|
||||
$indexName = aioseo()->sitemap->indexName;
|
||||
error_log( wp_json_encode( "$indexName index of $type sitemap generated in $time seconds using a maximum of $memory mb of memory." ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the post types that should be included in the sitemap.
|
||||
*
|
||||
* @since 4.0.0
|
||||
*
|
||||
* @param boolean $hasArchivesOnly Whether or not to only include post types which have archives.
|
||||
* @return array $postTypes The included post types.
|
||||
*/
|
||||
public function includedPostTypes( $hasArchivesOnly = false ) {
|
||||
if ( aioseo()->options->sitemap->{aioseo()->sitemap->type}->postTypes->all ) {
|
||||
$postTypes = aioseo()->helpers->getPublicPostTypes( true, $hasArchivesOnly );
|
||||
} else {
|
||||
$postTypes = aioseo()->options->sitemap->{aioseo()->sitemap->type}->postTypes->included;
|
||||
}
|
||||
|
||||
if ( ! $postTypes ) {
|
||||
return $postTypes;
|
||||
}
|
||||
|
||||
$options = aioseo()->options->noConflict();
|
||||
$dynamicOptions = aioseo()->dynamicOptions->noConflict();
|
||||
$publicPostTypes = aioseo()->helpers->getPublicPostTypes( true, $hasArchivesOnly );
|
||||
foreach ( $postTypes as $postType ) {
|
||||
// Check if post type is no longer registered.
|
||||
if ( ! in_array( $postType, $publicPostTypes, true ) || ! $dynamicOptions->searchAppearance->postTypes->has( $postType ) ) {
|
||||
$postTypes = aioseo()->helpers->unsetValue( $postTypes, $postType );
|
||||
continue;
|
||||
}
|
||||
|
||||
// Check if post type isn't noindexed.
|
||||
if ( aioseo()->helpers->isPostTypeNoindexed( $postType ) ) {
|
||||
if ( ! $this->checkForIndexedPost( $postType ) ) {
|
||||
$postTypes = aioseo()->helpers->unsetValue( $postTypes, $postType );
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
if (
|
||||
$dynamicOptions->searchAppearance->postTypes->$postType->advanced->robotsMeta->default &&
|
||||
! $options->searchAppearance->advanced->globalRobotsMeta->default &&
|
||||
$options->searchAppearance->advanced->globalRobotsMeta->noindex
|
||||
) {
|
||||
if ( ! $this->checkForIndexedPost( $postType ) ) {
|
||||
$postTypes = aioseo()->helpers->unsetValue( $postTypes, $postType );
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $postTypes;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if any post is explicitly indexed when the post type is noindexed.
|
||||
*
|
||||
* @since 4.0.0
|
||||
*
|
||||
* @param string $postType The post type to check for.
|
||||
* @return bool Whether or not there is an indexed post.
|
||||
*/
|
||||
private function checkForIndexedPost( $postType ) {
|
||||
$db = aioseo()->core->db->noConflict();
|
||||
$posts = $db->start( aioseo()->core->db->db->posts . ' as p', true )
|
||||
->select( 'p.ID' )
|
||||
->join( 'aioseo_posts as ap', '`ap`.`post_id` = `p`.`ID`' )
|
||||
->where( 'p.post_status', 'attachment' === $postType ? 'inherit' : 'publish' )
|
||||
->where( 'p.post_type', $postType )
|
||||
->whereRaw( '( `ap`.`robots_default` = 0 AND `ap`.`robots_noindex` = 0 )' )
|
||||
->limit( 1 )
|
||||
->run()
|
||||
->result();
|
||||
|
||||
if ( $posts && count( $posts ) ) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the taxonomies that should be included in the sitemap.
|
||||
*
|
||||
* @since 4.0.0
|
||||
*
|
||||
* @return array The included taxonomies.
|
||||
*/
|
||||
public function includedTaxonomies() {
|
||||
$taxonomies = [];
|
||||
if ( aioseo()->options->sitemap->{aioseo()->sitemap->type}->taxonomies->all ) {
|
||||
$taxonomies = get_taxonomies();
|
||||
} else {
|
||||
$taxonomies = aioseo()->options->sitemap->{aioseo()->sitemap->type}->taxonomies->included;
|
||||
}
|
||||
|
||||
if ( ! $taxonomies ) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$options = aioseo()->options->noConflict();
|
||||
$dynamicOptions = aioseo()->dynamicOptions->noConflict();
|
||||
$publicTaxonomies = aioseo()->helpers->getPublicTaxonomies( true );
|
||||
foreach ( $taxonomies as $taxonomy ) {
|
||||
if (
|
||||
aioseo()->helpers->isWooCommerceActive() &&
|
||||
aioseo()->helpers->isWooCommerceProductAttribute( $taxonomy )
|
||||
) {
|
||||
$taxonomies = aioseo()->helpers->unsetValue( $taxonomies, $taxonomy );
|
||||
if ( ! in_array( 'product_attributes', $taxonomies, true ) ) {
|
||||
$taxonomies[] = 'product_attributes';
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
// Check if taxonomy is no longer registered.
|
||||
if ( ! in_array( $taxonomy, $publicTaxonomies, true ) || ! $dynamicOptions->searchAppearance->taxonomies->has( $taxonomy ) ) {
|
||||
$taxonomies = aioseo()->helpers->unsetValue( $taxonomies, $taxonomy );
|
||||
continue;
|
||||
}
|
||||
|
||||
// Check if taxonomy isn't noindexed.
|
||||
if ( aioseo()->helpers->isTaxonomyNoindexed( $taxonomy ) ) {
|
||||
$taxonomies = aioseo()->helpers->unsetValue( $taxonomies, $taxonomy );
|
||||
continue;
|
||||
}
|
||||
|
||||
if (
|
||||
$dynamicOptions->searchAppearance->taxonomies->$taxonomy->advanced->robotsMeta->default &&
|
||||
! $options->searchAppearance->advanced->globalRobotsMeta->default &&
|
||||
$options->searchAppearance->advanced->globalRobotsMeta->noindex
|
||||
) {
|
||||
$taxonomies = aioseo()->helpers->unsetValue( $taxonomies, $taxonomy );
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
return $taxonomies;
|
||||
}
|
||||
|
||||
/**
|
||||
* Splits sitemap entries into chuncks based on the max. amount of URLs per index.
|
||||
*
|
||||
* @since 4.0.0
|
||||
*
|
||||
* @param array $entries The sitemap entries.
|
||||
* @return array The chunked sitemap entries.
|
||||
*/
|
||||
public function chunkEntries( $entries ) {
|
||||
return array_chunk( $entries, aioseo()->sitemap->linksPerIndex, true );
|
||||
}
|
||||
|
||||
/**
|
||||
* Formats the last Modified date of a user-submitted additional page as an ISO 8601 date.
|
||||
*
|
||||
* @since 4.0.0
|
||||
*
|
||||
* @param object $page The additional page object.
|
||||
* @return string The formatted datetime.
|
||||
*/
|
||||
public function lastModifiedAdditionalPage( $page ) {
|
||||
return gmdate( 'c', strtotime( (string) $page->lastModified ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a list of excluded post IDs.
|
||||
*
|
||||
* @since 4.0.0
|
||||
*
|
||||
* @return string The excluded IDs.
|
||||
*/
|
||||
public function excludedPosts() {
|
||||
static $excludedPosts = null;
|
||||
if ( null === $excludedPosts ) {
|
||||
$excludedPosts = $this->excludedObjectIds( 'excludePosts' );
|
||||
}
|
||||
|
||||
return $excludedPosts;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a list of excluded term IDs.
|
||||
*
|
||||
* @since 4.0.0
|
||||
*
|
||||
* @return string The excluded IDs.
|
||||
*/
|
||||
public function excludedTerms() {
|
||||
static $excludedTerms = null;
|
||||
if ( null === $excludedTerms ) {
|
||||
$excludedTerms = $this->excludedObjectIds( 'excludeTerms' );
|
||||
}
|
||||
|
||||
return $excludedTerms;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a list of excluded IDs for a given option as a comma separated string.
|
||||
*
|
||||
* Helper method for excludedPosts() and excludedTerms().
|
||||
*
|
||||
* @since 4.0.0
|
||||
* @version 4.4.7 Improved method name.
|
||||
*
|
||||
* @param string $option The option name.
|
||||
* @return string The excluded IDs.
|
||||
*/
|
||||
private function excludedObjectIds( $option ) {
|
||||
$type = aioseo()->sitemap->type;
|
||||
// The RSS Sitemap needs to exclude whatever is excluded in the general sitemap.
|
||||
if ( 'rss' === $type ) {
|
||||
$type = 'general';
|
||||
}
|
||||
|
||||
// Allow WPML to filter out hidden language posts/terms.
|
||||
$hiddenObjectIds = [];
|
||||
if ( aioseo()->helpers->isWpmlActive() ) {
|
||||
$hiddenLanguages = apply_filters( 'wpml_setting', [], 'hidden_languages' );
|
||||
foreach ( $hiddenLanguages as $language ) {
|
||||
$objectTypes = [];
|
||||
if ( 'excludePosts' === $option ) {
|
||||
$objectTypes = aioseo()->sitemap->helpers->includedPostTypes();
|
||||
$objectTypes = array_map( function( $postType ) {
|
||||
return "post_{$postType}";
|
||||
}, $objectTypes );
|
||||
}
|
||||
|
||||
if ( 'excludeTerms' === $option ) {
|
||||
$objectTypes = aioseo()->sitemap->helpers->includedTaxonomies();
|
||||
$objectTypes = array_map( function( $taxonomy ) {
|
||||
return "tax_{$taxonomy}";
|
||||
}, $objectTypes );
|
||||
}
|
||||
|
||||
$dbNoConflict = aioseo()->core->db->noConflict();
|
||||
$rows = $dbNoConflict->start( 'icl_translations' )
|
||||
->select( 'element_id' )
|
||||
->whereIn( 'element_type', $objectTypes )
|
||||
->where( 'language_code', $language )
|
||||
->run()
|
||||
->result();
|
||||
|
||||
$ids = array_map( function( $row ) {
|
||||
return (int) $row->element_id;
|
||||
}, $rows );
|
||||
|
||||
$hiddenObjectIds = array_merge( $hiddenObjectIds, $ids );
|
||||
}
|
||||
}
|
||||
|
||||
$hasFilter = has_filter( 'aioseo_sitemap_' . aioseo()->helpers->toSnakeCase( $option ) );
|
||||
$advanced = aioseo()->options->sitemap->$type->advancedSettings->enable;
|
||||
$excluded = array_merge( $hiddenObjectIds, aioseo()->options->sitemap->{$type}->advancedSettings->{$option} );
|
||||
|
||||
if (
|
||||
! $advanced &&
|
||||
empty( $excluded ) &&
|
||||
! $hasFilter
|
||||
) {
|
||||
return '';
|
||||
}
|
||||
|
||||
$ids = [];
|
||||
foreach ( $excluded as $object ) {
|
||||
if ( is_numeric( $object ) ) {
|
||||
$ids[] = (int) $object;
|
||||
continue;
|
||||
}
|
||||
|
||||
$object = json_decode( $object );
|
||||
if ( is_int( $object->value ) ) {
|
||||
$ids[] = $object->value;
|
||||
}
|
||||
}
|
||||
|
||||
if ( 'excludePosts' === $option ) {
|
||||
$ids = apply_filters( 'aioseo_sitemap_exclude_posts', $ids, $type );
|
||||
}
|
||||
|
||||
if ( 'excludeTerms' === $option ) {
|
||||
$ids = apply_filters( 'aioseo_sitemap_exclude_terms', $ids, $type );
|
||||
}
|
||||
|
||||
return count( $ids ) ? esc_sql( implode( ', ', $ids ) ) : '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the URLs of all active sitemaps.
|
||||
*
|
||||
* @since 4.0.0
|
||||
* @version 4.6.2 Removed the prefix from the list of URLs.
|
||||
*
|
||||
* @return array $urls The sitemap URLs.
|
||||
*/
|
||||
public function getSitemapUrls() {
|
||||
static $urls = [];
|
||||
if ( $urls ) {
|
||||
return $urls;
|
||||
}
|
||||
|
||||
$addonsUrls = array_filter( aioseo()->addons->doAddonFunction( 'helpers', 'getSitemapUrls' ) );
|
||||
|
||||
foreach ( $addonsUrls as $addonUrls ) {
|
||||
$urls = array_merge( $urls, $addonUrls );
|
||||
}
|
||||
|
||||
if ( aioseo()->options->sitemap->general->enable ) {
|
||||
$urls[] = $this->getUrl( 'general' );
|
||||
}
|
||||
if ( aioseo()->options->sitemap->rss->enable ) {
|
||||
$urls[] = $this->getUrl( 'rss' );
|
||||
}
|
||||
|
||||
return $urls;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the URLs of all active sitemaps with the 'Sitemap: ' prefix.
|
||||
*
|
||||
* @since 4.6.2
|
||||
*
|
||||
* @return array $urls The sitemap URLs.
|
||||
*/
|
||||
public function getSitemapUrlsPrefixed() {
|
||||
$urls = $this->getSitemapUrls();
|
||||
|
||||
foreach ( $urls as &$url ) {
|
||||
$url = 'Sitemap: ' . $url;
|
||||
}
|
||||
|
||||
return $urls;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extracts existing sitemap URLs from the robots.txt file.
|
||||
* We need this in case users have existing sitemap directives added to their robots.txt file.
|
||||
*
|
||||
* @since 4.0.10
|
||||
* @version 4.4.9
|
||||
*
|
||||
* @return array The sitemap URLs.
|
||||
*/
|
||||
public function extractSitemapUrlsFromRobotsTxt() {
|
||||
// First, we need to remove our filter, so that it doesn't run unintentionally.
|
||||
remove_filter( 'robots_txt', [ aioseo()->robotsTxt, 'buildRules' ], 10000 );
|
||||
$robotsTxt = apply_filters( 'robots_txt', '', true );
|
||||
add_filter( 'robots_txt', [ aioseo()->robotsTxt, 'buildRules' ], 10000 );
|
||||
|
||||
if ( ! $robotsTxt ) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$lines = explode( "\n", $robotsTxt );
|
||||
if ( ! is_array( $lines ) || ! count( $lines ) ) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return aioseo()->robotsTxt->extractSitemapUrls( $robotsTxt );
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the URL of the given sitemap type.
|
||||
*
|
||||
* @since 4.1.5
|
||||
*
|
||||
* @param string $type The sitemap type.
|
||||
* @return string The sitemap URL.
|
||||
*/
|
||||
public function getUrl( $type ) {
|
||||
$url = home_url( 'sitemap.xml' );
|
||||
|
||||
if ( 'rss' === $type ) {
|
||||
$url = home_url( 'sitemap.rss' );
|
||||
}
|
||||
|
||||
if ( 'general' === $type ) {
|
||||
// Check if user has a custom filename from the V3 migration.
|
||||
$filename = $this->filename( 'general' ) ?: 'sitemap';
|
||||
$url = home_url( $filename . '.xml' );
|
||||
}
|
||||
|
||||
$addon = aioseo()->addons->getLoadedAddon( $type );
|
||||
if ( ! empty( $addon->helpers ) && method_exists( $addon->helpers, 'getUrl' ) ) {
|
||||
$url = $addon->helpers->getUrl();
|
||||
}
|
||||
|
||||
return $url;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns if images should be excluded from the sitemap.
|
||||
*
|
||||
* @since 4.2.2
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function excludeImages() {
|
||||
$shouldExclude = aioseo()->options->sitemap->general->advancedSettings->enable && aioseo()->options->sitemap->general->advancedSettings->excludeImages;
|
||||
|
||||
return apply_filters( 'aioseo_sitemap_exclude_images', $shouldExclude );
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the post types to check against for the author sitemap.
|
||||
*
|
||||
* @since 4.4.4
|
||||
*
|
||||
* @return array The post types.
|
||||
*/
|
||||
public function getAuthorPostTypes() {
|
||||
// By default, WP only considers posts for author archives, but users can include additional post types.
|
||||
$postTypes = [ 'post' ];
|
||||
|
||||
return apply_filters( 'aioseo_sitemap_author_post_types', $postTypes );
|
||||
}
|
||||
|
||||
/**
|
||||
* Decode the Urls from Posts and Terms so they properly show in the Sitemap.
|
||||
*
|
||||
* @since 4.6.9
|
||||
*
|
||||
* @param mixed $data The data to decode.
|
||||
* @return array $result The converted data with decoded URLs.
|
||||
*/
|
||||
public function decodeSitemapEntries( $data ) {
|
||||
$result = [];
|
||||
|
||||
if ( empty( $data ) ) {
|
||||
return $result;
|
||||
}
|
||||
|
||||
// Decode Url to properly show Unicode Characters.
|
||||
foreach ( $data as $item ) {
|
||||
if ( isset( $item['loc'] ) ) {
|
||||
$item['loc'] = aioseo()->helpers->decodeUrl( $item['loc'] );
|
||||
}
|
||||
// This is for the RSS Sitemap.
|
||||
if ( isset( $item['guid'] ) ) {
|
||||
$item['guid'] = aioseo()->helpers->decodeUrl( $item['guid'] );
|
||||
}
|
||||
|
||||
$result[] = $item;
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,148 @@
|
||||
<?php
|
||||
namespace AIOSEO\Plugin\Common\Sitemap\Html;
|
||||
|
||||
// Exit if accessed directly.
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit;
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles the HTML sitemap block.
|
||||
*
|
||||
* @since 4.1.3
|
||||
*/
|
||||
class Block {
|
||||
/**
|
||||
* Class constructor.
|
||||
*
|
||||
* @since 4.1.1
|
||||
*/
|
||||
public function __construct() {
|
||||
add_action( 'init', [ $this, 'register' ] );
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers the block.
|
||||
*
|
||||
* @since 4.1.3
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function register() {
|
||||
aioseo()->blocks->registerBlock(
|
||||
'aioseo/html-sitemap', [
|
||||
'attributes' => [
|
||||
'default' => [
|
||||
'type' => 'boolean',
|
||||
'default' => true
|
||||
],
|
||||
'post_types' => [
|
||||
'type' => 'string',
|
||||
'default' => wp_json_encode( [ 'post', 'page' ] )
|
||||
],
|
||||
'post_types_all' => [
|
||||
'type' => 'boolean',
|
||||
'default' => true
|
||||
],
|
||||
'taxonomies' => [
|
||||
'type' => 'string',
|
||||
'default' => wp_json_encode( [ 'category', 'post_tag' ] )
|
||||
],
|
||||
'taxonomies_all' => [
|
||||
'type' => 'boolean',
|
||||
'default' => true
|
||||
],
|
||||
'show_label' => [
|
||||
'type' => 'boolean',
|
||||
'default' => true
|
||||
],
|
||||
'archives' => [
|
||||
'type' => 'boolean',
|
||||
'default' => false
|
||||
],
|
||||
'publication_date' => [
|
||||
'type' => 'boolean',
|
||||
'default' => true
|
||||
],
|
||||
'nofollow_links' => [
|
||||
'type' => 'boolean',
|
||||
'default' => false
|
||||
],
|
||||
'order_by' => [
|
||||
'type' => 'string',
|
||||
'default' => 'publish_date'
|
||||
],
|
||||
'order' => [
|
||||
'type' => 'string',
|
||||
'default' => 'asc'
|
||||
],
|
||||
'excluded_posts' => [
|
||||
'type' => 'string',
|
||||
'default' => wp_json_encode( [] )
|
||||
],
|
||||
'excluded_terms' => [
|
||||
'type' => 'string',
|
||||
'default' => wp_json_encode( [] )
|
||||
],
|
||||
'is_admin' => [
|
||||
'type' => 'boolean',
|
||||
'default' => false
|
||||
]
|
||||
],
|
||||
'render_callback' => [ $this, 'render' ],
|
||||
'editor_style' => 'aioseo-html-sitemap'
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders the block.
|
||||
*
|
||||
* @since 4.1.3
|
||||
*
|
||||
* @param array $attributes The attributes.
|
||||
* @return string The HTML sitemap code.
|
||||
*/
|
||||
public function render( $attributes ) {
|
||||
if ( ! $attributes['default'] ) {
|
||||
$jsonFields = [ 'post_types', 'taxonomies', 'excluded_posts', 'excluded_terms' ];
|
||||
foreach ( $attributes as $k => $v ) {
|
||||
if ( in_array( $k, $jsonFields, true ) ) {
|
||||
$attributes[ $k ] = json_decode( $v );
|
||||
}
|
||||
}
|
||||
|
||||
$attributes['excluded_posts'] = $this->extractIds( $attributes['excluded_posts'] );
|
||||
$attributes['excluded_terms'] = $this->extractIds( $attributes['excluded_terms'] );
|
||||
|
||||
if ( ! empty( $attributes['post_types_all'] ) ) {
|
||||
$attributes['post_types'] = aioseo()->helpers->getPublicPostTypes( true );
|
||||
}
|
||||
if ( ! empty( $attributes['taxonomies_all'] ) ) {
|
||||
$attributes['taxonomies'] = aioseo()->helpers->getPublicTaxonomies( true );
|
||||
}
|
||||
} else {
|
||||
$attributes = [];
|
||||
}
|
||||
|
||||
$attributes = aioseo()->htmlSitemap->frontend->getAttributes( $attributes );
|
||||
|
||||
return aioseo()->htmlSitemap->frontend->output( false, $attributes );
|
||||
}
|
||||
|
||||
/**
|
||||
* Extracts the IDs from the excluded objects.
|
||||
*
|
||||
* @since 4.1.3
|
||||
*
|
||||
* @param array $objects The objects.
|
||||
* @return array The object IDs.
|
||||
*/
|
||||
private function extractIds( $objects ) {
|
||||
return array_map( function ( $object ) {
|
||||
$object = json_decode( $object );
|
||||
|
||||
return (int) $object->value;
|
||||
}, $objects );
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,111 @@
|
||||
<?php
|
||||
namespace AIOSEO\Plugin\Common\Sitemap\Html;
|
||||
|
||||
// Exit if accessed directly.
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit;
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles the Compact Archive's output.
|
||||
*
|
||||
* @since 4.1.3
|
||||
*/
|
||||
class CompactArchive {
|
||||
/**
|
||||
* The shortcode attributes.
|
||||
*
|
||||
* @since 4.1.3
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
private $attributes;
|
||||
|
||||
/**
|
||||
* Outputs the compact archives sitemap.
|
||||
*
|
||||
* @since 4.1.3
|
||||
*
|
||||
* @param array $attributes The shortcode attributes.
|
||||
* @param boolean $echo Whether the HTML code should be printed or returned.
|
||||
* @return string The HTML for the compact archive.
|
||||
*/
|
||||
public function output( $attributes, $echo = true ) {
|
||||
$dateArchives = ( new Query() )->archives();
|
||||
$this->attributes = $attributes;
|
||||
|
||||
if ( 'asc' === strtolower( $this->attributes['order'] ) ) {
|
||||
$dateArchives = array_reverse( $dateArchives, true );
|
||||
}
|
||||
|
||||
$data = [
|
||||
'dateArchives' => $dateArchives,
|
||||
'lines' => ''
|
||||
];
|
||||
foreach ( $dateArchives as $year => $months ) {
|
||||
$data['lines'] .= $this->generateYearLine( $year, $months ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
|
||||
}
|
||||
|
||||
ob_start();
|
||||
aioseo()->templates->getTemplate( 'sitemap/html/compact-archive.php', $data );
|
||||
$output = ob_get_clean();
|
||||
|
||||
if ( $echo ) {
|
||||
echo $output; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
|
||||
}
|
||||
|
||||
return $output;
|
||||
}
|
||||
// phpcs:enable VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable
|
||||
|
||||
/**
|
||||
* Generates the HTML for a year line.
|
||||
*
|
||||
* @since 4.1.3
|
||||
*
|
||||
* @param int $year The year archive.
|
||||
* @param array $months The month archives for the current year.
|
||||
* @return string The HTML code for the year.
|
||||
*/
|
||||
protected function generateYearLine( $year, $months ) {
|
||||
$html = '<li><strong><a href="' . get_year_link( $year ) . '">' . esc_html( $year ) . '</a>: </strong> ';
|
||||
|
||||
for ( $month = 1; $month <= 12; $month++ ) {
|
||||
$html .= $this->generateMonth( $year, $months, $month );
|
||||
}
|
||||
|
||||
$html .= '</li>' . "\n";
|
||||
|
||||
return wp_kses_post( $html );
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates the HTML for a month.
|
||||
*
|
||||
* @since 4.1.3
|
||||
*
|
||||
* @param int $year The year archive.
|
||||
* @param array $months All month archives for the current year.
|
||||
* @param int $month The month archive.
|
||||
* @return string The HTML code for the month.
|
||||
*/
|
||||
public function generateMonth( $year, $months, $month ) {
|
||||
$hasPosts = isset( $months[ $month ] );
|
||||
$dummyDate = strtotime( "2009/{$month}/25" );
|
||||
$monthAbbrevation = date_i18n( 'M', $dummyDate );
|
||||
|
||||
$html = '<span class="aioseo-empty-month">' . esc_html( $monthAbbrevation ) . '</span> ';
|
||||
if ( $hasPosts ) {
|
||||
$noFollow = filter_var( $this->attributes['nofollow_links'], FILTER_VALIDATE_BOOLEAN );
|
||||
$html = sprintf(
|
||||
'<a href="%1$s" title="%2$s"%3$s>%4$s</a> ',
|
||||
get_month_link( $year, $month ),
|
||||
esc_attr( date_i18n( 'F Y', $dummyDate ) ),
|
||||
$noFollow ? ' rel="nofollow"' : '',
|
||||
esc_html( $monthAbbrevation )
|
||||
);
|
||||
}
|
||||
|
||||
return $html;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,483 @@
|
||||
<?php
|
||||
namespace AIOSEO\Plugin\Common\Sitemap\Html;
|
||||
|
||||
// Exit if accessed directly.
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit;
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles the output of the HTML sitemap.
|
||||
*
|
||||
* @since 4.1.3
|
||||
*/
|
||||
class Frontend {
|
||||
/**
|
||||
* Instance of Query class.
|
||||
*
|
||||
* @since 4.1.3
|
||||
*
|
||||
* @var Query
|
||||
*/
|
||||
public $query;
|
||||
|
||||
/**
|
||||
* The attributes for the block/widget/shortcode.
|
||||
*
|
||||
* @since 4.1.3
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
private $attributes = [];
|
||||
|
||||
/**
|
||||
* Class constructor.
|
||||
*
|
||||
* @since 4.1.3
|
||||
*/
|
||||
public function __construct() {
|
||||
$this->query = new Query();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the attributes.
|
||||
*
|
||||
* @since 4.1.3
|
||||
*
|
||||
* @param array $attributes The user-defined attributes
|
||||
* @return array The defaults with user-defined attributes merged.
|
||||
*/
|
||||
public function getAttributes( $attributes = [] ) {
|
||||
aioseo()->sitemap->type = 'html';
|
||||
|
||||
$defaults = [
|
||||
'label_tag' => 'h4',
|
||||
'show_label' => true,
|
||||
'order' => aioseo()->options->sitemap->html->sortDirection,
|
||||
'order_by' => aioseo()->options->sitemap->html->sortOrder,
|
||||
'nofollow_links' => false,
|
||||
'publication_date' => aioseo()->options->sitemap->html->publicationDate,
|
||||
'archives' => aioseo()->options->sitemap->html->compactArchives,
|
||||
'post_types' => aioseo()->sitemap->helpers->includedPostTypes(),
|
||||
'taxonomies' => aioseo()->sitemap->helpers->includedTaxonomies(),
|
||||
'excluded_posts' => [],
|
||||
'excluded_terms' => [],
|
||||
'is_admin' => false
|
||||
];
|
||||
|
||||
$attributes = shortcode_atts( $defaults, $attributes );
|
||||
$attributes['show_label'] = filter_var( $attributes['show_label'], FILTER_VALIDATE_BOOLEAN );
|
||||
$attributes['nofollow_links'] = filter_var( $attributes['nofollow_links'], FILTER_VALIDATE_BOOLEAN );
|
||||
$attributes['is_admin'] = filter_var( $attributes['is_admin'], FILTER_VALIDATE_BOOLEAN );
|
||||
|
||||
return $attributes;
|
||||
}
|
||||
|
||||
/**
|
||||
* Formats the publish date according to what's set under Settings > General.
|
||||
*
|
||||
* @since 4.1.3
|
||||
*
|
||||
* @param string $date The date that should be formatted.
|
||||
* @return string The formatted date.
|
||||
*/
|
||||
private function formatDate( $date ) {
|
||||
$dateFormat = apply_filters( 'aioseo_html_sitemap_date_format', get_option( 'date_format' ) );
|
||||
|
||||
return date_i18n( $dateFormat, strtotime( $date ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the posts of a given post type that should be included.
|
||||
*
|
||||
* @since 4.1.3
|
||||
*
|
||||
* @param string $postType The post type.
|
||||
* @param array $additionalArgs Additional arguments for the post query (optional).
|
||||
* @return array The post entries.
|
||||
*/
|
||||
private function posts( $postType, $additionalArgs = [] ) {
|
||||
$posts = $this->query->posts( $postType, $additionalArgs );
|
||||
if ( ! $posts ) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$entries = [];
|
||||
foreach ( $posts as $post ) {
|
||||
$entry = [
|
||||
'id' => $post->ID,
|
||||
'title' => get_the_title( $post ),
|
||||
'loc' => get_permalink( $post->ID ),
|
||||
'date' => $this->formatDate( $post->post_date_gmt ),
|
||||
'parent' => ! empty( $post->post_parent ) ? $post->post_parent : null
|
||||
];
|
||||
|
||||
$entries[] = $entry;
|
||||
}
|
||||
|
||||
return apply_filters( 'aioseo_html_sitemap_posts', $entries, $postType );
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the terms of a given taxonomy that should be included.
|
||||
*
|
||||
* @since 4.1.3
|
||||
*
|
||||
* @param string $taxonomy The taxonomy name.
|
||||
* @param array $additionalArgs Additional arguments for the query (optional).
|
||||
* @return array The term entries.
|
||||
*/
|
||||
private function terms( $taxonomy, $additionalArgs = [] ) {
|
||||
$terms = $this->query->terms( $taxonomy, $additionalArgs );
|
||||
if ( ! $terms ) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$entries = [];
|
||||
foreach ( $terms as $term ) {
|
||||
$entries[] = [
|
||||
'id' => $term->term_id,
|
||||
'title' => $term->name,
|
||||
'loc' => get_term_link( $term->term_id ),
|
||||
'parent' => ! empty( $term->parent ) ? $term->parent : null
|
||||
];
|
||||
}
|
||||
|
||||
return apply_filters( 'aioseo_html_sitemap_terms', $entries, $taxonomy );
|
||||
}
|
||||
|
||||
/**
|
||||
* Outputs the sitemap to the frontend.
|
||||
*
|
||||
* @since 4.1.3
|
||||
*
|
||||
* @param bool $echo Whether the sitemap should be printed to the screen.
|
||||
* @param array $attributes The shortcode attributes.
|
||||
* @return string|void The HTML sitemap.
|
||||
*/
|
||||
public function output( $echo = true, $attributes = [] ) {
|
||||
$this->attributes = $attributes;
|
||||
|
||||
if ( ! aioseo()->options->sitemap->html->enable ) {
|
||||
return;
|
||||
}
|
||||
|
||||
aioseo()->sitemap->type = 'html';
|
||||
if ( filter_var( $attributes['archives'], FILTER_VALIDATE_BOOLEAN ) ) {
|
||||
return ( new CompactArchive() )->output( $attributes, $echo );
|
||||
}
|
||||
|
||||
if ( ! empty( $attributes['default'] ) ) {
|
||||
$attributes = $this->getAttributes();
|
||||
}
|
||||
|
||||
$noResultsMessage = esc_html__( 'No posts/terms could be found.', 'all-in-one-seo-pack' );
|
||||
if ( empty( $this->attributes['post_types'] ) && empty( $this->attributes['taxonomies'] ) ) {
|
||||
if ( $echo ) {
|
||||
echo $noResultsMessage; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
|
||||
}
|
||||
|
||||
return $noResultsMessage;
|
||||
}
|
||||
|
||||
// TODO: Consider moving all remaining HTML code below to a dedicated view instead of printing it in PHP.
|
||||
$sitemap = sprintf(
|
||||
'<div class="aioseo-html-sitemap%s">',
|
||||
! $this->attributes['show_label'] ? ' labels-hidden' : ''
|
||||
);
|
||||
|
||||
$sitemap .= '<style>.aioseo-html-sitemap.labels-hidden ul { margin: 0; }</style>';
|
||||
|
||||
$hasPosts = false;
|
||||
$postTypes = $this->getIncludedObjects( $this->attributes['post_types'] );
|
||||
foreach ( $postTypes as $postType ) {
|
||||
if ( 'attachment' === $postType ) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Check if post type is still registered.
|
||||
if ( ! in_array( $postType, aioseo()->helpers->getPublicPostTypes( true ), true ) ) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$posts = $this->posts( $postType, $attributes );
|
||||
if ( empty( $posts ) ) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$hasPosts = true;
|
||||
|
||||
$postTypeObject = get_post_type_object( $postType );
|
||||
$label = ! empty( $postTypeObject->label ) ? $postTypeObject->label : ucfirst( $postType );
|
||||
|
||||
$sitemap .= '<div class="aioseo-html-' . esc_attr( $postType ) . '-sitemap">';
|
||||
$sitemap .= $this->generateLabel( $label );
|
||||
|
||||
if ( is_post_type_hierarchical( $postType ) ) {
|
||||
$sitemap .= $this->generateHierarchicalList( $posts ) . '</div>';
|
||||
if ( $this->attributes['show_label'] ) {
|
||||
$sitemap .= '<br />';
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
$sitemap .= $this->generateList( $posts );
|
||||
if ( $this->attributes['show_label'] ) {
|
||||
$sitemap .= '<br />';
|
||||
}
|
||||
}
|
||||
|
||||
$hasTerms = false;
|
||||
$taxonomies = $this->getIncludedObjects( $this->attributes['taxonomies'], false );
|
||||
foreach ( $taxonomies as $taxonomy ) {
|
||||
// Check if post type is still registered.
|
||||
if ( ! in_array( $taxonomy, aioseo()->helpers->getPublicTaxonomies( true ), true ) ) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$terms = $this->terms( $taxonomy, $attributes );
|
||||
if ( empty( $terms ) ) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$hasTerms = true;
|
||||
|
||||
$taxonomyObject = get_taxonomy( $taxonomy );
|
||||
$label = ! empty( $taxonomyObject->label ) ? $taxonomyObject->label : ucfirst( $taxonomy );
|
||||
|
||||
$sitemap .= '<div class="aioseo-html-' . esc_attr( $taxonomy ) . '-sitemap">';
|
||||
$sitemap .= $this->generateLabel( $label );
|
||||
|
||||
if ( is_taxonomy_hierarchical( $taxonomy ) ) {
|
||||
$sitemap .= $this->generateHierarchicalList( $terms ) . '</div>';
|
||||
if ( $this->attributes['show_label'] ) {
|
||||
$sitemap .= '<br />';
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
$sitemap .= $this->generateList( $terms );
|
||||
if ( $this->attributes['show_label'] ) {
|
||||
$sitemap .= '<br />';
|
||||
}
|
||||
}
|
||||
|
||||
$sitemap .= '</div>';
|
||||
|
||||
// Check if we actually were able to fetch any results.
|
||||
if ( ! $hasPosts && ! $hasTerms ) {
|
||||
$sitemap = $noResultsMessage;
|
||||
}
|
||||
|
||||
if ( $echo ) {
|
||||
echo $sitemap; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
|
||||
}
|
||||
|
||||
return $sitemap;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates the label for a section of the sitemap.
|
||||
*
|
||||
* @since 4.1.3
|
||||
*
|
||||
* @param string $label The label.
|
||||
* @return string The HTML code for the label.
|
||||
*/
|
||||
private function generateLabel( $label ) {
|
||||
$labelTag = ! empty( $this->attributes['label_tag'] ) ? $this->attributes['label_tag'] : 'h4';
|
||||
|
||||
return $this->attributes['show_label']
|
||||
? wp_kses_post( sprintf( '<%2$s>%1$s</%2$s>', $label, $labelTag ) )
|
||||
: '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates the HTML for a non-hierarchical list of objects.
|
||||
*
|
||||
* @since 4.1.3
|
||||
*
|
||||
* @param array $objects The object.
|
||||
* @return string The HTML code.
|
||||
*/
|
||||
private function generateList( $objects ) {
|
||||
$list = '<ul>';
|
||||
foreach ( $objects as $object ) {
|
||||
$list .= $this->generateListItem( $object ) . '</li>';
|
||||
}
|
||||
|
||||
return $list . '</ul></div>';
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates a list item for an object (without the closing tag).
|
||||
* We cannot close it as the caller might need to generate a hierarchical structure inside the list item.
|
||||
*
|
||||
* @since 4.1.3
|
||||
*
|
||||
* @param array $object The object.
|
||||
* @return string The HTML code.
|
||||
*/
|
||||
private function generateListItem( $object ) {
|
||||
$li = '';
|
||||
if ( ! empty( $object['title'] ) ) {
|
||||
$li .= '<li>';
|
||||
|
||||
// add nofollow to the link.
|
||||
if ( filter_var( $this->attributes['nofollow_links'], FILTER_VALIDATE_BOOLEAN ) ) {
|
||||
$li .= sprintf(
|
||||
'<a href="%1$s" %2$s %3$s>',
|
||||
esc_url( $object['loc'] ),
|
||||
'rel="nofollow"',
|
||||
$this->attributes['is_admin'] ? 'target="_blank"' : ''
|
||||
);
|
||||
} else {
|
||||
$li .= sprintf(
|
||||
'<a href="%1$s" %2$s>',
|
||||
esc_url( $object['loc'] ),
|
||||
$this->attributes['is_admin'] ? 'target="_blank"' : ''
|
||||
);
|
||||
}
|
||||
|
||||
$li .= sprintf( '%s', esc_attr( $object['title'] ) );
|
||||
|
||||
// add publication date on the list item.
|
||||
if ( ! empty( $object['date'] ) && filter_var( $this->attributes['publication_date'], FILTER_VALIDATE_BOOLEAN ) ) {
|
||||
$li .= sprintf( ' (%s)', esc_attr( $object['date'] ) );
|
||||
}
|
||||
|
||||
$li .= '</a>';
|
||||
}
|
||||
|
||||
return $li;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates the HTML for a hierarchical list of objects.
|
||||
*
|
||||
* @since 4.1.3
|
||||
*
|
||||
* @param array $objects The objects.
|
||||
* @return string The HTML of the hierarchical objects section.
|
||||
*/
|
||||
private function generateHierarchicalList( $objects ) {
|
||||
if ( empty( $objects ) ) {
|
||||
return '';
|
||||
}
|
||||
|
||||
$objects = $this->buildHierarchicalTree( $objects );
|
||||
|
||||
$list = '<ul>';
|
||||
foreach ( $objects as $object ) {
|
||||
$list .= $this->generateListItem( $object );
|
||||
|
||||
if ( ! empty( $object['children'] ) ) {
|
||||
$list .= $this->generateHierarchicalTree( $object );
|
||||
}
|
||||
$list .= '</li>';
|
||||
}
|
||||
$list .= '</ul>';
|
||||
|
||||
return $list;
|
||||
}
|
||||
|
||||
/**
|
||||
* Recursive helper function for generateHierarchicalList().
|
||||
* Generates hierarchical structure for objects with child objects.
|
||||
*
|
||||
* @since 4.1.3
|
||||
*
|
||||
* @param array $object The object.
|
||||
* @return string The HTML code of the hierarchical tree.
|
||||
*/
|
||||
private function generateHierarchicalTree( $object ) {
|
||||
static $nestedLevel = 0;
|
||||
|
||||
$tree = '<ul>';
|
||||
foreach ( $object['children'] as $child ) {
|
||||
$nestedLevel++;
|
||||
$tree .= $this->generateListItem( $child );
|
||||
if ( ! empty( $child['children'] ) ) {
|
||||
$tree .= $this->generateHierarchicalTree( $child );
|
||||
}
|
||||
$tree .= '</li>';
|
||||
}
|
||||
$tree .= '</ul>';
|
||||
|
||||
return $tree;
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds the structure for hierarchical objects that have a parent.
|
||||
*
|
||||
* @since 4.1.3
|
||||
* @version 4.2.8
|
||||
*
|
||||
* @param array $objects The list of hierarchical objects.
|
||||
* @return array Multidimensional array with the hierarchical structure.
|
||||
*/
|
||||
private function buildHierarchicalTree( $objects ) {
|
||||
$topLevelIds = [];
|
||||
$objects = json_decode( wp_json_encode( $objects ) );
|
||||
|
||||
foreach ( $objects as $listItem ) {
|
||||
|
||||
// Create an array of top level IDs for later reference.
|
||||
if ( empty( $listItem->parent ) ) {
|
||||
array_push( $topLevelIds, $listItem->id );
|
||||
}
|
||||
|
||||
// Create an array of children that belong to the current item.
|
||||
$children = array_filter( $objects, function( $child ) use ( $listItem ) {
|
||||
if ( ! empty( $child->parent ) ) {
|
||||
return absint( $child->parent ) === absint( $listItem->id );
|
||||
}
|
||||
} );
|
||||
|
||||
if ( ! empty( $children ) ) {
|
||||
$listItem->children = $children;
|
||||
}
|
||||
}
|
||||
|
||||
// Remove child objects from the root level since they've all been nested.
|
||||
$objects = array_filter( $objects, function ( $item ) use ( $topLevelIds ) {
|
||||
return in_array( $item->id, $topLevelIds, true );
|
||||
} );
|
||||
|
||||
return array_values( json_decode( wp_json_encode( $objects ), true ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the names of the included post types or taxonomies.
|
||||
*
|
||||
* @since 4.1.3
|
||||
*
|
||||
* @param array|string $objects The included post types/taxonomies.
|
||||
* @param boolean $arePostTypes Whether the objects are post types.
|
||||
* @return array The names of the included post types/taxonomies.
|
||||
*/
|
||||
private function getIncludedObjects( $objects, $arePostTypes = true ) {
|
||||
if ( is_array( $objects ) ) {
|
||||
return $objects;
|
||||
}
|
||||
|
||||
if ( empty( $objects ) ) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$exploded = explode( ',', $objects );
|
||||
$objects = array_map( function( $object ) {
|
||||
return trim( $object );
|
||||
}, $exploded );
|
||||
|
||||
$publicObjects = $arePostTypes
|
||||
? aioseo()->helpers->getPublicPostTypes( true )
|
||||
: aioseo()->helpers->getPublicTaxonomies( true );
|
||||
|
||||
$objects = array_filter( $objects, function( $object ) use ( $publicObjects ) {
|
||||
return in_array( $object, $publicObjects, true );
|
||||
});
|
||||
|
||||
return $objects;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,264 @@
|
||||
<?php
|
||||
namespace AIOSEO\Plugin\Common\Sitemap\Html;
|
||||
|
||||
// Exit if accessed directly.
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit;
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles all queries for the HTML sitemap.
|
||||
*
|
||||
* @since 4.1.3
|
||||
*/
|
||||
class Query {
|
||||
/**
|
||||
* Returns all eligible sitemap entries for a given post type.
|
||||
*
|
||||
* @since 4.1.3
|
||||
*
|
||||
* @param string $postType The post type.
|
||||
* @param array $attributes The attributes.
|
||||
* @return array The post objects.
|
||||
*/
|
||||
public function posts( $postType, $attributes ) {
|
||||
$fields = '`ID`, `post_title`,';
|
||||
$fields .= '`post_parent`, `post_date_gmt`, `post_modified_gmt`';
|
||||
|
||||
$orderBy = '';
|
||||
switch ( $attributes['order_by'] ) {
|
||||
case 'last_updated':
|
||||
$orderBy = 'post_modified_gmt';
|
||||
break;
|
||||
case 'alphabetical':
|
||||
$orderBy = 'post_title';
|
||||
break;
|
||||
case 'id':
|
||||
$orderBy = 'ID';
|
||||
break;
|
||||
case 'publish_date':
|
||||
default:
|
||||
$orderBy = 'post_date_gmt';
|
||||
break;
|
||||
}
|
||||
|
||||
switch ( strtolower( $attributes['order'] ) ) {
|
||||
case 'desc':
|
||||
$orderBy .= ' DESC';
|
||||
break;
|
||||
default:
|
||||
$orderBy .= ' ASC';
|
||||
}
|
||||
|
||||
$query = aioseo()->core->db
|
||||
->start( 'posts' )
|
||||
->select( $fields )
|
||||
->where( 'post_status', 'publish' )
|
||||
->where( 'post_type', $postType );
|
||||
|
||||
$excludedPosts = $this->getExcludedObjects( $attributes );
|
||||
if ( $excludedPosts ) {
|
||||
$query->whereRaw( "( `ID` NOT IN ( $excludedPosts ) )" );
|
||||
}
|
||||
|
||||
$posts = $query->orderBy( $orderBy )
|
||||
->run()
|
||||
->result();
|
||||
|
||||
foreach ( $posts as $post ) {
|
||||
$post->ID = (int) $post->ID;
|
||||
}
|
||||
|
||||
return $posts;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns all eligble sitemap entries for a given taxonomy.
|
||||
*
|
||||
* @since 4.1.3
|
||||
*
|
||||
* @param string $taxonomy The taxonomy name.
|
||||
* @param array $attributes The attributes.
|
||||
* @return array The term objects.
|
||||
*/
|
||||
public function terms( $taxonomy, $attributes = [] ) {
|
||||
$fields = 't.term_id, t.name, tt.parent';
|
||||
$termRelationshipsTable = aioseo()->core->db->db->prefix . 'term_relationships';
|
||||
$termTaxonomyTable = aioseo()->core->db->db->prefix . 'term_taxonomy';
|
||||
|
||||
$orderBy = '';
|
||||
switch ( $attributes['order_by'] ) {
|
||||
case 'alphabetical':
|
||||
$orderBy = 't.name';
|
||||
break;
|
||||
// We can only sort by date after getting the terms.
|
||||
case 'id':
|
||||
case 'publish_date':
|
||||
case 'last_updated':
|
||||
default:
|
||||
$orderBy = 't.term_id';
|
||||
break;
|
||||
}
|
||||
|
||||
switch ( strtolower( $attributes['order'] ) ) {
|
||||
case 'desc':
|
||||
$orderBy .= ' DESC';
|
||||
break;
|
||||
default:
|
||||
$orderBy .= ' ASC';
|
||||
}
|
||||
|
||||
$query = aioseo()->core->db
|
||||
->start( 'terms as t' )
|
||||
->select( $fields )
|
||||
->join( 'term_taxonomy as tt', 't.term_id = tt.term_id' )
|
||||
->whereRaw( "
|
||||
( `t`.`term_id` IN
|
||||
(
|
||||
SELECT `tt`.`term_id`
|
||||
FROM `$termTaxonomyTable` as tt
|
||||
WHERE `tt`.`taxonomy` = '$taxonomy'
|
||||
AND `tt`.`count` > 0
|
||||
)
|
||||
)" );
|
||||
|
||||
$excludedTerms = $this->getExcludedObjects( $attributes, false );
|
||||
if ( $excludedTerms ) {
|
||||
$query->whereRaw("
|
||||
( `t`.`term_id` NOT IN
|
||||
(
|
||||
SELECT `tr`.`term_taxonomy_id`
|
||||
FROM `$termRelationshipsTable` as tr
|
||||
WHERE `tr`.`term_taxonomy_id` IN ( $excludedTerms )
|
||||
)
|
||||
)" );
|
||||
}
|
||||
|
||||
$terms = $query->orderBy( $orderBy )
|
||||
->run()
|
||||
->result();
|
||||
|
||||
foreach ( $terms as $term ) {
|
||||
$term->term_id = (int) $term->term_id;
|
||||
$term->taxonomy = $taxonomy;
|
||||
}
|
||||
|
||||
$shouldSort = false;
|
||||
if ( 'last_updated' === $attributes['order_by'] ) {
|
||||
$shouldSort = true;
|
||||
foreach ( $terms as $term ) {
|
||||
$term->timestamp = strtotime( aioseo()->sitemap->content->getTermLastModified( $term->term_id ) );
|
||||
}
|
||||
}
|
||||
|
||||
if ( 'publish_date' === $attributes['order_by'] ) {
|
||||
$shouldSort = true;
|
||||
foreach ( $terms as $term ) {
|
||||
$term->timestamp = strtotime( $this->getTermPublishDate( $term->term_id ) );
|
||||
}
|
||||
}
|
||||
|
||||
if ( $shouldSort ) {
|
||||
if ( 'asc' === strtolower( $attributes['order'] ) ) {
|
||||
usort( $terms, function( $term1, $term2 ) {
|
||||
return $term1->timestamp > $term2->timestamp ? 1 : 0;
|
||||
} );
|
||||
} else {
|
||||
usort( $terms, function( $term1, $term2 ) {
|
||||
return $term1->timestamp < $term2->timestamp ? 1 : 0;
|
||||
} );
|
||||
}
|
||||
}
|
||||
|
||||
return $terms;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a list of date archives that can be included.
|
||||
*
|
||||
* @since 4.1.3
|
||||
*
|
||||
* @return array The date archives.
|
||||
*/
|
||||
public function archives() {
|
||||
$result = aioseo()->core->db
|
||||
->start( 'posts', false, 'SELECT DISTINCT' )
|
||||
->select( 'YEAR(post_date) AS year, MONTH(post_date) AS month' )
|
||||
->where( 'post_type', 'post' )
|
||||
->where( 'post_status', 'publish' )
|
||||
->whereRaw( "post_password=''" )
|
||||
->orderBy( '`year` DESC, `month` DESC' )
|
||||
->run()
|
||||
->result();
|
||||
|
||||
$dates = [];
|
||||
foreach ( $result as $date ) {
|
||||
$dates[ $date->year ][ $date->month ] = 1;
|
||||
}
|
||||
|
||||
return $dates;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the publish date for a given term.
|
||||
* This is the publish date of the oldest post that is assigned to the term.
|
||||
*
|
||||
* @since 4.1.3
|
||||
*
|
||||
* @param int $termId The term ID.
|
||||
* @return int The publish date timestamp.
|
||||
*/
|
||||
public function getTermPublishDate( $termId ) {
|
||||
$termRelationshipsTable = aioseo()->core->db->db->prefix . 'term_relationships';
|
||||
|
||||
$post = aioseo()->core->db
|
||||
->start( 'posts as p' )
|
||||
->select( 'MIN(`p`.`post_date_gmt`) as publish_date' )
|
||||
->whereRaw( "
|
||||
( `p`.`ID` IN
|
||||
(
|
||||
SELECT `tr`.`object_id`
|
||||
FROM `$termRelationshipsTable` as tr
|
||||
WHERE `tr`.`term_taxonomy_id` = '$termId'
|
||||
)
|
||||
)" )
|
||||
->run()
|
||||
->result();
|
||||
|
||||
return ! empty( $post[0]->publish_date ) ? strtotime( $post[0]->publish_date ) : 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a comma-separated string of excluded object IDs.
|
||||
*
|
||||
* @since 4.1.3
|
||||
*
|
||||
* @param array $attributes The attributes.
|
||||
* @param boolean $posts Whether the objects are posts.
|
||||
* @return string The excluded object IDs.
|
||||
*/
|
||||
private function getExcludedObjects( $attributes, $posts = true ) {
|
||||
$excludedObjects = $posts
|
||||
? aioseo()->sitemap->helpers->excludedPosts()
|
||||
: aioseo()->sitemap->helpers->excludedTerms();
|
||||
$key = $posts ? 'excluded_posts' : 'excluded_terms';
|
||||
|
||||
if ( ! empty( $attributes[ $key ] ) ) {
|
||||
$ids = explode( ',', $excludedObjects );
|
||||
|
||||
$extraIds = [];
|
||||
if ( is_array( $attributes[ $key ] ) ) {
|
||||
$extraIds = $attributes[ $key ];
|
||||
}
|
||||
if ( is_string( $attributes[ $key ] ) ) {
|
||||
$extraIds = array_map( 'trim', explode( ',', $attributes[ $key ] ) );
|
||||
}
|
||||
|
||||
$ids = array_filter( array_merge( $ids, $extraIds ), 'is_numeric' );
|
||||
|
||||
$excludedObjects = esc_sql( implode( ', ', $ids ) );
|
||||
}
|
||||
|
||||
return $excludedObjects;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
<?php
|
||||
namespace AIOSEO\Plugin\Common\Sitemap\Html;
|
||||
|
||||
// Exit if accessed directly.
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit;
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles the HTML sitemap shortcode.
|
||||
*
|
||||
* @since 4.1.3
|
||||
*/
|
||||
class Shortcode {
|
||||
/**
|
||||
* Class constructor.
|
||||
*
|
||||
* @since 4.1.3
|
||||
*/
|
||||
public function __construct() {
|
||||
add_shortcode( 'aioseo_html_sitemap', [ $this, 'render' ] );
|
||||
}
|
||||
|
||||
/**
|
||||
* Shortcode callback.
|
||||
*
|
||||
* @since 4.1.3
|
||||
*
|
||||
* @param array $attributes The shortcode attributes.
|
||||
* @return string|void The HTML sitemap.
|
||||
*/
|
||||
public function render( $attributes ) {
|
||||
$attributes = aioseo()->htmlSitemap->frontend->getAttributes( $attributes );
|
||||
|
||||
return aioseo()->htmlSitemap->frontend->output( false, $attributes );
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,220 @@
|
||||
<?php
|
||||
namespace AIOSEO\Plugin\Common\Sitemap\Html {
|
||||
// Exit if accessed directly.
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit;
|
||||
}
|
||||
|
||||
/**
|
||||
* Main class for the HTML sitemap.
|
||||
*
|
||||
* @since 4.1.3
|
||||
*/
|
||||
class Sitemap {
|
||||
/** Instance of the frontend class.
|
||||
*
|
||||
* @since 4.1.3
|
||||
*
|
||||
* @var Frontend
|
||||
*/
|
||||
public $frontend;
|
||||
|
||||
/**
|
||||
* Instance of the shortcode class.
|
||||
*
|
||||
* @since 4.1.3
|
||||
*
|
||||
* @var Shortcode
|
||||
*/
|
||||
public $shortcode;
|
||||
|
||||
/**
|
||||
* Instance of the block class.
|
||||
*
|
||||
* @since 4.1.3
|
||||
*
|
||||
* @var Block
|
||||
*/
|
||||
public $block;
|
||||
|
||||
/**
|
||||
* Whether the current queried page is the dedicated sitemap page.
|
||||
*
|
||||
* @since 4.1.3
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
public $isDedicatedPage = false;
|
||||
|
||||
/**
|
||||
* Class constructor.
|
||||
*
|
||||
* @since 4.1.3
|
||||
*/
|
||||
public function __construct() {
|
||||
$this->frontend = new Frontend();
|
||||
$this->shortcode = new Shortcode();
|
||||
$this->block = new Block();
|
||||
|
||||
add_action( 'widgets_init', [ $this, 'registerWidget' ] );
|
||||
add_filter( 'aioseo_canonical_url', [ $this, 'getCanonicalUrl' ] );
|
||||
|
||||
if ( ! is_admin() || wp_doing_ajax() || wp_doing_cron() ) {
|
||||
add_action( 'template_redirect', [ $this, 'checkForDedicatedPage' ] );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Register our HTML sitemap widget.
|
||||
*
|
||||
* @since 4.1.3
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function registerWidget() {
|
||||
if ( aioseo()->helpers->canRegisterLegacyWidget( 'aioseo-html-sitemap-widget' ) ) {
|
||||
register_widget( 'AIOSEO\Plugin\Common\Sitemap\Html\Widget' );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether the current request is for our dedicated HTML sitemap page.
|
||||
*
|
||||
* @since 4.1.3
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function checkForDedicatedPage() {
|
||||
if ( ! aioseo()->options->sitemap->html->enable ) {
|
||||
return;
|
||||
}
|
||||
|
||||
global $wp;
|
||||
$sitemapUrl = aioseo()->options->sitemap->html->pageUrl;
|
||||
if ( ! $sitemapUrl || empty( $wp->request ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$sitemapUrl = wp_parse_url( $sitemapUrl );
|
||||
if ( empty( $sitemapUrl['path'] ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$sitemapUrl = trim( $sitemapUrl['path'], '/' );
|
||||
if ( trim( $wp->request, '/' ) === $sitemapUrl ) {
|
||||
$this->isDedicatedPage = true;
|
||||
$this->generatePage();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether the current request is for our dedicated HTML sitemap page.
|
||||
*
|
||||
* @since 4.1.3
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
private function generatePage() {
|
||||
global $wp_query, $wp, $post; // phpcs:ignore Squiz.NamingConventions.ValidVariableName
|
||||
|
||||
$postId = -1337; // Set a negative ID to prevent conflicts with existing posts.
|
||||
$sitemapUrl = aioseo()->options->sitemap->html->pageUrl;
|
||||
$path = trim( wp_parse_url( $sitemapUrl )['path'], '/' );
|
||||
|
||||
$fakePost = new \stdClass();
|
||||
$fakePost->ID = $postId;
|
||||
$fakePost->post_author = 1;
|
||||
$fakePost->post_date = current_time( 'mysql' );
|
||||
$fakePost->post_date_gmt = current_time( 'mysql', 1 );
|
||||
$fakePost->post_title = apply_filters( 'aioseo_html_sitemap_page_title', __( 'Sitemap', 'all-in-one-seo-pack' ) );
|
||||
$fakePost->post_content = '[aioseo_html_sitemap archives=false]';
|
||||
// We're using post instead of page to prevent calls to get_ancestors(), which will trigger errors.
|
||||
// To loead the page template, we set is_page to true on the WP_Query object.
|
||||
$fakePost->post_type = 'post';
|
||||
$fakePost->post_status = 'publish';
|
||||
$fakePost->comment_status = 'closed';
|
||||
$fakePost->ping_status = 'closed';
|
||||
$fakePost->post_name = $path;
|
||||
$fakePost->filter = 'raw'; // Needed to prevent calls to the database when creating the WP_Post object.
|
||||
$postObject = new \WP_Post( $fakePost );
|
||||
|
||||
$post = $postObject;
|
||||
|
||||
// We'll set as much properties on the WP_Query object as we can to prevent conflicts with other plugins/themes.
|
||||
// phpcs:disable Squiz.NamingConventions.ValidVariableName
|
||||
$wp_query->is_404 = false;
|
||||
$wp_query->is_page = true;
|
||||
$wp_query->is_singular = true;
|
||||
$wp_query->post = $postObject;
|
||||
$wp_query->posts = [ $postObject ];
|
||||
$wp_query->queried_object = $postObject;
|
||||
$wp_query->queried_object_id = $postId;
|
||||
$wp_query->found_posts = 1;
|
||||
$wp_query->post_count = 1;
|
||||
$wp_query->max_num_pages = 1;
|
||||
|
||||
unset( $wp_query->query['error'] );
|
||||
$wp_query->query_vars['error'] = '';
|
||||
// phpcs:enable Squiz.NamingConventions.ValidVariableName
|
||||
|
||||
// We need to add the post object to the cache so that get_post() calls don't trigger database calls.
|
||||
wp_cache_add( $postId, $postObject, 'posts' );
|
||||
|
||||
$GLOBALS['wp_query'] = $wp_query; // phpcs:ignore Squiz.NamingConventions.ValidVariableName
|
||||
$wp->register_globals();
|
||||
|
||||
// Setting is_404 is not sufficient, so we still need to change the status code.
|
||||
status_header( 200 );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the canonical URL for the dedicated HTML sitemap page.
|
||||
*
|
||||
* @since 4.5.7
|
||||
*
|
||||
* @param string $originalUrl The canonical URL.
|
||||
* @return string The canonical URL.
|
||||
*/
|
||||
public function getCanonicalUrl( $originalUrl ) {
|
||||
$sitemapOptions = aioseo()->options->sitemap->html;
|
||||
|
||||
if ( ! $sitemapOptions->enable || ! $this->isDedicatedPage ) {
|
||||
return $originalUrl;
|
||||
}
|
||||
|
||||
// If the user has set a custom URL for the sitemap page, use that.
|
||||
if ( $sitemapOptions->pageUrl ) {
|
||||
return $sitemapOptions->pageUrl;
|
||||
}
|
||||
|
||||
// Return the current URL of WP.
|
||||
global $wp;
|
||||
|
||||
return home_url( $wp->request );
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
namespace {
|
||||
// Exit if accessed directly.
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit;
|
||||
}
|
||||
|
||||
if ( ! function_exists( 'aioseo_html_sitemap' ) ) {
|
||||
/**
|
||||
* Global function that can be used to print the HTML sitemap.
|
||||
*
|
||||
* @since 4.1.3
|
||||
*
|
||||
* @param array $attributes User-defined attributes that override the default settings.
|
||||
* @param boolean $echo Whether to echo the output or return it.
|
||||
* @return string The HTML sitemap code.
|
||||
*/
|
||||
function aioseo_html_sitemap( $attributes = [], $echo = true ) {
|
||||
$attributes = aioseo()->htmlSitemap->frontend->getAttributes( $attributes );
|
||||
|
||||
return aioseo()->htmlSitemap->frontend->output( $echo, $attributes );
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,209 @@
|
||||
<?php
|
||||
namespace AIOSEO\Plugin\Common\Sitemap\Html;
|
||||
|
||||
// Exit if accessed directly.
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit;
|
||||
}
|
||||
|
||||
/**
|
||||
* Class Widget.
|
||||
*
|
||||
* @since 4.1.3
|
||||
*/
|
||||
class Widget extends \WP_Widget {
|
||||
/**
|
||||
* The default attributes.
|
||||
*
|
||||
* @since 4.2.7
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
private $defaults = [];
|
||||
|
||||
/**
|
||||
* Class constructor.
|
||||
*
|
||||
* @since 4.1.3
|
||||
*/
|
||||
public function __construct() {
|
||||
// The default widget settings.
|
||||
$this->defaults = [
|
||||
'title' => '',
|
||||
'show_label' => 'on',
|
||||
'archives' => '',
|
||||
'nofollow_links' => '',
|
||||
'order' => 'asc',
|
||||
'order_by' => 'publish_date',
|
||||
'publication_date' => 'on',
|
||||
'post_types' => [ 'post', 'page' ],
|
||||
'taxonomies' => [ 'category', 'post_tag' ],
|
||||
'excluded_posts' => '',
|
||||
'excluded_terms' => ''
|
||||
];
|
||||
|
||||
$widgetSlug = 'aioseo-html-sitemap-widget';
|
||||
$widgetOptions = [
|
||||
'classname' => $widgetSlug,
|
||||
// Translators: The short plugin name ("AIOSEO").
|
||||
'description' => sprintf( esc_html__( '%1$s HTML sitemap widget.', 'all-in-one-seo-pack' ), AIOSEO_PLUGIN_SHORT_NAME )
|
||||
];
|
||||
$controlOptions = [
|
||||
'id_base' => $widgetSlug
|
||||
];
|
||||
|
||||
// Translators: 1 - The plugin short name ("AIOSEO").
|
||||
$name = sprintf( esc_html__( '%1$s - HTML Sitemap', 'all-in-one-seo-pack' ), AIOSEO_PLUGIN_SHORT_NAME );
|
||||
$name .= ' ' . esc_html__( '(legacy)', 'all-in-one-seo-pack' );
|
||||
parent::__construct( $widgetSlug, $name, $widgetOptions, $controlOptions );
|
||||
}
|
||||
|
||||
/**
|
||||
* Callback for the widget.
|
||||
*
|
||||
* @since 4.1.3
|
||||
*
|
||||
* @param array $args The widget arguments.
|
||||
* @param array $instance The widget instance options.
|
||||
* @return void
|
||||
*/
|
||||
public function widget( $args, $instance ) {
|
||||
if ( ! aioseo()->options->sitemap->html->enable ) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Merge with defaults.
|
||||
$instance = wp_parse_args( (array) $instance, $this->defaults );
|
||||
|
||||
echo $args['before_widget']; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
|
||||
|
||||
if ( ! empty( $instance['title'] ) ) {
|
||||
echo $args['before_title'] . apply_filters( 'widget_title', $instance['title'], $instance, $this->id_base ) . $args['after_title']; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped,Generic.Files.LineLength.MaxExceeded
|
||||
}
|
||||
|
||||
$instance = aioseo()->htmlSitemap->frontend->getAttributes( $instance );
|
||||
aioseo()->htmlSitemap->frontend->output( true, $instance );
|
||||
echo $args['after_widget']; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
|
||||
}
|
||||
|
||||
/**
|
||||
* Callback to update the widget options.
|
||||
*
|
||||
* @since 4.1.3
|
||||
*
|
||||
* @param array $newOptions The new options.
|
||||
* @param array $oldOptions The old options.
|
||||
* @return array The new options.
|
||||
*/
|
||||
public function update( $newOptions, $oldOptions ) { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable
|
||||
$settings = [
|
||||
'title',
|
||||
'order',
|
||||
'order_by',
|
||||
'show_label',
|
||||
'publication_date',
|
||||
'archives',
|
||||
'excluded_posts',
|
||||
'excluded_terms'
|
||||
];
|
||||
|
||||
foreach ( $settings as $setting ) {
|
||||
$newOptions[ $setting ] = ! empty( $newOptions[ $setting ] ) ? wp_strip_all_tags( $newOptions[ $setting ] ) : '';
|
||||
}
|
||||
|
||||
$includedPostTypes = [];
|
||||
if ( ! empty( $newOptions['post_types'] ) ) {
|
||||
$postTypes = $this->getPublicPostTypes( true );
|
||||
foreach ( $newOptions['post_types'] as $v ) {
|
||||
if ( is_numeric( $v ) ) {
|
||||
$includedPostTypes[] = $postTypes[ $v ];
|
||||
} else {
|
||||
$includedPostTypes[] = $v;
|
||||
}
|
||||
}
|
||||
}
|
||||
$newOptions['post_types'] = $includedPostTypes;
|
||||
|
||||
$includedTaxonomies = [];
|
||||
if ( ! empty( $newOptions['taxonomies'] ) ) {
|
||||
$taxonomies = aioseo()->helpers->getPublicTaxonomies( true );
|
||||
foreach ( $newOptions['taxonomies'] as $v ) {
|
||||
if ( is_numeric( $v ) ) {
|
||||
$includedTaxonomies[] = $taxonomies[ $v ];
|
||||
} else {
|
||||
$includedTaxonomies[] = $v;
|
||||
}
|
||||
}
|
||||
}
|
||||
$newOptions['taxonomies'] = $includedTaxonomies;
|
||||
|
||||
if ( ! empty( $newOptions['excluded_posts'] ) ) {
|
||||
$newOptions['excluded_posts'] = $this->sanitizeExcludedIds( $newOptions['excluded_posts'] );
|
||||
}
|
||||
|
||||
if ( ! empty( $newOptions['excluded_terms'] ) ) {
|
||||
$newOptions['excluded_terms'] = $this->sanitizeExcludedIds( $newOptions['excluded_terms'] );
|
||||
}
|
||||
|
||||
return $newOptions;
|
||||
}
|
||||
|
||||
/**
|
||||
* Callback for the widgets options form.
|
||||
*
|
||||
* @since 4.1.3
|
||||
*
|
||||
* @param array $instance The widget options.
|
||||
* @return void
|
||||
*/
|
||||
public function form( $instance ) {
|
||||
// phpcs:disable VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable
|
||||
$instance = wp_parse_args( (array) $instance, $this->defaults );
|
||||
$postTypeObjects = $this->getPublicPostTypes();
|
||||
$postTypes = $this->getPublicPostTypes( true );
|
||||
$taxonomyObjects = aioseo()->helpers->getPublicTaxonomies();
|
||||
// phpcs:enable VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable
|
||||
|
||||
include AIOSEO_DIR . '/app/Common/Views/sitemap/html/widget-options.php';
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the public post types (without attachments).
|
||||
*
|
||||
* @since 4.1.3
|
||||
*
|
||||
* @param boolean $namesOnly Whether only the names should be returned.
|
||||
* @return array The public post types.
|
||||
*/
|
||||
private function getPublicPostTypes( $namesOnly = false ) {
|
||||
$postTypes = aioseo()->helpers->getPublicPostTypes( $namesOnly );
|
||||
foreach ( $postTypes as $k => $postType ) {
|
||||
if ( is_array( $postType ) && 'attachment' === $postType['name'] ) {
|
||||
unset( $postTypes[ $k ] );
|
||||
break;
|
||||
}
|
||||
if ( ! is_array( $postType ) && 'attachment' === $postType ) {
|
||||
unset( $postTypes[ $k ] );
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return array_values( $postTypes );
|
||||
}
|
||||
|
||||
/**
|
||||
* Sanitizes the excluded IDs by removing any non-integer values.
|
||||
*
|
||||
* @since 4.1.3
|
||||
*
|
||||
* @param string $ids The IDs as a string, comma-separated.
|
||||
* @return string The sanitized IDs as a string, comma-separated.
|
||||
*/
|
||||
private function sanitizeExcludedIds( $ids ) {
|
||||
$ids = array_map( 'trim', explode( ',', $ids ) );
|
||||
$ids = array_filter( $ids, 'is_numeric' );
|
||||
$ids = esc_sql( implode( ', ', $ids ) );
|
||||
|
||||
return $ids;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,368 @@
|
||||
<?php
|
||||
namespace AIOSEO\Plugin\Common\Sitemap\Image;
|
||||
|
||||
// Exit if accessed directly.
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines which images are included in a post/term.
|
||||
*
|
||||
* @since 4.0.0
|
||||
*/
|
||||
class Image {
|
||||
/**
|
||||
* The image scan action name.
|
||||
*
|
||||
* @since 4.0.13
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
private $imageScanAction = 'aioseo_image_sitemap_scan';
|
||||
|
||||
/**
|
||||
* The supported image extensions.
|
||||
*
|
||||
* @since 4.2.2
|
||||
*
|
||||
* @var array[string]
|
||||
*/
|
||||
public $supportedExtensions = [
|
||||
'gif',
|
||||
'heic',
|
||||
'jpeg',
|
||||
'jpg',
|
||||
'png',
|
||||
'svg',
|
||||
'webp',
|
||||
'ico'
|
||||
];
|
||||
|
||||
/**
|
||||
* The post object.
|
||||
*
|
||||
* @since 4.2.7
|
||||
*
|
||||
* @var \WP_Post
|
||||
*/
|
||||
private $post = null;
|
||||
|
||||
/**
|
||||
* Class constructor.
|
||||
*
|
||||
* @since 4.0.5
|
||||
*/
|
||||
public function __construct() {
|
||||
// Column may not have been created yet.
|
||||
if ( ! aioseo()->core->db->columnExists( 'aioseo_posts', 'image_scan_date' ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
// NOTE: This needs to go above the is_admin check in order for it to run at all.
|
||||
add_action( $this->imageScanAction, [ $this, 'scanPosts' ] );
|
||||
|
||||
// Don't schedule a scan if we are not in the admin.
|
||||
if ( ! is_admin() ) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ( wp_doing_ajax() || wp_doing_cron() ) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Don't schedule a scan if an importer or the V3 migration is running.
|
||||
// We'll do our scans there.
|
||||
if (
|
||||
aioseo()->importExport->isImportRunning() ||
|
||||
aioseo()->migration->isMigrationRunning()
|
||||
) {
|
||||
return;
|
||||
}
|
||||
// Action Scheduler hooks.
|
||||
add_action( 'init', [ $this, 'scheduleScan' ], 3001 );
|
||||
}
|
||||
|
||||
/**
|
||||
* Schedules the image sitemap scan.
|
||||
*
|
||||
* @since 4.0.5
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function scheduleScan() {
|
||||
if (
|
||||
! aioseo()->options->sitemap->general->enable ||
|
||||
aioseo()->sitemap->helpers->excludeImages()
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
aioseo()->actionScheduler->scheduleSingle( $this->imageScanAction, 10 );
|
||||
}
|
||||
|
||||
/**
|
||||
* Scans posts for images.
|
||||
*
|
||||
* @since 4.0.5
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function scanPosts() {
|
||||
if (
|
||||
! aioseo()->options->sitemap->general->enable ||
|
||||
aioseo()->sitemap->helpers->excludeImages()
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
$postsPerScan = apply_filters( 'aioseo_image_sitemap_posts_per_scan', 10 );
|
||||
$postTypes = implode( "', '", aioseo()->helpers->getPublicPostTypes( true ) );
|
||||
|
||||
$posts = aioseo()->core->db
|
||||
->start( aioseo()->core->db->db->posts . ' as p', true )
|
||||
->select( '`p`.`ID`, `p`.`post_type`, `p`.`post_content`, `p`.`post_excerpt`, `p`.`post_modified_gmt`' )
|
||||
->leftJoin( 'aioseo_posts as ap', '`ap`.`post_id` = `p`.`ID`' )
|
||||
->whereRaw( '( `ap`.`id` IS NULL OR `p`.`post_modified_gmt` > `ap`.`image_scan_date` OR `ap`.`image_scan_date` IS NULL )' )
|
||||
->whereRaw( "`p`.`post_status` IN ( 'publish', 'inherit' )" )
|
||||
->whereRaw( "`p`.`post_type` IN ( '$postTypes' )" )
|
||||
->limit( $postsPerScan )
|
||||
->run()
|
||||
->result();
|
||||
|
||||
if ( ! $posts ) {
|
||||
aioseo()->actionScheduler->scheduleSingle( $this->imageScanAction, 15 * MINUTE_IN_SECONDS, [], true );
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
foreach ( $posts as $post ) {
|
||||
$this->scanPost( $post );
|
||||
}
|
||||
|
||||
aioseo()->actionScheduler->scheduleSingle( $this->imageScanAction, 30, [], true );
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the image entries for a given post.
|
||||
*
|
||||
* @since 4.0.0
|
||||
*
|
||||
* @param \WP_Post|int $post The post object or ID.
|
||||
* @return void
|
||||
*/
|
||||
public function scanPost( $post ) {
|
||||
if ( is_numeric( $post ) ) {
|
||||
$post = get_post( $post );
|
||||
}
|
||||
|
||||
$this->post = $post;
|
||||
|
||||
if ( ! empty( $post->post_password ) ) {
|
||||
$this->updatePost( $post->ID );
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if ( 'attachment' === $post->post_type ) {
|
||||
if ( ! wp_attachment_is( 'image', $post->ID ) ) {
|
||||
$this->updatePost( $post->ID );
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$image = $this->buildEntries( [ $post->ID ] );
|
||||
$this->updatePost( $post->ID, $image );
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$images = $this->extract();
|
||||
$images = $this->removeImageDimensions( $images );
|
||||
|
||||
$images = apply_filters( 'aioseo_sitemap_images', $images, $post );
|
||||
|
||||
// Limit to a 1,000 URLs, in accordance to Google's specifications.
|
||||
$images = array_slice( $images, 0, 1000 );
|
||||
$this->updatePost( $post->ID, $this->buildEntries( $images ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the image entries for a given term.
|
||||
*
|
||||
* @since 4.0.0
|
||||
*
|
||||
* @param \WP_Term $term The term object.
|
||||
* @return array The image entries.
|
||||
*/
|
||||
public function term( $term ) {
|
||||
if ( aioseo()->sitemap->helpers->excludeImages() ) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$id = get_term_meta( $term->term_id, 'thumbnail_id', true );
|
||||
if ( ! $id ) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return $this->buildEntries( [ $id ] );
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds the image entries.
|
||||
*
|
||||
* @since 4.0.0
|
||||
*
|
||||
* @param array $images The images, consisting of attachment IDs or external URLs.
|
||||
* @return array The image entries.
|
||||
*/
|
||||
private function buildEntries( $images ) {
|
||||
$entries = [];
|
||||
foreach ( $images as $image ) {
|
||||
$idOrUrl = $this->getImageIdOrUrl( $image );
|
||||
$imageUrl = is_numeric( $idOrUrl ) ? wp_get_attachment_url( $idOrUrl ) : $idOrUrl;
|
||||
$imageUrl = aioseo()->sitemap->helpers->formatUrl( $imageUrl );
|
||||
if ( ! $imageUrl || ! preg_match( $this->getImageExtensionRegexPattern(), (string) $imageUrl ) ) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$entries[ $idOrUrl ] = [ 'image:loc' => $imageUrl ];
|
||||
}
|
||||
|
||||
return array_values( $entries );
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the ID of the image if it's hosted on the site. Otherwise it returns the external URL.
|
||||
*
|
||||
* @since 4.1.3
|
||||
*
|
||||
* @param int|string $image The attachment ID or URL.
|
||||
* @return int|string The attachment ID or URL.
|
||||
*/
|
||||
private function getImageIdOrUrl( $image ) {
|
||||
if ( is_numeric( $image ) ) {
|
||||
return $image;
|
||||
}
|
||||
|
||||
$attachmentId = false;
|
||||
if ( aioseo()->helpers->isValidAttachment( $image ) ) {
|
||||
$attachmentId = aioseo()->helpers->attachmentUrlToPostId( $image );
|
||||
}
|
||||
|
||||
return $attachmentId ? $attachmentId : $image;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extracts all image URls and IDs from the post.
|
||||
*
|
||||
* @since 4.0.0
|
||||
*
|
||||
* @return array The image URLs and IDs.
|
||||
*/
|
||||
private function extract() {
|
||||
$images = [];
|
||||
|
||||
if ( has_post_thumbnail( $this->post ) ) {
|
||||
$images[] = get_the_post_thumbnail_url( $this->post );
|
||||
}
|
||||
|
||||
// Get the galleries here before doShortcodes() runs below to prevent buggy behaviour.
|
||||
// WordPress is supposed to only return the attached images but returns a different result if the shortcode has no valid attributes, so we need to grab them manually.
|
||||
$images = array_merge( $images, $this->getPostGalleryImages() );
|
||||
|
||||
// Now, get the remaining images from image tags in the post content.
|
||||
$parsedPostContent = do_blocks( $this->post->post_content );
|
||||
$parsedPostContent = aioseo()->helpers->doShortcodes( $parsedPostContent, true, $this->post->ID );
|
||||
$parsedPostContent = preg_replace( '/\s\s+/u', ' ', (string) trim( $parsedPostContent ) ); // Trim both internal and external whitespace.
|
||||
|
||||
// Get the images from any third-party plugins/themes that are active.
|
||||
$thirdParty = new ThirdParty( $this->post, $parsedPostContent );
|
||||
$images = array_merge( $images, $thirdParty->extract() );
|
||||
|
||||
preg_match_all( '#<(amp-)?img[^>]+src="([^">]+)"#', (string) $parsedPostContent, $matches );
|
||||
foreach ( $matches[2] as $url ) {
|
||||
$images[] = aioseo()->helpers->makeUrlAbsolute( $url );
|
||||
}
|
||||
|
||||
return array_unique( $images );
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns all images from WP Core post galleries.
|
||||
*
|
||||
* @since 4.2.2
|
||||
*
|
||||
* @return array[string] The image URLs.
|
||||
*/
|
||||
private function getPostGalleryImages() {
|
||||
$images = [];
|
||||
$galleries = get_post_galleries( $this->post, false );
|
||||
foreach ( $galleries as $gallery ) {
|
||||
foreach ( $gallery['src'] as $imageUrl ) {
|
||||
$images[] = $imageUrl;
|
||||
}
|
||||
}
|
||||
|
||||
// Now, get rid of them so that we don't process the shortcodes again.
|
||||
$regex = get_shortcode_regex( [ 'gallery' ] );
|
||||
$this->post->post_content = preg_replace( "/$regex/i", '', (string) $this->post->post_content );
|
||||
|
||||
return $images;
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes image dimensions from the slug.
|
||||
*
|
||||
* @since 4.0.0
|
||||
*
|
||||
* @param array $urls The image URLs.
|
||||
* @return array $preparedUrls The formatted image URLs.
|
||||
*/
|
||||
private function removeImageDimensions( $urls ) {
|
||||
$preparedUrls = [];
|
||||
foreach ( $urls as $url ) {
|
||||
$preparedUrls[] = aioseo()->helpers->removeImageDimensions( $url );
|
||||
}
|
||||
|
||||
return array_unique( array_filter( $preparedUrls ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Stores the image data for a given post in our DB table.
|
||||
*
|
||||
* @since 4.0.5
|
||||
*
|
||||
* @param int $postId The post ID.
|
||||
* @param array $images The images.
|
||||
* @return void
|
||||
*/
|
||||
private function updatePost( $postId, $images = [] ) {
|
||||
$post = \AIOSEO\Plugin\Common\Models\Post::getPost( $postId );
|
||||
$meta = $post->exists() ? [] : aioseo()->migration->meta->getMigratedPostMeta( $postId );
|
||||
$meta['post_id'] = $postId;
|
||||
$meta['images'] = ! empty( $images ) ? $images : null;
|
||||
$meta['image_scan_date'] = gmdate( 'Y-m-d H:i:s' );
|
||||
|
||||
$post->set( $meta );
|
||||
$post->save();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the image extension regex pattern.
|
||||
*
|
||||
* @since 4.2.2
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getImageExtensionRegexPattern() {
|
||||
static $pattern;
|
||||
if ( null !== $pattern ) {
|
||||
return $pattern;
|
||||
}
|
||||
|
||||
$pattern = '/http.*\.(' . implode( '|', $this->supportedExtensions ) . ')$/i';
|
||||
|
||||
return $pattern;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,349 @@
|
||||
<?php
|
||||
namespace AIOSEO\Plugin\Common\Sitemap\Image;
|
||||
|
||||
// Exit if accessed directly.
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit;
|
||||
}
|
||||
|
||||
/**
|
||||
* Holds all code to extract images from third-party content.
|
||||
*
|
||||
* @since 4.2.2
|
||||
*/
|
||||
class ThirdParty {
|
||||
/**
|
||||
* The post object.
|
||||
*
|
||||
* @since 4.2.2
|
||||
*
|
||||
* @var \WP_Post
|
||||
*/
|
||||
private $post;
|
||||
|
||||
/**
|
||||
* The parsed post content.
|
||||
* The post object holds the unparsed content as we need that for Divi.
|
||||
*
|
||||
* @since 4.2.5
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
private $parsedPostContent;
|
||||
|
||||
/**
|
||||
* The image URLs and IDs.
|
||||
*
|
||||
* @since 4.2.2
|
||||
*
|
||||
* @var array[mixed]
|
||||
*/
|
||||
private $images = [];
|
||||
|
||||
/**
|
||||
* Divi shortcodes.
|
||||
*
|
||||
* @since 4.2.3
|
||||
*
|
||||
* @var string[]
|
||||
*/
|
||||
private $shortcodes = [
|
||||
'et_pb_section',
|
||||
'et_pb_column',
|
||||
'et_pb_row',
|
||||
'et_pb_image',
|
||||
'et_pb_gallery',
|
||||
'et_pb_accordion',
|
||||
'et_pb_accordion_item',
|
||||
'et_pb_counters',
|
||||
'et_pb_blurb',
|
||||
'et_pb_cta',
|
||||
'et_pb_code',
|
||||
'et_pb_contact_form',
|
||||
'et_pb_divider',
|
||||
'et_pb_filterable_portfolio',
|
||||
'et_pb_map',
|
||||
'et_pb_number_counter',
|
||||
'et_pb_post_slider',
|
||||
'et_pb_pricing_tables',
|
||||
'et_pb_pricing_table',
|
||||
'et_pb_shop',
|
||||
'et_pb_slider',
|
||||
'et_pb_slide',
|
||||
'et_pb_tabs',
|
||||
'et_pb_tab',
|
||||
'et_pb_text',
|
||||
'et_pb_video',
|
||||
'et_pb_audio',
|
||||
'et_pb_blog',
|
||||
'et_pb_circle_counter',
|
||||
'et_pb_comments',
|
||||
'et_pb_countdown_timer',
|
||||
'et_pb_signup',
|
||||
'et_pb_login',
|
||||
'et_pb_menu',
|
||||
'et_pb_team_member',
|
||||
'et_pb_post_nav',
|
||||
'et_pb_post_title',
|
||||
'et_pb_search',
|
||||
'et_pb_sidebar',
|
||||
'et_pb_social_media_follow',
|
||||
'et_pb_social_media_follow_network',
|
||||
'et_pb_testimonial',
|
||||
'et_pb_toggle',
|
||||
'et_pb_video_slider',
|
||||
'et_pb_video_slider_item',
|
||||
];
|
||||
|
||||
/**
|
||||
* Class constructor.
|
||||
*
|
||||
* @since 4.2.2
|
||||
*
|
||||
* @param \WP_Post $post The post object.
|
||||
* @param string $parsedPostContent The parsed post content.
|
||||
*/
|
||||
public function __construct( $post, $parsedPostContent ) {
|
||||
$this->post = $post;
|
||||
$this->parsedPostContent = $parsedPostContent;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extracts the images from third-party content.
|
||||
*
|
||||
* @since 4.2.2
|
||||
*
|
||||
* @return array[mixed] The image URLs and IDs.
|
||||
*/
|
||||
public function extract() {
|
||||
$integrations = [
|
||||
'acf',
|
||||
'divi',
|
||||
'nextGen',
|
||||
'wooCommerce',
|
||||
'kadenceBlocks'
|
||||
];
|
||||
|
||||
foreach ( $integrations as $integration ) {
|
||||
$this->{$integration}();
|
||||
}
|
||||
|
||||
return $this->images;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extracts image URLs from ACF fields.
|
||||
*
|
||||
* @since 4.2.2
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
private function acf() {
|
||||
if ( ! class_exists( 'ACF' ) || ! function_exists( 'get_fields' ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$fields = get_fields( $this->post->ID );
|
||||
if ( ! $fields ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$images = $this->acfHelper( $fields );
|
||||
$this->images = array_merge( $this->images, $images );
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper function for acf().
|
||||
*
|
||||
* @since 4.2.2
|
||||
*
|
||||
* @param array $fields The ACF fields.
|
||||
* @return array[string] The image URLs or IDs.
|
||||
*/
|
||||
private function acfHelper( $fields ) {
|
||||
$images = [];
|
||||
foreach ( $fields as $value ) {
|
||||
if ( is_array( $value ) ) {
|
||||
// Recursively loop over grouped fields.
|
||||
// We continue on since arrays aren't necessarily groups and might also simply aLready contain the value we're looking for.
|
||||
$images = array_merge( $images, $this->acfHelper( $value ) );
|
||||
|
||||
if ( isset( $value['type'] ) && 'image' !== strtolower( $value['type'] ) ) {
|
||||
$images[] = $value['url'];
|
||||
}
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
// Capture the value if it's an image URL, but not the default thumbnail from ACF.
|
||||
if ( is_string( $value ) && preg_match( aioseo()->sitemap->image->getImageExtensionRegexPattern(), (string) $value ) && ! preg_match( '/media\/default\.png$/i', (string) $value ) ) {
|
||||
$images[] = $value;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Capture the value if it's a numeric image ID, but make sure it's not an array of random field object properties.
|
||||
if (
|
||||
is_numeric( $value ) &&
|
||||
! isset( $fields['ID'] ) &&
|
||||
! isset( $fields['thumbnail'] )
|
||||
) {
|
||||
$images[] = $value;
|
||||
}
|
||||
}
|
||||
|
||||
return $images;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extracts images from Divi shortcodes.
|
||||
*
|
||||
* @since 4.1.8
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
private function divi() {
|
||||
if ( ! defined( 'ET_BUILDER_VERSION' ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$urls = [];
|
||||
$regex = implode( '|', array_map( 'preg_quote', $this->shortcodes ) );
|
||||
|
||||
preg_match_all(
|
||||
"/\[($regex)(?![\w-])([^\]\/]*(?:\/(?!\])[^\]\/]*)*?)(?:(\/)\]|\](?:([^\[]*+(?:\[(?!\/\2\])[^\[]*+)*+)\[\/\2\])?)(\]?)/i",
|
||||
(string) $this->post->post_content,
|
||||
$matches,
|
||||
PREG_SET_ORDER
|
||||
);
|
||||
|
||||
foreach ( $matches as $shortcode ) {
|
||||
$attributes = shortcode_parse_atts( $shortcode[0] );
|
||||
if ( ! empty( $attributes['src'] ) ) {
|
||||
$urls[] = $attributes['src'];
|
||||
}
|
||||
|
||||
if ( ! empty( $attributes['image_src'] ) ) {
|
||||
$urls[] = $attributes['image_src'];
|
||||
}
|
||||
|
||||
if ( ! empty( $attributes['image_url'] ) ) {
|
||||
$urls[] = $attributes['image_url'];
|
||||
}
|
||||
|
||||
if ( ! empty( $attributes['portrait_url'] ) ) {
|
||||
$urls[] = $attributes['portrait_url'];
|
||||
}
|
||||
|
||||
if ( ! empty( $attributes['image'] ) ) {
|
||||
$urls[] = $attributes['image'];
|
||||
}
|
||||
|
||||
if ( ! empty( $attributes['background_image'] ) ) {
|
||||
$urls[] = $attributes['background_image'];
|
||||
}
|
||||
|
||||
if ( ! empty( $attributes['logo'] ) ) {
|
||||
$urls[] = $attributes['logo'];
|
||||
}
|
||||
|
||||
if ( ! empty( $attributes['gallery_ids'] ) ) {
|
||||
$attachmentIds = explode( ',', $attributes['gallery_ids'] );
|
||||
foreach ( $attachmentIds as $attachmentId ) {
|
||||
$urls[] = wp_get_attachment_url( $attachmentId );
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$this->images = array_merge( $this->images, $urls );
|
||||
}
|
||||
|
||||
/**
|
||||
* Extracts the image IDs of more advanced NextGen Pro gallerlies like the Mosaic and Thumbnail Grid.
|
||||
*
|
||||
* @since 4.2.5
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
private function nextGen() {
|
||||
if ( ! defined( 'NGG_PLUGIN_BASENAME' ) && ! defined( 'NGG_PRO_PLUGIN_BASENAME' ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
preg_match_all( '/data-image-id=\"([0-9]*)\"/i', (string) $this->parsedPostContent, $imageIds );
|
||||
if ( ! empty( $imageIds[1] ) ) {
|
||||
$this->images = array_merge( $this->images, $imageIds[1] );
|
||||
}
|
||||
|
||||
// For this specific check, we only want to parse blocks and do not want to run shortcodes because some NextGen blocks (e.g. Mosaic) are parsed into shortcodes.
|
||||
// And after parsing the shortcodes, the attributes we're looking for are gone.
|
||||
$contentWithBlocksParsed = do_blocks( $this->post->post_content );
|
||||
|
||||
$imageIds = [];
|
||||
preg_match_all( '/\[ngg.*src="galleries" ids="(.*?)".*\]/i', (string) $contentWithBlocksParsed, $shortcodes );
|
||||
if ( empty( $shortcodes[1] ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
foreach ( $shortcodes[1] as $shortcode ) {
|
||||
$galleryIds = explode( ',', $shortcode[0] );
|
||||
foreach ( $galleryIds as $galleryId ) {
|
||||
global $nggdb;
|
||||
$galleryImageIds = $nggdb->get_ids_from_gallery( $galleryId );
|
||||
if ( empty( $galleryImageIds ) ) {
|
||||
continue;
|
||||
}
|
||||
|
||||
foreach ( $galleryImageIds as $galleryImageId ) {
|
||||
$image = $nggdb->find_image( $galleryImageId );
|
||||
if ( ! empty( $image ) ) {
|
||||
$imageIds[] = $image->get_permalink();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$this->images = array_merge( $this->images, $imageIds );
|
||||
}
|
||||
|
||||
/**
|
||||
* Extracts the image IDs of WooCommerce product galleries.
|
||||
*
|
||||
* @since 4.1.2
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
private function wooCommerce() {
|
||||
if ( ! aioseo()->helpers->isWooCommerceActive() || 'product' !== $this->post->post_type ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$productImageIds = get_post_meta( $this->post->ID, '_product_image_gallery', true );
|
||||
if ( ! $productImageIds ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$productImageIds = explode( ',', $productImageIds );
|
||||
$this->images = array_merge( $this->images, $productImageIds );
|
||||
}
|
||||
|
||||
/**
|
||||
* Extracts the image IDs of Kadence Block galleries.
|
||||
*
|
||||
* @since 4.4.5
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
private function kadenceBlocks() {
|
||||
if ( ! defined( 'KADENCE_BLOCKS_VERSION' ) ) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$blocks = aioseo()->helpers->parseBlocks( $this->post );
|
||||
|
||||
foreach ( $blocks as $block ) {
|
||||
if ( 'kadence/advancedgallery' === $block['blockName'] && ! empty( $block['attrs']['ids'] ) ) {
|
||||
$this->images = array_merge( $this->images, $block['attrs']['ids'] );
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,240 @@
|
||||
<?php
|
||||
namespace AIOSEO\Plugin\Common\Sitemap;
|
||||
|
||||
// Exit if accessed directly.
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit;
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles sitemap localization logic.
|
||||
*
|
||||
* @since 4.2.1
|
||||
*/
|
||||
class Localization {
|
||||
/**
|
||||
* This is cached so we don't do the lookup each query.
|
||||
*
|
||||
* @since 4.0.0
|
||||
*
|
||||
* @var boolean
|
||||
*/
|
||||
private static $wpml = null;
|
||||
|
||||
/**
|
||||
* Class constructor.
|
||||
*
|
||||
* @since 4.2.1
|
||||
*/
|
||||
public function __construct() {
|
||||
if ( aioseo()->helpers->isWpmlActive() ) {
|
||||
self::$wpml = [
|
||||
'defaultLanguage' => apply_filters( 'wpml_default_language', null ),
|
||||
'activeLanguages' => apply_filters( 'wpml_active_languages', null )
|
||||
];
|
||||
|
||||
if ( apply_filters( 'aioseo_sitemap_localization_disable', '__return_false' ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
add_filter( 'aioseo_sitemap_term', [ $this, 'localizeEntry' ], 10, 4 );
|
||||
add_filter( 'aioseo_sitemap_post', [ $this, 'localizeEntry' ], 10, 4 );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Localize the entries if WPML (or others in the future) are active.
|
||||
*
|
||||
* @since 4.0.0
|
||||
*
|
||||
* @param array $entry The entry.
|
||||
* @param int $entryId The post/term ID.
|
||||
* @param string $objectName The post type or taxonomy name.
|
||||
* @param string $objectType Whether the entry is a post or term.
|
||||
* @return array The entry.
|
||||
*/
|
||||
public function localizeEntry( $entry, $entryId, $objectName, $objectType ) {
|
||||
$elementId = $entryId;
|
||||
$elementType = 'post_' . $objectName;
|
||||
if ( 'term' === $objectType ) {
|
||||
$term = aioseo()->helpers->getTerm( $entryId, $objectName );
|
||||
$elementId = $term->term_taxonomy_id;
|
||||
$elementType = 'tax_' . $objectName;
|
||||
}
|
||||
|
||||
$translationGroupId = apply_filters( 'wpml_element_trid', null, $elementId, $elementType );
|
||||
$translations = apply_filters( 'wpml_get_element_translations', null, $translationGroupId, $elementType );
|
||||
if ( empty( $translations ) ) {
|
||||
return $entry;
|
||||
}
|
||||
|
||||
$entry['languages'] = [];
|
||||
$hiddenLanguages = apply_filters( 'wpml_setting', [], 'hidden_languages' );
|
||||
foreach ( $translations as $translation ) {
|
||||
if (
|
||||
empty( $translation->element_id ) ||
|
||||
! isset( self::$wpml['activeLanguages'][ $translation->language_code ] ) ||
|
||||
in_array( $translation->language_code, $hiddenLanguages, true )
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$currentLanguage = ! empty( self::$wpml['activeLanguages'][ $translation->language_code ] ) ? self::$wpml['activeLanguages'][ $translation->language_code ] : null;
|
||||
$languageCode = ! empty( $currentLanguage['tag'] ) ? $currentLanguage['tag'] : $translation->language_code;
|
||||
|
||||
if ( (int) $elementId === (int) $translation->element_id ) {
|
||||
$entry['language'] = $languageCode;
|
||||
continue;
|
||||
}
|
||||
|
||||
$translatedObjectId = apply_filters( 'wpml_object_id', $entryId, $objectName, false, $translation->language_code );
|
||||
if (
|
||||
( 'post' === $objectType && $this->isExcludedPost( $translatedObjectId ) ) ||
|
||||
( 'term' === $objectType && $this->isExcludedTerm( $translatedObjectId ) )
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if ( 'post' === $objectType ) {
|
||||
$permalink = get_permalink( $translatedObjectId );
|
||||
|
||||
// Special treatment for the home page translations.
|
||||
if ( 'page' === get_option( 'show_on_front' ) && aioseo()->helpers->wpmlIsHomePage( $entryId ) ) {
|
||||
$permalink = aioseo()->helpers->wpmlHomeUrl( $translation->language_code );
|
||||
}
|
||||
} else {
|
||||
$permalink = get_term_link( $translatedObjectId, $objectName );
|
||||
}
|
||||
|
||||
if ( ! empty( $languageCode ) && ! empty( $permalink ) ) {
|
||||
$entry['languages'][] = [
|
||||
'language' => $languageCode,
|
||||
'location' => $permalink
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
// Also include the main page as a translated variant, per Google's specifications, but only if we found at least one other language.
|
||||
if ( ! empty( $entry['language'] ) && ! empty( $entry['languages'] ) ) {
|
||||
$entry['languages'][] = [
|
||||
'language' => $entry['language'],
|
||||
'location' => $entry['loc']
|
||||
];
|
||||
} else {
|
||||
unset( $entry['languages'] );
|
||||
}
|
||||
|
||||
$entry = $this->validateSubentries( $entry );
|
||||
|
||||
return $entry;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates the subentries with translated variants to ensure all required values are set.
|
||||
*
|
||||
* @since 4.2.3
|
||||
*
|
||||
* @param array $entry The entry.
|
||||
* @return array The validated entry.
|
||||
*/
|
||||
private function validateSubentries( $entry ) {
|
||||
if ( ! isset( $entry['languages'] ) ) {
|
||||
return $entry;
|
||||
}
|
||||
|
||||
foreach ( $entry['languages'] as $index => $subentry ) {
|
||||
if ( empty( $subentry['language'] ) || empty( $subentry['location'] ) ) {
|
||||
unset( $entry['languages'][ $index ] );
|
||||
}
|
||||
}
|
||||
|
||||
return $entry;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether the given post should be excluded.
|
||||
*
|
||||
* @since 4.2.4
|
||||
*
|
||||
* @param int $postId The post ID.
|
||||
* @return bool Whether the post should be excluded.
|
||||
*/
|
||||
private function isExcludedPost( $postId ) {
|
||||
static $excludedPostIds = null;
|
||||
if ( null === $excludedPostIds ) {
|
||||
$excludedPostIds = explode( ', ', aioseo()->sitemap->helpers->excludedPosts() );
|
||||
$excludedPostIds = array_map( function ( $postId ) {
|
||||
return (int) $postId;
|
||||
}, $excludedPostIds );
|
||||
}
|
||||
|
||||
if ( in_array( $postId, $excludedPostIds, true ) ) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Let's also check if the post is published and not password-protected.
|
||||
$post = get_post( $postId );
|
||||
if ( ! is_a( $post, 'WP_Post' ) ) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if ( ! empty( $post->post_password ) || 'publish' !== $post->post_status ) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Now, we must also check for noindex.
|
||||
$metaData = aioseo()->meta->metaData->getMetaData( $post );
|
||||
if ( ! empty( $metaData->robots_noindex ) ) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether the given term should be excluded.
|
||||
*
|
||||
* @since 4.2.4
|
||||
*
|
||||
* @param int $termId The term ID.
|
||||
* @return bool Whether the term should be excluded.
|
||||
*/
|
||||
private function isExcludedTerm( $termId ) {
|
||||
static $excludedTermIds = null;
|
||||
if ( null === $excludedTermIds ) {
|
||||
$excludedTermIds = explode( ', ', aioseo()->sitemap->helpers->excludedTerms() );
|
||||
$excludedTermIds = array_map( function ( $termId ) {
|
||||
return (int) $termId;
|
||||
}, $excludedTermIds );
|
||||
}
|
||||
|
||||
if ( in_array( $termId, $excludedTermIds, true ) ) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Now, we must also check for noindex.
|
||||
$term = aioseo()->helpers->getTerm( $termId );
|
||||
if ( ! is_a( $term, 'WP_Term' ) ) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// At least one post must be assigned to the term.
|
||||
$posts = aioseo()->core->db->start( 'term_relationships' )
|
||||
->select( 'object_id' )
|
||||
->where( 'term_taxonomy_id =', $term->term_taxonomy_id )
|
||||
->limit( 1 )
|
||||
->run()
|
||||
->result();
|
||||
|
||||
if ( empty( $posts ) ) {
|
||||
return true;
|
||||
}
|
||||
|
||||
$metaData = aioseo()->meta->metaData->getMetaData( $term );
|
||||
if ( ! empty( $metaData->robots_noindex ) ) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,144 @@
|
||||
<?php
|
||||
namespace AIOSEO\Plugin\Common\Sitemap;
|
||||
|
||||
// Exit if accessed directly.
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit;
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles outputting the sitemap.
|
||||
*
|
||||
* @since 4.0.0
|
||||
*/
|
||||
class Output {
|
||||
/**
|
||||
* Outputs the sitemap.
|
||||
*
|
||||
* @since 4.0.0
|
||||
*
|
||||
* @param array $entries The sitemap entries.
|
||||
* @return void
|
||||
*/
|
||||
public function output( $entries ) { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable
|
||||
if ( ! in_array( aioseo()->sitemap->type, [ 'general', 'rss' ], true ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
// phpcs:disable VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable
|
||||
$entries = aioseo()->sitemap->helpers->decodeSitemapEntries( $entries );
|
||||
$charset = aioseo()->helpers->getCharset();
|
||||
$excludeImages = aioseo()->sitemap->helpers->excludeImages();
|
||||
$generation = ! isset( aioseo()->sitemap->isStatic ) || aioseo()->sitemap->isStatic ? __( 'statically', 'all-in-one-seo-pack' ) : __( 'dynamically', 'all-in-one-seo-pack' );
|
||||
$version = aioseo()->helpers->getAioseoVersion();
|
||||
|
||||
if ( ! empty( $version ) ) {
|
||||
$version = 'v' . $version;
|
||||
}
|
||||
|
||||
echo '<?xml version="1.0" encoding="' . esc_attr( $charset ) . "\"?>\r\n";
|
||||
echo '<!-- ' . sprintf(
|
||||
// Translators: 1 - "statically" or "dynamically", 2 - The date, 3 - The time, 4 - The plugin name ("All in One SEO"), 5 - Currently installed version.
|
||||
esc_html__( 'This sitemap was %1$s generated on %2$s at %3$s by %4$s %5$s - the original SEO plugin for WordPress.', 'all-in-one-seo-pack' ),
|
||||
esc_html( $generation ),
|
||||
esc_html( date_i18n( get_option( 'date_format' ) ) ),
|
||||
esc_html( date_i18n( get_option( 'time_format' ) ) ),
|
||||
esc_html( AIOSEO_PLUGIN_NAME ),
|
||||
esc_html( $version )
|
||||
) . ' -->';
|
||||
|
||||
if ( 'rss' === aioseo()->sitemap->type ) {
|
||||
$xslUrl = home_url() . '/default-sitemap.xsl';
|
||||
|
||||
if ( ! is_multisite() ) {
|
||||
$title = get_bloginfo( 'name' );
|
||||
$description = get_bloginfo( 'blogdescription' );
|
||||
$link = home_url();
|
||||
} else {
|
||||
$title = get_blog_option( get_current_blog_id(), 'blogname' );
|
||||
$description = get_blog_option( get_current_blog_id(), 'blogdescription' );
|
||||
$link = get_blog_option( get_current_blog_id(), 'siteurl' );
|
||||
}
|
||||
|
||||
$ttl = apply_filters( 'aioseo_sitemap_rss_ttl', 60 );
|
||||
|
||||
echo "\r\n\r\n<?xml-stylesheet type=\"text/xsl\" href=\"" . esc_url( $xslUrl ) . "\"?>\r\n";
|
||||
include_once AIOSEO_DIR . '/app/Common/Views/sitemap/xml/rss.php';
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if ( 'root' === aioseo()->sitemap->indexName && aioseo()->sitemap->indexes ) {
|
||||
$xslUrl = add_query_arg( 'sitemap', aioseo()->sitemap->indexName, home_url() . '/default-sitemap.xsl' );
|
||||
|
||||
echo "\r\n\r\n<?xml-stylesheet type=\"text/xsl\" href=\"" . esc_url( $xslUrl ) . "\"?>\r\n";
|
||||
include AIOSEO_DIR . '/app/Common/Views/sitemap/xml/root.php';
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$xslUrl = add_query_arg( 'sitemap', aioseo()->sitemap->indexName, home_url() . '/default-sitemap.xsl' );
|
||||
|
||||
echo "\r\n\r\n<?xml-stylesheet type=\"text/xsl\" href=\"" . esc_url( $xslUrl ) . "\"?>\r\n";
|
||||
include AIOSEO_DIR . '/app/Common/Views/sitemap/xml/default.php';
|
||||
}
|
||||
|
||||
/**
|
||||
* Escapes and echoes the given XML tag value.
|
||||
*
|
||||
* @since 4.0.0
|
||||
*
|
||||
* @param string $value The tag value.
|
||||
* @param bool $wrap Whether the value should we wrapped in a CDATA section.
|
||||
* @return void
|
||||
*/
|
||||
public function escapeAndEcho( $value, $wrap = true ) {
|
||||
$safeText = is_string( $value ) ? wp_check_invalid_utf8( $value, true ) : $value;
|
||||
$isZero = is_numeric( $value ) ? 0 === (int) $value : false;
|
||||
if ( ! $safeText && ! $isZero ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$cdataRegex = '\<\!\[CDATA\[.*?\]\]\>';
|
||||
$regex = "/(?=.*?{$cdataRegex})(?<non_cdata_followed_by_cdata>(.*?))(?<cdata>({$cdataRegex}))|(?<non_cdata>(.*))/sx";
|
||||
|
||||
$safeText = (string) preg_replace_callback(
|
||||
$regex,
|
||||
static function( $matches ) {
|
||||
if ( ! $matches[0] ) {
|
||||
return '';
|
||||
}
|
||||
|
||||
if ( ! empty( $matches['non_cdata'] ) ) {
|
||||
// Escape HTML entities in the non-CDATA section.
|
||||
return _wp_specialchars( $matches['non_cdata'], ENT_XML1 );
|
||||
}
|
||||
|
||||
// Return the CDATA Section unchanged, escape HTML entities in the rest.
|
||||
return _wp_specialchars( $matches['non_cdata_followed_by_cdata'], ENT_XML1 ) . $matches['cdata'];
|
||||
},
|
||||
$safeText
|
||||
);
|
||||
|
||||
$safeText = $safeText ? $safeText : ( $isZero ? $value : '' );
|
||||
|
||||
if ( ! $wrap ) {
|
||||
return print( $safeText ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
|
||||
}
|
||||
|
||||
printf( '<![CDATA[%1$s]]>', $safeText ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the URL for the sitemap stylesheet.
|
||||
*
|
||||
* This is needed for compatibility with multilingual plugins such as WPML.
|
||||
*
|
||||
* @since 4.0.0
|
||||
*
|
||||
* @return string The URL to the sitemap stylesheet.
|
||||
*/
|
||||
private function xslUrl() {
|
||||
return esc_url( apply_filters( 'aioseo_sitemap_xsl_url', aioseo()->helpers->localizedUrl( '/sitemap.xsl' ) ) );
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,235 @@
|
||||
<?php
|
||||
namespace AIOSEO\Plugin\Common\Sitemap;
|
||||
|
||||
// Exit if accessed directly.
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines the priority/frequency.
|
||||
*
|
||||
* @since 4.0.0
|
||||
*/
|
||||
class Priority {
|
||||
/**
|
||||
* Whether the advanced settings are enabled for the sitemap.
|
||||
*
|
||||
* @since 4.0.0
|
||||
*
|
||||
* @var boolean
|
||||
*/
|
||||
private static $advanced;
|
||||
|
||||
/**
|
||||
* The global priority for the page type.
|
||||
*
|
||||
* @since 4.0.0
|
||||
*
|
||||
* @var boolean
|
||||
*/
|
||||
private static $globalPriority = [];
|
||||
|
||||
/**
|
||||
* The global frequency for the page type.
|
||||
*
|
||||
* @since 4.0.0
|
||||
*
|
||||
* @var boolean
|
||||
*/
|
||||
private static $globalFrequency = [];
|
||||
|
||||
/**
|
||||
* Whether or not we have grouped our settings.
|
||||
*
|
||||
* @since 4.0.0
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
private static $grouped = [];
|
||||
|
||||
/**
|
||||
* The current object type priority.
|
||||
*
|
||||
* @since 4.0.0
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
private static $objectTypePriority = [];
|
||||
|
||||
/**
|
||||
* The current object type frequency.
|
||||
*
|
||||
* @since 4.0.0
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
private static $objectTypeFrequency = [];
|
||||
|
||||
/**
|
||||
* Returns the sitemap priority for a given page.
|
||||
*
|
||||
* @since 4.0.0
|
||||
*
|
||||
* @param string $pageType The type of page (e.g. homepage, blog, post, taxonomies, etc.).
|
||||
* @param \stdClass|bool $object The post/term object (optional).
|
||||
* @param string $objectType The post/term object type (optional).
|
||||
* @return float The priority.
|
||||
*/
|
||||
public function priority( $pageType, $object = false, $objectType = '' ) {
|
||||
// Store setting values in static properties so that we can cache them.
|
||||
// Otherwise this has a significant impact on the load time of the sitemap.
|
||||
if ( ! self::$advanced ) {
|
||||
self::$advanced = aioseo()->options->sitemap->general->advancedSettings->enable;
|
||||
}
|
||||
|
||||
if ( ! isset( self::$globalPriority[ $pageType . $objectType ] ) ) {
|
||||
$options = aioseo()->options->noConflict();
|
||||
|
||||
$pageTypeConditional = 'date' === $pageType ? 'archive' : $pageType;
|
||||
self::$globalPriority[ $pageType . $objectType ] = self::$advanced && $options->sitemap->general->advancedSettings->priority->has( $pageTypeConditional )
|
||||
? json_decode( $options->sitemap->general->advancedSettings->priority->$pageTypeConditional->priority )
|
||||
: false;
|
||||
}
|
||||
|
||||
if ( ! isset( self::$grouped[ $pageType . $objectType ] ) ) {
|
||||
$options = aioseo()->options->noConflict();
|
||||
self::$grouped[ $pageType . $objectType ] = self::$advanced &&
|
||||
$options->sitemap->general->advancedSettings->priority->has( $pageType ) &&
|
||||
$options->sitemap->general->advancedSettings->priority->$pageType->has( 'grouped' )
|
||||
? $options->sitemap->general->advancedSettings->priority->$pageType->grouped
|
||||
: true;
|
||||
}
|
||||
|
||||
if ( empty( self::$grouped[ $pageType . $objectType ] ) && self::$advanced ) {
|
||||
if ( ! isset( self::$objectTypePriority[ $pageType . $objectType ] ) ) {
|
||||
$dynamicOptions = aioseo()->dynamicOptions->noConflict();
|
||||
self::$objectTypePriority[ $pageType . $objectType ] = $dynamicOptions->sitemap->priority->has( $pageType ) && $dynamicOptions->sitemap->priority->$pageType->has( $objectType )
|
||||
? json_decode( $dynamicOptions->sitemap->priority->$pageType->$objectType->priority )
|
||||
: false;
|
||||
}
|
||||
}
|
||||
|
||||
$priority = $this->defaultPriority( $pageType );
|
||||
if ( self::$globalPriority[ $pageType . $objectType ] ) {
|
||||
$defaultValue = ! self::$grouped[ $pageType . $objectType ] &&
|
||||
self::$advanced &&
|
||||
! empty( self::$objectTypePriority[ $pageType . $objectType ] )
|
||||
? self::$objectTypePriority[ $pageType . $objectType ]
|
||||
: self::$globalPriority[ $pageType . $objectType ];
|
||||
$priority = 'default' === $defaultValue->value ? $priority : $defaultValue->value;
|
||||
}
|
||||
|
||||
return $priority;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the sitemap frequency for a given page.
|
||||
*
|
||||
* @since 4.0.0
|
||||
*
|
||||
* @param string $pageType The type of page (e.g. homepage, blog, post, taxonomies, etc.).
|
||||
* @param \stdClass|bool $object The post/term object (optional).
|
||||
* @param string $objectType The post/term object type (optional).
|
||||
* @return float The frequency.
|
||||
*/
|
||||
public function frequency( $pageType, $object = false, $objectType = '' ) {
|
||||
// Store setting values in static properties so that we can cache them.
|
||||
// Otherwise this has a significant impact on the load time of the sitemap.
|
||||
if ( ! self::$advanced ) {
|
||||
self::$advanced = aioseo()->options->sitemap->general->advancedSettings->enable;
|
||||
}
|
||||
if ( ! isset( self::$globalFrequency[ $pageType . $objectType ] ) ) {
|
||||
$options = aioseo()->options->noConflict();
|
||||
$pageTypeConditional = 'date' === $pageType ? 'archive' : $pageType;
|
||||
self::$globalFrequency[ $pageType . $objectType ] = self::$advanced && $options->sitemap->general->advancedSettings->priority->has( $pageTypeConditional )
|
||||
? json_decode( $options->sitemap->general->advancedSettings->priority->$pageTypeConditional->frequency )
|
||||
: false;
|
||||
}
|
||||
|
||||
if ( ! isset( self::$grouped[ $pageType . $objectType ] ) ) {
|
||||
$options = aioseo()->options->noConflict();
|
||||
self::$grouped[ $pageType . $objectType ] = self::$advanced &&
|
||||
$options->sitemap->general->advancedSettings->priority->has( $pageType ) &&
|
||||
$options->sitemap->general->advancedSettings->priority->$pageType->has( 'grouped' )
|
||||
? $options->sitemap->general->advancedSettings->priority->$pageType->grouped
|
||||
: true;
|
||||
}
|
||||
|
||||
if ( empty( self::$grouped[ $pageType . $objectType ] ) && self::$advanced ) {
|
||||
if ( ! isset( self::$objectTypeFrequency[ $pageType . $objectType ] ) ) {
|
||||
$dynamicOptions = aioseo()->dynamicOptions->noConflict();
|
||||
|
||||
self::$objectTypeFrequency[ $pageType . $objectType ] = $dynamicOptions->sitemap->priority->has( $pageType ) && $dynamicOptions->sitemap->priority->$pageType->has( $objectType )
|
||||
? json_decode( $dynamicOptions->sitemap->priority->$pageType->$objectType->frequency )
|
||||
: false;
|
||||
}
|
||||
}
|
||||
|
||||
$frequency = $this->defaultFrequency( $pageType );
|
||||
if ( self::$globalFrequency[ $pageType . $objectType ] ) {
|
||||
$defaultValue = ! self::$grouped[ $pageType . $objectType ] &&
|
||||
self::$advanced &&
|
||||
! empty( self::$objectTypeFrequency[ $pageType . $objectType ] )
|
||||
? self::$objectTypeFrequency[ $pageType . $objectType ]
|
||||
: self::$globalFrequency[ $pageType . $objectType ];
|
||||
$frequency = 'default' === $defaultValue->value ? $frequency : $defaultValue->value;
|
||||
}
|
||||
|
||||
return $frequency;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the default priority for the page.
|
||||
*
|
||||
* @since 4.0.0
|
||||
*
|
||||
* @param string $pageType The type of page.
|
||||
* @return float The default priority.
|
||||
*/
|
||||
private function defaultPriority( $pageType ) {
|
||||
$defaults = [
|
||||
'homePage' => 1.0,
|
||||
'blog' => 0.9,
|
||||
'sitemap' => 0.8,
|
||||
'postTypes' => 0.7,
|
||||
'archive' => 0.5,
|
||||
'author' => 0.3,
|
||||
'taxonomies' => 0.3,
|
||||
'other' => 0.5
|
||||
];
|
||||
|
||||
if ( array_key_exists( $pageType, $defaults ) ) {
|
||||
return $defaults[ $pageType ];
|
||||
}
|
||||
|
||||
return $defaults['other'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the default frequency for the page.
|
||||
*
|
||||
* @since 4.0.0
|
||||
*
|
||||
* @param string $pageType The type of page.
|
||||
* @return float The default frequency.
|
||||
*/
|
||||
private function defaultFrequency( $pageType ) {
|
||||
$defaults = [
|
||||
'homePage' => 'always',
|
||||
'sitemap' => 'hourly',
|
||||
'blog' => 'daily',
|
||||
'postTypes' => 'weekly',
|
||||
'author' => 'weekly',
|
||||
'archive' => 'monthly',
|
||||
'taxonomies' => 'monthly',
|
||||
'other' => 'weekly'
|
||||
];
|
||||
|
||||
if ( array_key_exists( $pageType, $defaults ) ) {
|
||||
return $defaults[ $pageType ];
|
||||
}
|
||||
|
||||
return $defaults['other'];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,408 @@
|
||||
<?php
|
||||
namespace AIOSEO\Plugin\Common\Sitemap;
|
||||
|
||||
// Exit if accessed directly.
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit;
|
||||
}
|
||||
|
||||
use AIOSEO\Plugin\Common\Utils as CommonUtils;
|
||||
|
||||
/**
|
||||
* Handles all complex queries for the sitemap.
|
||||
*
|
||||
* @since 4.0.0
|
||||
*/
|
||||
class Query {
|
||||
/**
|
||||
* Returns all eligble sitemap entries for a given post type.
|
||||
*
|
||||
* @since 4.0.0
|
||||
*
|
||||
* @param mixed $postTypes The post type(s). Either a singular string or an array of strings.
|
||||
* @param array $additionalArgs Any additional arguments for the post query.
|
||||
* @return array|int The post objects or the post count.
|
||||
*/
|
||||
public function posts( $postTypes, $additionalArgs = [] ) {
|
||||
$includedPostTypes = $postTypes;
|
||||
$postTypesArray = ! is_array( $postTypes ) ? [ $postTypes ] : $postTypes;
|
||||
if ( is_array( $postTypes ) ) {
|
||||
$includedPostTypes = implode( "', '", $postTypes );
|
||||
}
|
||||
|
||||
if (
|
||||
empty( $includedPostTypes ) ||
|
||||
( 'attachment' === $includedPostTypes && 'disabled' !== aioseo()->dynamicOptions->searchAppearance->postTypes->attachment->redirectAttachmentUrls )
|
||||
) {
|
||||
return [];
|
||||
}
|
||||
|
||||
// Set defaults.
|
||||
$fields = '`p`.`ID`, `p`.`post_title`, `p`.`post_content`, `p`.`post_excerpt`, `p`.`post_type`, `p`.`post_password`, ';
|
||||
$fields .= '`p`.`post_parent`, `p`.`post_date_gmt`, `p`.`post_modified_gmt`, `ap`.`priority`, `ap`.`frequency`';
|
||||
$maxAge = '';
|
||||
|
||||
if ( ! aioseo()->sitemap->helpers->excludeImages() ) {
|
||||
$fields .= ', `ap`.`images`';
|
||||
}
|
||||
|
||||
// Order by highest priority first (highest priority at the top),
|
||||
// then by post modified date (most recently updated at the top).
|
||||
$orderBy = '`ap`.`priority` DESC, `p`.`post_modified_gmt` DESC';
|
||||
|
||||
// Override defaults if passed as additional arg.
|
||||
foreach ( $additionalArgs as $name => $value ) {
|
||||
// Attachments need to be fetched with all their fields because we need to get their post parent further down the line.
|
||||
$$name = esc_sql( $value );
|
||||
if ( 'root' === $name && $value && 'attachment' !== $includedPostTypes ) {
|
||||
$fields = '`p`.`ID`, `p`.`post_type`';
|
||||
}
|
||||
if ( 'count' === $name && $value ) {
|
||||
$fields = 'count(`p`.`ID`) as total';
|
||||
}
|
||||
}
|
||||
|
||||
$query = aioseo()->core->db
|
||||
->start( aioseo()->core->db->db->posts . ' as p', true )
|
||||
->select( $fields )
|
||||
->leftJoin( 'aioseo_posts as ap', '`ap`.`post_id` = `p`.`ID`' )
|
||||
->where( 'p.post_status', 'attachment' === $includedPostTypes ? 'inherit' : 'publish' )
|
||||
->whereRaw( "p.post_type IN ( '$includedPostTypes' )" );
|
||||
|
||||
$homePageId = (int) get_option( 'page_on_front' );
|
||||
|
||||
if ( ! is_array( $postTypes ) ) {
|
||||
if ( ! aioseo()->helpers->isPostTypeNoindexed( $includedPostTypes ) ) {
|
||||
$query->whereRaw( "( `ap`.`robots_noindex` IS NULL OR `ap`.`robots_default` = 1 OR `ap`.`robots_noindex` = 0 OR post_id = $homePageId )" );
|
||||
} else {
|
||||
$query->whereRaw( "( `ap`.`robots_default` = 0 AND `ap`.`robots_noindex` = 0 OR post_id = $homePageId )" );
|
||||
}
|
||||
} else {
|
||||
$robotsMetaSql = [];
|
||||
foreach ( $postTypes as $postType ) {
|
||||
if ( ! aioseo()->helpers->isPostTypeNoindexed( $postType ) ) {
|
||||
$robotsMetaSql[] = "( `p`.`post_type` = '$postType' AND ( `ap`.`robots_noindex` IS NULL OR `ap`.`robots_default` = 1 OR `ap`.`robots_noindex` = 0 OR post_id = $homePageId ) )";
|
||||
} else {
|
||||
$robotsMetaSql[] = "( `p`.`post_type` = '$postType' AND ( `ap`.`robots_default` = 0 AND `ap`.`robots_noindex` = 0 OR post_id = $homePageId ) )";
|
||||
}
|
||||
}
|
||||
$query->whereRaw( '( ' . implode( ' OR ', $robotsMetaSql ) . ' )' );
|
||||
}
|
||||
|
||||
$excludedPosts = aioseo()->sitemap->helpers->excludedPosts();
|
||||
|
||||
if ( $excludedPosts ) {
|
||||
$query->whereRaw( "( `p`.`ID` NOT IN ( $excludedPosts ) OR post_id = $homePageId )" );
|
||||
}
|
||||
|
||||
// Exclude posts assigned to excluded terms.
|
||||
$excludedTerms = aioseo()->sitemap->helpers->excludedTerms();
|
||||
if ( $excludedTerms ) {
|
||||
$termRelationshipsTable = aioseo()->core->db->db->prefix . 'term_relationships';
|
||||
$query->whereRaw("
|
||||
( `p`.`ID` NOT IN
|
||||
(
|
||||
SELECT `tr`.`object_id`
|
||||
FROM `$termRelationshipsTable` as tr
|
||||
WHERE `tr`.`term_taxonomy_id` IN ( $excludedTerms )
|
||||
)
|
||||
)" );
|
||||
}
|
||||
|
||||
if ( $maxAge ) {
|
||||
$query->whereRaw( "( `p`.`post_date_gmt` >= '$maxAge' )" );
|
||||
}
|
||||
|
||||
if (
|
||||
'rss' === aioseo()->sitemap->type ||
|
||||
(
|
||||
aioseo()->sitemap->indexes &&
|
||||
empty( $additionalArgs['root'] ) &&
|
||||
empty( $additionalArgs['count'] )
|
||||
)
|
||||
) {
|
||||
$query->limit( aioseo()->sitemap->linksPerIndex, aioseo()->sitemap->offset );
|
||||
}
|
||||
|
||||
$isStaticHomepage = 'page' === get_option( 'show_on_front' );
|
||||
if ( $isStaticHomepage ) {
|
||||
$excludedPostIds = array_map( 'intval', explode( ',', $excludedPosts ) );
|
||||
$blogPageId = (int) get_option( 'page_for_posts' );
|
||||
|
||||
if ( in_array( 'page', $postTypesArray, true ) ) {
|
||||
// Exclude the blog page from the pages post type.
|
||||
if ( $blogPageId ) {
|
||||
$query->whereRaw( "`p`.`ID` != $blogPageId" );
|
||||
}
|
||||
|
||||
// Custom order by statement to always move the home page to the top.
|
||||
if ( $homePageId ) {
|
||||
$orderBy = "case when `p`.`ID` = $homePageId then 0 else 1 end, $orderBy";
|
||||
}
|
||||
}
|
||||
|
||||
// Include the blog page in the posts post type unless manually excluded.
|
||||
if (
|
||||
$blogPageId &&
|
||||
! in_array( $blogPageId, $excludedPostIds, true ) &&
|
||||
in_array( 'post', $postTypesArray, true )
|
||||
) {
|
||||
// We are using a database class hack to get in an OR clause to
|
||||
// bypass all the other WHERE statements and just include the
|
||||
// blog page ID manually.
|
||||
$query->whereRaw( "1=1 OR `p`.`ID` = $blogPageId" );
|
||||
|
||||
// Custom order by statement to always move the blog posts page to the top.
|
||||
$orderBy = "case when `p`.`ID` = $blogPageId then 0 else 1 end, $orderBy";
|
||||
}
|
||||
}
|
||||
|
||||
$query->orderBy( $orderBy );
|
||||
$query = $this->filterPostQuery( $query, $postTypes );
|
||||
|
||||
// Return the total if we are just counting the posts.
|
||||
if ( ! empty( $additionalArgs['count'] ) ) {
|
||||
return (int) $query->run( true, 'var' )
|
||||
->result();
|
||||
}
|
||||
|
||||
$posts = $query->run()
|
||||
->result();
|
||||
|
||||
// Convert ID from string to int.
|
||||
foreach ( $posts as $post ) {
|
||||
$post->ID = (int) $post->ID;
|
||||
}
|
||||
|
||||
return $this->filterPosts( $posts );
|
||||
}
|
||||
|
||||
/**
|
||||
* Filters the post query.
|
||||
*
|
||||
* @since 4.1.4
|
||||
*
|
||||
* @param \AIOSEO\Plugin\Common\Utils\Database $query The query.
|
||||
* @param string $postType The post type.
|
||||
* @return \AIOSEO\Plugin\Common\Utils\Database The filtered query.
|
||||
*/
|
||||
private function filterPostQuery( $query, $postType ) {
|
||||
switch ( $postType ) {
|
||||
case 'product':
|
||||
return $this->excludeHiddenProducts( $query );
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
return $query;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a condition to the query to exclude hidden WooCommerce products.
|
||||
*
|
||||
* @since 4.1.4
|
||||
*
|
||||
* @param \AIOSEO\Plugin\Common\Utils\Database $query The query.
|
||||
* @return \AIOSEO\Plugin\Common\Utils\Database The filtered query.
|
||||
*/
|
||||
private function excludeHiddenProducts( $query ) {
|
||||
if (
|
||||
! aioseo()->helpers->isWooCommerceActive() ||
|
||||
! apply_filters( 'aioseo_sitemap_woocommerce_exclude_hidden_products', true )
|
||||
) {
|
||||
return $query;
|
||||
}
|
||||
|
||||
static $hiddenProductIds = null;
|
||||
if ( null === $hiddenProductIds ) {
|
||||
$tempDb = new CommonUtils\Database();
|
||||
$hiddenProducts = $tempDb->start( 'term_relationships as tr' )
|
||||
->select( 'tr.object_id' )
|
||||
->join( 'term_taxonomy as tt', 'tr.term_taxonomy_id = tt.term_taxonomy_id' )
|
||||
->join( 'terms as t', 'tt.term_id = t.term_id' )
|
||||
->where( 't.name', 'exclude-from-catalog' )
|
||||
->run()
|
||||
->result();
|
||||
|
||||
if ( empty( $hiddenProducts ) ) {
|
||||
return $query;
|
||||
}
|
||||
|
||||
$hiddenProductIds = [];
|
||||
foreach ( $hiddenProducts as $hiddenProduct ) {
|
||||
$hiddenProductIds[] = (int) $hiddenProduct->object_id;
|
||||
}
|
||||
$hiddenProductIds = esc_sql( implode( ', ', $hiddenProductIds ) );
|
||||
}
|
||||
|
||||
$query->whereRaw( "p.ID NOT IN ( $hiddenProductIds )" );
|
||||
|
||||
return $query;
|
||||
}
|
||||
|
||||
/**
|
||||
* Filters the queried posts.
|
||||
*
|
||||
* @since 4.0.0
|
||||
*
|
||||
* @param array $posts The posts.
|
||||
* @return array $remainingPosts The remaining posts.
|
||||
*/
|
||||
public function filterPosts( $posts ) {
|
||||
$remainingPosts = [];
|
||||
foreach ( $posts as $post ) {
|
||||
switch ( $post->post_type ) {
|
||||
case 'attachment':
|
||||
if ( ! $this->isInvalidAttachment( $post ) ) {
|
||||
$remainingPosts[] = $post;
|
||||
}
|
||||
break;
|
||||
default:
|
||||
$remainingPosts[] = $post;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return $remainingPosts;
|
||||
}
|
||||
|
||||
/**
|
||||
* Excludes attachments if their post parent isn't published or parent post type isn't registered anymore.
|
||||
*
|
||||
* @since 4.0.0
|
||||
*
|
||||
* @param Object $post The post.
|
||||
* @return boolean Whether the attachment is invalid.
|
||||
*/
|
||||
private function isInvalidAttachment( $post ) {
|
||||
if ( empty( $post->post_parent ) ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$parent = get_post( $post->post_parent );
|
||||
if ( ! is_object( $parent ) ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (
|
||||
'publish' !== $parent->post_status ||
|
||||
! in_array( $parent->post_type, get_post_types(), true ) ||
|
||||
$parent->post_password
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns all eligible sitemap entries for a given taxonomy.
|
||||
*
|
||||
* @since 4.0.0
|
||||
*
|
||||
* @param string $taxonomy The taxonomy.
|
||||
* @param array $additionalArgs Any additional arguments for the term query.
|
||||
* @return array|int The term objects or the term count.
|
||||
*/
|
||||
public function terms( $taxonomy, $additionalArgs = [] ) {
|
||||
// Set defaults.
|
||||
$fields = 't.term_id';
|
||||
$offset = aioseo()->sitemap->offset;
|
||||
|
||||
// Override defaults if passed as additional arg.
|
||||
foreach ( $additionalArgs as $name => $value ) {
|
||||
$$name = esc_sql( $value );
|
||||
if ( 'root' === $name && $value ) {
|
||||
$fields = 't.term_id, tt.count';
|
||||
}
|
||||
if ( 'count' === $name && $value ) {
|
||||
$fields = 'count(t.term_id) as total';
|
||||
}
|
||||
}
|
||||
|
||||
$termRelationshipsTable = aioseo()->core->db->db->prefix . 'term_relationships';
|
||||
$termTaxonomyTable = aioseo()->core->db->db->prefix . 'term_taxonomy';
|
||||
|
||||
// Include all terms that have assigned posts or whose children have assigned posts.
|
||||
$query = aioseo()->core->db
|
||||
->start( aioseo()->core->db->db->terms . ' as t', true )
|
||||
->select( $fields )
|
||||
->leftJoin( 'term_taxonomy as tt', '`tt`.`term_id` = `t`.`term_id`' )
|
||||
->whereRaw( "
|
||||
( `t`.`term_id` IN
|
||||
(
|
||||
SELECT `tt`.`term_id`
|
||||
FROM `$termTaxonomyTable` as tt
|
||||
WHERE `tt`.`taxonomy` = '$taxonomy'
|
||||
AND
|
||||
(
|
||||
`tt`.`count` > 0 OR
|
||||
EXISTS (
|
||||
SELECT 1
|
||||
FROM `$termTaxonomyTable` as tt2
|
||||
WHERE `tt2`.`parent` = `tt`.`term_id`
|
||||
AND `tt2`.`count` > 0
|
||||
)
|
||||
)
|
||||
)
|
||||
)" );
|
||||
|
||||
$excludedTerms = aioseo()->sitemap->helpers->excludedTerms();
|
||||
if ( $excludedTerms ) {
|
||||
$query->whereRaw("
|
||||
( `t`.`term_id` NOT IN
|
||||
(
|
||||
SELECT `tr`.`term_taxonomy_id`
|
||||
FROM `$termRelationshipsTable` as tr
|
||||
WHERE `tr`.`term_taxonomy_id` IN ( $excludedTerms )
|
||||
)
|
||||
)" );
|
||||
}
|
||||
|
||||
if (
|
||||
aioseo()->sitemap->indexes &&
|
||||
empty( $additionalArgs['root'] ) &&
|
||||
empty( $additionalArgs['count'] )
|
||||
) {
|
||||
$query->limit( aioseo()->sitemap->linksPerIndex, $offset );
|
||||
}
|
||||
|
||||
// Return the total if we are just counting the terms.
|
||||
if ( ! empty( $additionalArgs['count'] ) ) {
|
||||
return (int) $query->run( true, 'var' )
|
||||
->result();
|
||||
}
|
||||
|
||||
$terms = $query->orderBy( '`t`.`term_id` ASC' )
|
||||
->run()
|
||||
->result();
|
||||
|
||||
foreach ( $terms as $term ) {
|
||||
// Convert ID from string to int.
|
||||
$term->term_id = (int) $term->term_id;
|
||||
// Add taxonomy name to object manually instead of querying it to prevent redundant join.
|
||||
$term->taxonomy = $taxonomy;
|
||||
}
|
||||
|
||||
return $terms;
|
||||
}
|
||||
|
||||
/**
|
||||
* Wipes all data and forces the plugin to rescan the site for images.
|
||||
*
|
||||
* @since 4.0.13
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function resetImages() {
|
||||
aioseo()->core->db
|
||||
->update( 'aioseo_posts' )
|
||||
->set(
|
||||
[
|
||||
'images' => null,
|
||||
'image_scan_date' => null
|
||||
]
|
||||
)
|
||||
->run();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,263 @@
|
||||
<?php
|
||||
namespace AIOSEO\Plugin\Common\Sitemap;
|
||||
|
||||
// Exit if accessed directly.
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses the current request and checks whether we need to serve a sitemap or a stylesheet.
|
||||
*
|
||||
* @since 4.2.1
|
||||
*/
|
||||
class RequestParser {
|
||||
/**
|
||||
* The cleaned slug of the current request.
|
||||
*
|
||||
* @since 4.2.1
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public $slug;
|
||||
|
||||
/**
|
||||
* Whether we've checked if the page needs to be redirected.
|
||||
*
|
||||
* @since 4.2.3
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
protected $checkedForRedirects = false;
|
||||
|
||||
/**
|
||||
* CLass constructor.
|
||||
*
|
||||
* @since 4.2.1
|
||||
*/
|
||||
public function __construct() {
|
||||
if ( is_admin() ) {
|
||||
return;
|
||||
}
|
||||
|
||||
add_action( 'parse_request', [ $this, 'checkRequest' ] );
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether we need to serve a sitemap or related stylesheet.
|
||||
*
|
||||
* @since 4.2.1
|
||||
*
|
||||
* @param \WP $wp The main WordPress environment instance.
|
||||
* @return void
|
||||
*/
|
||||
public function checkRequest( $wp ) {
|
||||
$this->slug = $wp->request ?? $this->cleanSlug( $wp->request );
|
||||
if ( ! $this->slug && isset( $_SERVER['REQUEST_URI'] ) ) {
|
||||
// We must fallback to the REQUEST URI in case the site uses plain permalinks.
|
||||
$this->slug = $this->cleanSlug( sanitize_text_field( wp_unslash( $_SERVER['REQUEST_URI'] ) ) );
|
||||
}
|
||||
|
||||
if ( ! $this->slug ) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if we need to remove the trailing slash or redirect another sitemap URL like "wp-sitemap.xml".
|
||||
$this->maybeRedirect();
|
||||
|
||||
$this->checkForXsl();
|
||||
|
||||
if ( aioseo()->options->sitemap->general->enable ) {
|
||||
$this->checkForGeneralSitemap();
|
||||
}
|
||||
|
||||
if ( aioseo()->options->sitemap->rss->enable ) {
|
||||
$this->checkForRssSitemap();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Cleans the slug of the current request before we use it.
|
||||
*
|
||||
* @since 4.2.3
|
||||
*
|
||||
* @param string $slug The slug.
|
||||
* @return string The cleaned slug.
|
||||
*/
|
||||
public function cleanSlug( $slug ) {
|
||||
$slug = strtolower( $slug );
|
||||
$slug = aioseo()->helpers->unleadingSlashIt( $slug );
|
||||
$slug = untrailingslashit( $slug );
|
||||
|
||||
return $slug;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether the general XML sitemap needs to be served.
|
||||
*
|
||||
* @since 4.2.1
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
private function checkForGeneralSitemap() {
|
||||
$fileName = aioseo()->sitemap->helpers->filename( 'general' );
|
||||
$indexesEnabled = aioseo()->options->sitemap->general->indexes;
|
||||
|
||||
if ( ! $indexesEnabled ) {
|
||||
// If indexes are disabled, check for the root index.
|
||||
if ( preg_match( "/^{$fileName}\.xml(\.gz)?$/i", (string) $this->slug, $match ) ) {
|
||||
$this->setContext( 'general', $fileName );
|
||||
aioseo()->sitemap->generate();
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// First, check for the root index.
|
||||
if ( preg_match( "/^{$fileName}\.xml(\.gz)?$/i", (string) $this->slug, $match ) ) {
|
||||
$this->setContext( 'general', $fileName );
|
||||
aioseo()->sitemap->generate();
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (
|
||||
// Now, check for the other indexes.
|
||||
preg_match( "/^(?P<objectName>.+)-{$fileName}\.xml(\.gz)?$/i", (string) $this->slug, $match ) ||
|
||||
preg_match( "/^(?P<objectName>.+)-{$fileName}(?P<pageNumber>\d+)\.xml(\.gz)?$/i", (string) $this->slug, $match )
|
||||
) {
|
||||
$pageNumber = ! empty( $match['pageNumber'] ) ? $match['pageNumber'] : 0;
|
||||
$this->setContext( 'general', $fileName, $match['objectName'], $pageNumber );
|
||||
aioseo()->sitemap->generate();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether the RSS sitemap needs to be served.
|
||||
*
|
||||
* @since 4.2.1
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
private function checkForRssSitemap() {
|
||||
if ( ! preg_match( '/^sitemap(\.latest)?\.rss$/i', (string) $this->slug, $match ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$this->setContext( 'rss' );
|
||||
aioseo()->sitemap->generate();
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if we need to serve a stylesheet.
|
||||
*
|
||||
* @since 4.2.1
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
protected function checkForXsl() {
|
||||
// Trim off the URL params.
|
||||
$newSlug = preg_replace( '/\?.*$/', '', (string) $this->slug );
|
||||
if ( preg_match( '/^default-sitemap\.xsl$/i', (string) $newSlug ) ) {
|
||||
aioseo()->sitemap->xsl->generate();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the context for the requested sitemap.
|
||||
*
|
||||
* @since 4.2.1
|
||||
*
|
||||
* @param string $type The sitemap type (e.g. "general" or "rss").
|
||||
* @param string $fileName The sitemap filename.
|
||||
* @param string $indexName The index name ("root" or an object name like "post", "page", "post_tag", etc.).
|
||||
* @param int $pageNumber The index number.
|
||||
* @return void|never
|
||||
*/
|
||||
public function setContext( $type, $fileName = 'sitemap', $indexName = 'root', $pageNumber = 0 ) {
|
||||
$indexesEnabled = aioseo()->options->sitemap->{$type}->indexes;
|
||||
|
||||
aioseo()->sitemap->type = $type;
|
||||
aioseo()->sitemap->filename = $fileName;
|
||||
aioseo()->sitemap->indexes = $indexesEnabled;
|
||||
aioseo()->sitemap->indexName = $indexName;
|
||||
aioseo()->sitemap->linksPerIndex = aioseo()->options->sitemap->{$type}->linksPerIndex <= 50000 ? aioseo()->options->sitemap->{$type}->linksPerIndex : 50000;
|
||||
aioseo()->sitemap->pageNumber = $pageNumber >= 1 ? $pageNumber - 1 : 0;
|
||||
aioseo()->sitemap->offset = aioseo()->sitemap->linksPerIndex * aioseo()->sitemap->pageNumber;
|
||||
aioseo()->sitemap->isStatic = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Redirects or alters the current request if:
|
||||
* 1. The request includes our deprecated "aiosp_sitemap_path" URL param.
|
||||
* 2. The request is for one of our sitemaps, but has a trailing slash.
|
||||
* 3. The request is for the first index of a type, but has a page number.
|
||||
* 4. The request is for a sitemap from WordPress Core/other plugin.
|
||||
*
|
||||
* @since 4.2.1
|
||||
*/
|
||||
protected function maybeRedirect() {
|
||||
if ( $this->checkedForRedirects ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$requestUri = isset( $_SERVER['REQUEST_URI'] ) ? sanitize_text_field( wp_unslash( $_SERVER['REQUEST_URI'] ) ) : '';
|
||||
if ( ! $requestUri ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$this->checkedForRedirects = true;
|
||||
|
||||
// The request includes our deprecated "aiosp_sitemap_path" URL param.
|
||||
if ( preg_match( '/^\/\?aiosp_sitemap_path=root/i', (string) $requestUri ) ) {
|
||||
wp_safe_redirect( home_url( 'sitemap.xml' ) );
|
||||
exit;
|
||||
}
|
||||
|
||||
// The request is for one of our sitemaps, but has a trailing slash.
|
||||
if ( preg_match( '/\/(.*sitemap[0-9]*?\.xml(\.gz)?|.*sitemap(\.latest)?\.rss)\/$/i', (string) $requestUri ) ) {
|
||||
wp_safe_redirect( home_url() . untrailingslashit( $requestUri ) );
|
||||
exit;
|
||||
}
|
||||
|
||||
// The request is for the first index of a type, but has a page number.
|
||||
if ( preg_match( '/.*sitemap(0|1){1}?\.xml(\.gz)?$/i', (string) $requestUri ) ) {
|
||||
$pathWithoutNumber = preg_replace( '/(.*sitemap)(0|1){1}?(\.xml(\.gz)?)$/i', '$1$3', $requestUri );
|
||||
wp_safe_redirect( home_url() . $pathWithoutNumber );
|
||||
exit;
|
||||
}
|
||||
|
||||
// The request is for a sitemap from WordPress Core/other plugin, but the general sitemap is enabled.
|
||||
if ( ! aioseo()->options->sitemap->general->enable ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$sitemapPatterns = [
|
||||
'general' => [
|
||||
'sitemap\.txt',
|
||||
'sitemaps\.xml',
|
||||
'sitemap-xml\.xml',
|
||||
'sitemap[0-9]+\.xml',
|
||||
'sitemap(|[-_\/])?index[0-9]*\.xml',
|
||||
'wp-sitemap\.xml',
|
||||
],
|
||||
'rss' => [
|
||||
'rss[0-9]*\.xml',
|
||||
]
|
||||
];
|
||||
|
||||
$addonSitemapPatterns = aioseo()->addons->doAddonFunction( 'helpers', 'getOtherSitemapPatterns' );
|
||||
if ( ! empty( $addonSitemapPatterns ) ) {
|
||||
$sitemapPatterns = array_merge( $sitemapPatterns, $addonSitemapPatterns );
|
||||
}
|
||||
|
||||
foreach ( $sitemapPatterns as $type => $patterns ) {
|
||||
foreach ( $patterns as $pattern ) {
|
||||
if ( preg_match( "/^$pattern$/i", (string) $this->slug ) ) {
|
||||
wp_safe_redirect( aioseo()->sitemap->helpers->getUrl( $type ) );
|
||||
exit;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,587 @@
|
||||
<?php
|
||||
namespace AIOSEO\Plugin\Common\Sitemap;
|
||||
|
||||
// Exit if accessed directly.
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines which indexes should appear in the sitemap root index.
|
||||
*
|
||||
* @since 4.0.0
|
||||
*/
|
||||
class Root {
|
||||
/**
|
||||
* Returns the indexes for the sitemap root index.
|
||||
*
|
||||
* @since 4.0.0
|
||||
*
|
||||
* @return array The indexes.
|
||||
*/
|
||||
public function indexes() {
|
||||
$indexes = [];
|
||||
if ( 'general' !== aioseo()->sitemap->type ) {
|
||||
$addonIndexes = aioseo()->addons->doAddonFunction( 'root', 'indexes' );
|
||||
|
||||
foreach ( $addonIndexes as $addonIndex ) {
|
||||
if ( $addonIndex ) {
|
||||
return $addonIndex;
|
||||
}
|
||||
}
|
||||
|
||||
return $indexes;
|
||||
}
|
||||
|
||||
$filename = aioseo()->sitemap->filename;
|
||||
$postTypes = aioseo()->sitemap->helpers->includedPostTypes();
|
||||
$taxonomies = aioseo()->sitemap->helpers->includedTaxonomies();
|
||||
|
||||
$indexes = array_merge( $indexes, $this->getAdditionalIndexes() );
|
||||
|
||||
if ( $postTypes ) {
|
||||
$postArchives = [];
|
||||
|
||||
foreach ( $postTypes as $postType ) {
|
||||
$postIndexes = $this->buildIndexesPostType( $postType );
|
||||
$indexes = array_merge( $indexes, $postIndexes );
|
||||
|
||||
if (
|
||||
get_post_type_archive_link( $postType ) &&
|
||||
aioseo()->dynamicOptions->noConflict()->searchAppearance->archives->has( $postType ) &&
|
||||
(
|
||||
aioseo()->dynamicOptions->searchAppearance->archives->$postType->advanced->robotsMeta->default ||
|
||||
! aioseo()->dynamicOptions->searchAppearance->archives->$postType->advanced->robotsMeta->noindex
|
||||
)
|
||||
) {
|
||||
$lastModifiedPostTime = aioseo()->sitemap->helpers->lastModifiedPostTime( $postType );
|
||||
if ( $lastModifiedPostTime ) {
|
||||
$postArchives[ $postType ] = $lastModifiedPostTime;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ( ! empty( $postArchives ) ) {
|
||||
usort( $postArchives, function( $date1, $date2 ) {
|
||||
return $date1 < $date2 ? 1 : 0;
|
||||
} );
|
||||
|
||||
$indexes[] = [
|
||||
'loc' => aioseo()->helpers->localizedUrl( "/post-archive-$filename.xml" ),
|
||||
'lastmod' => $postArchives[0],
|
||||
'count' => count( $postArchives )
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
if ( $taxonomies ) {
|
||||
foreach ( $taxonomies as $taxonomy ) {
|
||||
$indexes = array_merge( $indexes, $this->buildIndexesTaxonomy( $taxonomy ) );
|
||||
}
|
||||
}
|
||||
|
||||
$postsTable = aioseo()->core->db->db->posts;
|
||||
if (
|
||||
aioseo()->sitemap->helpers->lastModifiedPost() &&
|
||||
aioseo()->options->sitemap->general->author &&
|
||||
aioseo()->options->searchAppearance->archives->author->show &&
|
||||
(
|
||||
aioseo()->options->searchAppearance->archives->author->advanced->robotsMeta->default ||
|
||||
! aioseo()->options->searchAppearance->archives->author->advanced->robotsMeta->noindex
|
||||
) &&
|
||||
(
|
||||
aioseo()->options->searchAppearance->advanced->globalRobotsMeta->default ||
|
||||
! aioseo()->options->searchAppearance->advanced->globalRobotsMeta->noindex
|
||||
)
|
||||
) {
|
||||
$usersTable = aioseo()->core->db->db->users; // We get the table name from WPDB since multisites share the same table.
|
||||
$authorPostTypes = aioseo()->sitemap->helpers->getAuthorPostTypes();
|
||||
$implodedPostTypes = aioseo()->helpers->implodeWhereIn( $authorPostTypes, true );
|
||||
$result = aioseo()->core->db->execute(
|
||||
"SELECT count(*) as amountOfAuthors FROM
|
||||
(
|
||||
SELECT u.ID FROM {$usersTable} as u
|
||||
INNER JOIN {$postsTable} as p ON u.ID = p.post_author
|
||||
WHERE p.post_status = 'publish' AND p.post_type IN ( {$implodedPostTypes} )
|
||||
GROUP BY u.ID
|
||||
) as x",
|
||||
true
|
||||
)->result();
|
||||
|
||||
if ( ! empty( $result[0]->amountOfAuthors ) ) {
|
||||
$indexes = array_merge( $indexes, $this->buildAuthorIndexes( (int) $result[0]->amountOfAuthors ) );
|
||||
}
|
||||
}
|
||||
|
||||
if (
|
||||
aioseo()->sitemap->helpers->lastModifiedPost() &&
|
||||
aioseo()->options->sitemap->general->date &&
|
||||
aioseo()->options->searchAppearance->archives->date->show &&
|
||||
(
|
||||
aioseo()->options->searchAppearance->archives->date->advanced->robotsMeta->default ||
|
||||
! aioseo()->options->searchAppearance->archives->date->advanced->robotsMeta->noindex
|
||||
) &&
|
||||
(
|
||||
aioseo()->options->searchAppearance->advanced->globalRobotsMeta->default ||
|
||||
! aioseo()->options->searchAppearance->advanced->globalRobotsMeta->noindex
|
||||
)
|
||||
) {
|
||||
$result = aioseo()->core->db->execute(
|
||||
"SELECT count(*) as amountOfUrls FROM (
|
||||
SELECT post_date
|
||||
FROM {$postsTable}
|
||||
WHERE post_type = 'post' AND post_status = 'publish'
|
||||
GROUP BY
|
||||
YEAR(post_date),
|
||||
MONTH(post_date)
|
||||
LIMIT 50000
|
||||
) as dates",
|
||||
true
|
||||
)->result();
|
||||
|
||||
$indexes[] = $this->buildIndex( 'date', $result[0]->amountOfUrls );
|
||||
}
|
||||
|
||||
if (
|
||||
aioseo()->helpers->isWooCommerceActive() &&
|
||||
in_array( 'product_attributes', aioseo()->sitemap->helpers->includedTaxonomies(), true )
|
||||
) {
|
||||
$productAttributes = aioseo()->sitemap->content->productAttributes( true );
|
||||
|
||||
if ( ! empty( $productAttributes ) ) {
|
||||
$indexes[] = $this->buildIndex( 'product_attributes', $productAttributes );
|
||||
}
|
||||
}
|
||||
|
||||
if ( isset( aioseo()->standalone->buddyPress->sitemap ) ) {
|
||||
$indexes = array_merge( $indexes, aioseo()->standalone->buddyPress->sitemap->indexes() );
|
||||
}
|
||||
|
||||
return apply_filters( 'aioseo_sitemap_indexes', array_filter( $indexes ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the additional page indexes.
|
||||
*
|
||||
* @since 4.2.1
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
private function getAdditionalIndexes() {
|
||||
$additionalPages = [];
|
||||
if ( aioseo()->options->sitemap->general->additionalPages->enable ) {
|
||||
$additionalPages = array_map( 'json_decode', aioseo()->options->sitemap->general->additionalPages->pages );
|
||||
$additionalPages = array_filter( $additionalPages, function( $additionalPage ) {
|
||||
return ! empty( $additionalPage->url );
|
||||
} );
|
||||
}
|
||||
|
||||
$entries = [];
|
||||
foreach ( $additionalPages as $additionalPage ) {
|
||||
$entries[] = [
|
||||
'loc' => $additionalPage->url,
|
||||
'lastmod' => aioseo()->sitemap->helpers->lastModifiedAdditionalPage( $additionalPage ),
|
||||
'changefreq' => $additionalPage->frequency->value,
|
||||
'priority' => $additionalPage->priority->value,
|
||||
'isTimezone' => true
|
||||
];
|
||||
}
|
||||
|
||||
if ( aioseo()->options->sitemap->general->additionalPages->enable ) {
|
||||
$entries = apply_filters( 'aioseo_sitemap_additional_pages', $entries );
|
||||
}
|
||||
|
||||
$postTypes = aioseo()->sitemap->helpers->includedPostTypes();
|
||||
$shouldIncludeHomepage = 'posts' === get_option( 'show_on_front' ) || ! in_array( 'page', $postTypes, true );
|
||||
if ( ! $shouldIncludeHomepage && ! count( $entries ) ) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$indexes = $this->buildAdditionalIndexes( $entries, $shouldIncludeHomepage );
|
||||
|
||||
return $indexes;
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds a given index.
|
||||
*
|
||||
* @since 4.0.0
|
||||
*
|
||||
* @param string $indexName The index name.
|
||||
* @param integer $amountOfUrls The amount of URLs in the index.
|
||||
* @return array The index.
|
||||
*/
|
||||
private function buildIndex( $indexName, $amountOfUrls ) {
|
||||
$filename = aioseo()->sitemap->filename;
|
||||
|
||||
return [
|
||||
'loc' => aioseo()->helpers->localizedUrl( "/$indexName-$filename.xml" ),
|
||||
'lastmod' => aioseo()->sitemap->helpers->lastModifiedPostTime(),
|
||||
'count' => $amountOfUrls
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds the additional pages index.
|
||||
*
|
||||
* @since 4.0.0
|
||||
*
|
||||
* @param array $entries The additional pages.
|
||||
* @param bool $shouldIncludeHomepage Whether or not the homepage should be included.
|
||||
* @return array The indexes.
|
||||
*/
|
||||
private function buildAdditionalIndexes( $entries, $shouldIncludeHomepage ) {
|
||||
if ( $shouldIncludeHomepage ) {
|
||||
$entries[] = [
|
||||
'loc' => home_url(),
|
||||
'lastmod' => aioseo()->sitemap->helpers->lastModifiedPostTime()
|
||||
];
|
||||
}
|
||||
|
||||
if ( empty( $entries ) ) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$filename = aioseo()->sitemap->filename;
|
||||
$chunks = aioseo()->sitemap->helpers->chunkEntries( $entries );
|
||||
|
||||
$indexes = [];
|
||||
for ( $i = 0; $i < count( $chunks ); $i++ ) {
|
||||
$chunk = array_values( $chunks[ $i ] );
|
||||
$indexNumber = 1 < count( $chunks ) ? $i + 1 : '';
|
||||
|
||||
$index = [
|
||||
'loc' => aioseo()->helpers->localizedUrl( "/addl-$filename$indexNumber.xml" ),
|
||||
'lastmod' => ! empty( $chunk[0]['lastmod'] ) ? aioseo()->helpers->dateTimeToIso8601( $chunk[0]['lastmod'] ) : '',
|
||||
'count' => count( $chunks[ $i ] )
|
||||
];
|
||||
|
||||
$indexes[] = $index;
|
||||
}
|
||||
|
||||
return $indexes;
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds the author archive indexes.
|
||||
*
|
||||
* @since 4.3.1
|
||||
*
|
||||
* @param integer $amountOfAuthors The amount of author archives.
|
||||
* @return array The indexes.
|
||||
*/
|
||||
private function buildAuthorIndexes( $amountOfAuthors ) {
|
||||
if ( ! $amountOfAuthors ) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$postTypes = aioseo()->sitemap->helpers->includedPostTypes();
|
||||
$filename = aioseo()->sitemap->filename;
|
||||
$chunks = $amountOfAuthors / aioseo()->sitemap->linksPerIndex;
|
||||
if ( $chunks < 1 ) {
|
||||
$chunks = 1;
|
||||
}
|
||||
|
||||
$indexes = [];
|
||||
for ( $i = 0; $i < $chunks; $i++ ) {
|
||||
$indexNumber = 1 < $chunks ? $i + 1 : '';
|
||||
|
||||
$usersTableName = aioseo()->core->db->db->users; // We get the table name from WPDB since multisites share the same table.
|
||||
$lastModified = aioseo()->core->db->start( "$usersTableName as u", true )
|
||||
->select( 'MAX(p.post_modified_gmt) as lastModified' )
|
||||
->join( 'posts as p', 'u.ID = p.post_author' )
|
||||
->where( 'p.post_status', 'publish' )
|
||||
->whereIn( 'p.post_type', $postTypes )
|
||||
->groupBy( 'u.ID' )
|
||||
->orderBy( 'lastModified DESC' )
|
||||
->limit( aioseo()->sitemap->linksPerIndex, $i * aioseo()->sitemap->linksPerIndex )
|
||||
->run()
|
||||
->result();
|
||||
|
||||
$lastModified = ! empty( $lastModified[0]->lastModified ) ? aioseo()->helpers->dateTimeToIso8601( $lastModified[0]->lastModified ) : '';
|
||||
|
||||
$index = [
|
||||
'loc' => aioseo()->helpers->localizedUrl( "/author-$filename$indexNumber.xml" ),
|
||||
'lastmod' => $lastModified,
|
||||
'count' => $i + 1 === $chunks ? $amountOfAuthors % aioseo()->sitemap->linksPerIndex : aioseo()->sitemap->linksPerIndex
|
||||
];
|
||||
|
||||
$indexes[] = $index;
|
||||
}
|
||||
|
||||
return $indexes;
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds indexes for all eligible posts of a given post type.
|
||||
*
|
||||
* @since 4.0.0
|
||||
*
|
||||
* @param string $postType The post type.
|
||||
* @return array The indexes.
|
||||
*/
|
||||
private function buildIndexesPostType( $postType ) {
|
||||
$prefix = aioseo()->core->db->prefix;
|
||||
$postsTable = $prefix . 'posts';
|
||||
$aioseoPostsTable = $prefix . 'aioseo_posts';
|
||||
$termRelationshipsTable = $prefix . 'term_relationships';
|
||||
$termTaxonomyTable = $prefix . 'term_taxonomy';
|
||||
$termsTable = $prefix . 'terms';
|
||||
$linksPerIndex = aioseo()->sitemap->linksPerIndex;
|
||||
|
||||
if ( 'attachment' === $postType && 'disabled' !== aioseo()->dynamicOptions->searchAppearance->postTypes->attachment->redirectAttachmentUrls ) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$excludedPostIds = [];
|
||||
$excludedTermIds = aioseo()->sitemap->helpers->excludedTerms();
|
||||
if ( ! empty( $excludedTermIds ) ) {
|
||||
$excludedTermIds = explode( ', ', $excludedTermIds );
|
||||
$excludedPostIds = aioseo()->core->db->start( 'term_relationships' )
|
||||
->select( 'object_id' )
|
||||
->whereIn( 'term_taxonomy_id', $excludedTermIds )
|
||||
->run()
|
||||
->result();
|
||||
|
||||
$excludedPostIds = array_map( function( $post ) {
|
||||
return $post->object_id;
|
||||
}, $excludedPostIds );
|
||||
}
|
||||
|
||||
if ( 'page' === $postType ) {
|
||||
$isStaticHomepage = 'page' === get_option( 'show_on_front' );
|
||||
if ( $isStaticHomepage ) {
|
||||
$blogPageId = (int) get_option( 'page_for_posts' );
|
||||
$excludedPostIds[] = $blogPageId;
|
||||
}
|
||||
}
|
||||
|
||||
$whereClause = '';
|
||||
$excludedPostsString = aioseo()->sitemap->helpers->excludedPosts();
|
||||
if ( ! empty( $excludedPostsString ) ) {
|
||||
$excludedPostIds = array_merge( $excludedPostIds, explode( ', ', $excludedPostsString ) );
|
||||
}
|
||||
|
||||
if ( ! empty( $excludedPostIds ) ) {
|
||||
$implodedPostIds = aioseo()->helpers->implodeWhereIn( $excludedPostIds, true );
|
||||
$whereClause = "AND p.ID NOT IN ( $implodedPostIds )";
|
||||
}
|
||||
|
||||
if (
|
||||
apply_filters( 'aioseo_sitemap_woocommerce_exclude_hidden_products', true ) &&
|
||||
aioseo()->helpers->isWooCommerceActive() &&
|
||||
'product' === $postType
|
||||
) {
|
||||
$whereClause .= " AND p.ID NOT IN (
|
||||
SELECT CONVERT(tr.object_id, unsigned) AS object_id
|
||||
FROM {$termRelationshipsTable} AS tr
|
||||
JOIN {$termTaxonomyTable} AS tt ON tr.term_taxonomy_id = tt.term_taxonomy_id
|
||||
JOIN {$termsTable} AS t ON tt.term_id = t.term_id
|
||||
WHERE t.name = 'exclude-from-catalog'
|
||||
)";
|
||||
}
|
||||
|
||||
// Include the blog page in the posts post type unless manually excluded.
|
||||
$blogPageId = (int) get_option( 'page_for_posts' );
|
||||
if (
|
||||
$blogPageId &&
|
||||
! in_array( $blogPageId, $excludedPostIds, true ) &&
|
||||
'post' === $postType
|
||||
) {
|
||||
$whereClause .= " OR `p`.`ID` = $blogPageId ";
|
||||
}
|
||||
|
||||
$posts = aioseo()->core->db->execute(
|
||||
aioseo()->core->db->db->prepare(
|
||||
"SELECT ID, post_modified_gmt
|
||||
FROM (
|
||||
SELECT @row := @row + 1 AS rownum, ID, post_modified_gmt
|
||||
FROM (
|
||||
SELECT p.ID, ap.priority, p.post_modified_gmt
|
||||
FROM {$postsTable} AS p
|
||||
LEFT JOIN {$aioseoPostsTable} AS ap ON p.ID = ap.post_id
|
||||
WHERE p.post_status IN ( 'publish', 'inherit' )
|
||||
AND p.post_type = %s
|
||||
AND p.post_password = ''
|
||||
AND (ap.robots_noindex IS NULL OR ap.robots_default = 1 OR ap.robots_noindex = 0)
|
||||
{$whereClause}
|
||||
ORDER BY ap.priority DESC, p.post_modified_gmt DESC
|
||||
) AS x
|
||||
CROSS JOIN (SELECT @row := 0) AS vars
|
||||
ORDER BY post_modified_gmt DESC
|
||||
) AS y
|
||||
WHERE rownum = 1 OR rownum % %d = 1;",
|
||||
[
|
||||
$postType,
|
||||
$linksPerIndex
|
||||
]
|
||||
),
|
||||
true
|
||||
)->result();
|
||||
|
||||
$totalPosts = aioseo()->core->db->execute(
|
||||
aioseo()->core->db->db->prepare(
|
||||
"SELECT COUNT(*) as count
|
||||
FROM {$postsTable} as p
|
||||
LEFT JOIN {$aioseoPostsTable} as ap ON p.ID = ap.post_id
|
||||
WHERE p.post_status IN ( 'publish', 'inherit' )
|
||||
AND p.post_type = %s
|
||||
AND p.post_password = ''
|
||||
AND (ap.robots_noindex IS NULL OR ap.robots_default = 1 OR ap.robots_noindex = 0)
|
||||
{$whereClause}
|
||||
",
|
||||
[
|
||||
$postType
|
||||
]
|
||||
),
|
||||
true
|
||||
)->result();
|
||||
|
||||
if ( $posts ) {
|
||||
$indexes = [];
|
||||
$filename = aioseo()->sitemap->filename;
|
||||
$postCount = count( $posts );
|
||||
for ( $i = 0; $i < $postCount; $i++ ) {
|
||||
$indexNumber = 0 !== $i && 1 < $postCount ? $i + 1 : '';
|
||||
|
||||
$indexes[] = [
|
||||
'loc' => aioseo()->helpers->localizedUrl( "/$postType-$filename$indexNumber.xml" ),
|
||||
'lastmod' => aioseo()->helpers->dateTimeToIso8601( $posts[ $i ]->post_modified_gmt ),
|
||||
'count' => $linksPerIndex
|
||||
];
|
||||
}
|
||||
|
||||
// We need to update the count of the last index since it won't necessarily be the same as the links per index.
|
||||
$indexes[ count( $indexes ) - 1 ]['count'] = $totalPosts[0]->count - ( $linksPerIndex * ( $postCount - 1 ) );
|
||||
|
||||
return $indexes;
|
||||
}
|
||||
|
||||
if ( ! $posts ) {
|
||||
$addonsPosts = aioseo()->addons->doAddonFunction( 'root', 'buildIndexesPostType', [ $postType ] );
|
||||
|
||||
foreach ( $addonsPosts as $addonPosts ) {
|
||||
if ( $addonPosts ) {
|
||||
$posts = $addonPosts;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ( ! $posts ) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return $this->buildIndexes( $postType, $posts );
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds indexes for all eligible terms of a given taxonomy.
|
||||
*
|
||||
* @since 4.0.0
|
||||
*
|
||||
* @param string $taxonomy The taxonomy.
|
||||
* @return array The indexes.
|
||||
*/
|
||||
private function buildIndexesTaxonomy( $taxonomy ) {
|
||||
$terms = aioseo()->sitemap->content->terms( $taxonomy, [ 'root' => true ] );
|
||||
|
||||
if ( ! $terms ) {
|
||||
$addonsTerms = aioseo()->addons->doAddonFunction( 'root', 'buildIndexesTaxonomy', [ $taxonomy ] );
|
||||
|
||||
foreach ( $addonsTerms as $addonTerms ) {
|
||||
if ( $addonTerms ) {
|
||||
$terms = $addonTerms;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ( ! $terms ) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return $this->buildIndexes( $taxonomy, $terms );
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds indexes for a given type.
|
||||
*
|
||||
* Acts as a helper function for buildIndexesPostTypes() and buildIndexesTaxonomies().
|
||||
*
|
||||
* @since 4.0.0
|
||||
*
|
||||
* @param string $name The name of the object parent.
|
||||
* @param array $entries The sitemap entries.
|
||||
* @return array The indexes.
|
||||
*/
|
||||
public function buildIndexes( $name, $entries ) {
|
||||
$filename = aioseo()->sitemap->filename;
|
||||
$chunks = aioseo()->sitemap->helpers->chunkEntries( $entries );
|
||||
$indexes = [];
|
||||
for ( $i = 0; $i < count( $chunks ); $i++ ) {
|
||||
$chunk = array_values( $chunks[ $i ] );
|
||||
$indexNumber = 0 !== $i && 1 < count( $chunks ) ? $i + 1 : '';
|
||||
|
||||
$index = [
|
||||
'loc' => aioseo()->helpers->localizedUrl( "/$name-$filename$indexNumber.xml" ),
|
||||
'count' => count( $chunks[ $i ] )
|
||||
];
|
||||
|
||||
if ( isset( $entries[0]->ID ) ) {
|
||||
$ids = array_map( function( $post ) {
|
||||
return $post->ID;
|
||||
}, $chunk );
|
||||
$ids = implode( "', '", $ids );
|
||||
|
||||
$lastModified = null;
|
||||
if ( ! apply_filters( 'aioseo_sitemap_lastmod_disable', false ) ) {
|
||||
$lastModified = aioseo()->core->db
|
||||
->start( aioseo()->core->db->db->posts . ' as p', true )
|
||||
->select( 'MAX(`p`.`post_modified_gmt`) as last_modified' )
|
||||
->whereRaw( "( `p`.`ID` IN ( '$ids' ) )" )
|
||||
->run()
|
||||
->result();
|
||||
}
|
||||
|
||||
if ( ! empty( $lastModified[0]->last_modified ) ) {
|
||||
$index['lastmod'] = aioseo()->helpers->dateTimeToIso8601( $lastModified[0]->last_modified );
|
||||
}
|
||||
$indexes[] = $index;
|
||||
continue;
|
||||
}
|
||||
|
||||
$termIds = [];
|
||||
foreach ( $chunk as $term ) {
|
||||
$termIds[] = $term->term_id;
|
||||
}
|
||||
$termIds = implode( "', '", $termIds );
|
||||
|
||||
$termRelationshipsTable = aioseo()->core->db->db->prefix . 'term_relationships';
|
||||
|
||||
$lastModified = null;
|
||||
if ( ! apply_filters( 'aioseo_sitemap_lastmod_disable', false ) ) {
|
||||
$lastModified = aioseo()->core->db
|
||||
->start( aioseo()->core->db->db->posts . ' as p', true )
|
||||
->select( 'MAX(`p`.`post_modified_gmt`) as last_modified' )
|
||||
->whereRaw( "
|
||||
( `p`.`ID` IN
|
||||
(
|
||||
SELECT CONVERT(`tr`.`object_id`, unsigned)
|
||||
FROM `$termRelationshipsTable` as tr
|
||||
WHERE `tr`.`term_taxonomy_id` IN ( '$termIds' )
|
||||
)
|
||||
)" )
|
||||
->run()
|
||||
->result();
|
||||
}
|
||||
|
||||
if ( ! empty( $lastModified[0]->last_modified ) ) {
|
||||
$index['lastmod'] = aioseo()->helpers->dateTimeToIso8601( $lastModified[0]->last_modified );
|
||||
}
|
||||
$indexes[] = $index;
|
||||
}
|
||||
|
||||
return $indexes;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,419 @@
|
||||
<?php
|
||||
namespace AIOSEO\Plugin\Common\Sitemap;
|
||||
|
||||
// Exit if accessed directly.
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit;
|
||||
}
|
||||
|
||||
use AIOSEO\Plugin\Common\Models;
|
||||
|
||||
/**
|
||||
* Handles our sitemaps.
|
||||
*
|
||||
* @since 4.0.0
|
||||
*/
|
||||
class Sitemap extends SitemapAbstract {
|
||||
/**
|
||||
* The sitemap filename.
|
||||
*
|
||||
* @since 4.4.2
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public $filename = '';
|
||||
|
||||
/**
|
||||
* Whether the sitemap indexes are enabled.
|
||||
*
|
||||
* @since 4.4.2
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
public $indexes = false;
|
||||
|
||||
/**
|
||||
* The sitemap index name.
|
||||
*
|
||||
* @since 4.4.2
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public $indexName = '';
|
||||
|
||||
/**
|
||||
* The number of links per index.
|
||||
*
|
||||
* @since 4.4.2
|
||||
*
|
||||
* @var int
|
||||
*/
|
||||
public $linksPerIndex = 1000;
|
||||
|
||||
/**
|
||||
* The current page number.
|
||||
*
|
||||
* @since 4.4.2
|
||||
*
|
||||
* @var int
|
||||
*/
|
||||
public $pageNumber = 0;
|
||||
|
||||
/**
|
||||
* The entries' offset.
|
||||
*
|
||||
* @since 4.4.2
|
||||
*
|
||||
* @var int
|
||||
*/
|
||||
public $offset = 0;
|
||||
|
||||
/**
|
||||
* Whether the sitemap is static.
|
||||
*
|
||||
* @since 4.4.2
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
public $isStatic = false;
|
||||
|
||||
/**
|
||||
* Class constructor.
|
||||
*
|
||||
* @since 4.0.0
|
||||
*/
|
||||
public function __construct() {
|
||||
$this->content = new Content();
|
||||
$this->root = new Root();
|
||||
$this->query = new Query();
|
||||
$this->file = new File();
|
||||
$this->image = new Image\Image();
|
||||
$this->priority = new Priority();
|
||||
$this->output = new Output();
|
||||
$this->helpers = new Helpers();
|
||||
$this->requestParser = new RequestParser();
|
||||
$this->xsl = new Xsl();
|
||||
|
||||
new Localization();
|
||||
|
||||
$this->disableWpSitemap();
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds our hooks.
|
||||
* Note: This runs init and is triggered in the main AIOSEO class.
|
||||
*
|
||||
* @since 4.0.0
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function init() {
|
||||
add_action( 'aioseo_static_sitemap_regeneration', [ $this, 'regenerateStaticSitemap' ] );
|
||||
|
||||
// Check if static files need to be updated.
|
||||
add_action( 'wp_insert_post', [ $this, 'regenerateOnUpdate' ] );
|
||||
add_action( 'edited_term', [ $this, 'regenerateStaticSitemap' ] );
|
||||
|
||||
add_action( 'admin_init', [ $this, 'detectStatic' ] );
|
||||
|
||||
$this->maybeAddHtaccessRewriteRules();
|
||||
}
|
||||
|
||||
/**
|
||||
* Disables the WP Core sitemap if our general sitemap is enabled.
|
||||
*
|
||||
* @since 4.2.1
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
protected function disableWpSitemap() {
|
||||
if ( ! aioseo()->options->sitemap->general->enable ) {
|
||||
return;
|
||||
}
|
||||
|
||||
remove_action( 'init', 'wp_sitemaps_get_server' );
|
||||
add_filter( 'wp_sitemaps_enabled', '__return_false' );
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the .htaccess rewrite rules are present if the user is using Apache. If not, add them.
|
||||
*
|
||||
* @since 4.2.5
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
private function maybeAddHtaccessRewriteRules() {
|
||||
if ( ! aioseo()->helpers->isApache() || wp_doing_ajax() || wp_doing_cron() ) {
|
||||
return;
|
||||
}
|
||||
|
||||
ob_start();
|
||||
aioseo()->templates->getTemplate( 'sitemap/htaccess-rewrite-rules.php' );
|
||||
$rewriteRules = ob_get_clean();
|
||||
|
||||
$escapedRewriteRules = aioseo()->helpers->escapeRegex( $rewriteRules );
|
||||
|
||||
$contents = aioseo()->helpers->decodeHtmlEntities( aioseo()->htaccess->getContents() );
|
||||
if ( get_option( 'permalink_structure' ) ) {
|
||||
if ( preg_match( '/All in One SEO Sitemap Rewrite Rules/i', (string) $contents ) && ! aioseo()->core->cache->get( 'aioseo_sitemap_htaccess_rewrite_rules_remove' ) ) {
|
||||
aioseo()->core->cache->update( 'aioseo_sitemap_htaccess_rewrite_rules_remove', time(), HOUR_IN_SECONDS );
|
||||
|
||||
$contents = preg_replace( "/$escapedRewriteRules/i", '', (string) $contents );
|
||||
aioseo()->htaccess->saveContents( $contents );
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if ( preg_match( '/All in One SEO Sitemap Rewrite Rules/i', (string) $contents ) || aioseo()->core->cache->get( 'aioseo_sitemap_htaccess_rewrite_rules_add' ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
aioseo()->core->cache->update( 'aioseo_sitemap_htaccess_rewrite_rules_add', time(), HOUR_IN_SECONDS );
|
||||
|
||||
$contents .= $rewriteRules;
|
||||
|
||||
aioseo()->htaccess->saveContents( $contents );
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if static sitemap files prevent dynamic sitemap generation.
|
||||
*
|
||||
* @since 4.0.0
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function detectStatic() {
|
||||
$isGeneralSitemapStatic = aioseo()->options->sitemap->general->advancedSettings->enable &&
|
||||
in_array( 'staticSitemap', aioseo()->internalOptions->internal->deprecatedOptions, true ) &&
|
||||
! aioseo()->options->deprecated->sitemap->general->advancedSettings->dynamic;
|
||||
|
||||
if ( $isGeneralSitemapStatic ) {
|
||||
Models\Notification::deleteNotificationByName( 'sitemap-static-files' );
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
require_once ABSPATH . 'wp-admin/includes/file.php';
|
||||
$files = list_files( get_home_path(), 1 );
|
||||
if ( ! count( $files ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$detectedFiles = [];
|
||||
if ( ! $isGeneralSitemapStatic ) {
|
||||
foreach ( $files as $filename ) {
|
||||
if ( preg_match( '#.*sitemap.*#', (string) $filename ) ) {
|
||||
// We don't want to delete the video sitemap here at all.
|
||||
$isVideoSitemap = preg_match( '#.*video.*#', (string) $filename ) ? true : false;
|
||||
if ( ! $isVideoSitemap ) {
|
||||
$detectedFiles[] = $filename;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$this->maybeShowStaticSitemapNotification( $detectedFiles );
|
||||
}
|
||||
|
||||
/**
|
||||
* If there are files, show a notice, otherwise delete it.
|
||||
*
|
||||
* @since 4.0.0
|
||||
*
|
||||
* @param array $detectedFiles An array of detected files.
|
||||
* @return void
|
||||
*/
|
||||
protected function maybeShowStaticSitemapNotification( $detectedFiles ) {
|
||||
if ( ! count( $detectedFiles ) ) {
|
||||
Models\Notification::deleteNotificationByName( 'sitemap-static-files' );
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$notification = Models\Notification::getNotificationByName( 'sitemap-static-files' );
|
||||
if ( $notification->notification_name ) {
|
||||
return;
|
||||
}
|
||||
|
||||
Models\Notification::addNotification( [
|
||||
'slug' => uniqid(),
|
||||
'notification_name' => 'sitemap-static-files',
|
||||
'title' => __( 'Static sitemap files detected', 'all-in-one-seo-pack' ),
|
||||
'content' => sprintf(
|
||||
// Translators: 1 - The plugin short name ("AIOSEO"), 2 - Same as previous.
|
||||
__( '%1$s has detected static sitemap files in the root folder of your WordPress installation.
|
||||
As long as these files are present, %2$s is not able to dynamically generate your sitemap.', 'all-in-one-seo-pack' ),
|
||||
AIOSEO_PLUGIN_SHORT_NAME,
|
||||
AIOSEO_PLUGIN_SHORT_NAME
|
||||
),
|
||||
'type' => 'error',
|
||||
'level' => [ 'all' ],
|
||||
'button1_label' => __( 'Delete Static Files', 'all-in-one-seo-pack' ),
|
||||
'button1_action' => 'http://action#sitemap/delete-static-files',
|
||||
'start' => gmdate( 'Y-m-d H:i:s' )
|
||||
] );
|
||||
}
|
||||
|
||||
/**
|
||||
* Regenerates the static sitemap files when a post is updated.
|
||||
*
|
||||
* @since 4.0.0
|
||||
*
|
||||
* @param integer $postId The post ID.
|
||||
* @return void
|
||||
*/
|
||||
public function regenerateOnUpdate( $postId ) {
|
||||
if ( aioseo()->helpers->isValidPost( $postId ) ) {
|
||||
$this->scheduleRegeneration();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Schedules an action to regenerate the static sitemap files.
|
||||
*
|
||||
* @since 4.0.5
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function scheduleRegeneration() {
|
||||
try {
|
||||
if (
|
||||
! aioseo()->options->deprecated->sitemap->general->advancedSettings->dynamic &&
|
||||
! as_next_scheduled_action( 'aioseo_static_sitemap_regeneration' )
|
||||
) {
|
||||
as_schedule_single_action( time() + 60, 'aioseo_static_sitemap_regeneration', [], 'aioseo' );
|
||||
}
|
||||
} catch ( \Exception $e ) {
|
||||
// Do nothing.
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Regenerates the static sitemap files.
|
||||
*
|
||||
* @since 4.0.5
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function regenerateStaticSitemap() {
|
||||
aioseo()->sitemap->file->generate();
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates the requested sitemap.
|
||||
*
|
||||
* @since 4.0.0
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function generate() {
|
||||
if ( empty( $this->type ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
// This is a hack to prevent WordPress from running it's default stuff during our processing.
|
||||
global $wp_query; // phpcs:ignore Squiz.NamingConventions.ValidVariableName
|
||||
$wp_query->is_home = false; // phpcs:ignore Squiz.NamingConventions.ValidVariableName
|
||||
|
||||
// This prevents the sitemap from including terms twice when WPML is active.
|
||||
if ( class_exists( 'SitePress' ) ) {
|
||||
global $sitepress_settings; // phpcs:ignore Squiz.NamingConventions.ValidVariableName
|
||||
// Before building the sitemap make sure links aren't translated.
|
||||
// The setting should not be updated in the DB.
|
||||
$sitepress_settings['auto_adjust_ids'] = 0; // phpcs:ignore Squiz.NamingConventions.ValidVariableName
|
||||
}
|
||||
|
||||
// If requested sitemap should be static and doesn't exist, then generate it.
|
||||
// We'll then serve it dynamically for the current request so that we don't serve a blank page.
|
||||
$this->doesFileExist();
|
||||
|
||||
$options = aioseo()->options->noConflict();
|
||||
if ( ! $options->sitemap->{aioseo()->sitemap->type}->enable ) {
|
||||
aioseo()->helpers->notFoundPage();
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$entries = aioseo()->sitemap->content->get();
|
||||
$total = aioseo()->sitemap->content->getTotal();
|
||||
if ( ! $entries ) {
|
||||
$addonsEntries = aioseo()->addons->doAddonFunction( 'content', 'get' );
|
||||
$addonTotals = aioseo()->addons->doAddonFunction( 'content', 'getTotal' );
|
||||
foreach ( $addonsEntries as $addonSlug => $addonEntries ) {
|
||||
if ( ! empty( $addonEntries ) ) {
|
||||
$entries = $addonEntries;
|
||||
$total = ! empty( $addonTotals[ $addonSlug ] ) ? $addonTotals[ $addonSlug ] : count( $entries );
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ( 0 === $total && empty( $entries ) ) {
|
||||
status_header( 404 );
|
||||
}
|
||||
|
||||
$this->xsl->saveXslData(
|
||||
aioseo()->sitemap->requestParser->slug,
|
||||
$entries,
|
||||
$total
|
||||
);
|
||||
|
||||
$this->headers();
|
||||
aioseo()->sitemap->output->output( $entries );
|
||||
aioseo()->addons->doAddonFunction( 'output', 'output', [ $entries ] );
|
||||
|
||||
exit;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if static file should be served and generates it if it doesn't exist.
|
||||
*
|
||||
* This essentially acts as a safety net in case a file doesn't exist yet or has been deleted.
|
||||
*
|
||||
* @since 4.0.0
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
protected function doesFileExist() {
|
||||
aioseo()->addons->doAddonFunction( 'sitemap', 'doesFileExist' );
|
||||
|
||||
if (
|
||||
'general' !== $this->type ||
|
||||
! aioseo()->options->sitemap->general->advancedSettings->enable ||
|
||||
! in_array( 'staticSitemap', aioseo()->internalOptions->internal->deprecatedOptions, true ) ||
|
||||
aioseo()->options->sitemap->general->advancedSettings->dynamic
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
require_once ABSPATH . 'wp-admin/includes/file.php';
|
||||
if ( isset( $_SERVER['REQUEST_URI'] ) && ! aioseo()->core->fs->exists( get_home_path() . sanitize_text_field( wp_unslash( $_SERVER['REQUEST_URI'] ) ) ) ) {
|
||||
$this->scheduleRegeneration();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the HTTP headers for the sitemap.
|
||||
*
|
||||
* @since 4.0.0
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function headers() {
|
||||
$charset = aioseo()->helpers->getCharset();
|
||||
header( "Content-Type: text/xml; charset=$charset", true );
|
||||
header( 'X-Robots-Tag: noindex, follow', true );
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers an active sitemap addon and its classes.
|
||||
* NOTE: This is deprecated and only there for users who already were using the previous sitemap addons version.
|
||||
*
|
||||
* @final 4.2.7
|
||||
* @since 4.0.0
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function addAddon() {}
|
||||
}
|
||||
@@ -0,0 +1,176 @@
|
||||
<?php
|
||||
namespace AIOSEO\Plugin\Common\Sitemap;
|
||||
|
||||
// Exit if accessed directly.
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit;
|
||||
}
|
||||
|
||||
/**
|
||||
* Abstract class holding the class properties of our main AIOSEO class.
|
||||
*
|
||||
* @since 4.4.3
|
||||
*/
|
||||
abstract class SitemapAbstract {
|
||||
/**
|
||||
* Content class instance.
|
||||
*
|
||||
* @since 4.2.7
|
||||
*
|
||||
* @var Content
|
||||
*/
|
||||
public $content = null;
|
||||
|
||||
/**
|
||||
* Root class instance.
|
||||
*
|
||||
* @since 4.2.7
|
||||
*
|
||||
* @var Root
|
||||
*/
|
||||
public $root = null;
|
||||
|
||||
/**
|
||||
* Query class instance.
|
||||
*
|
||||
* @since 4.2.7
|
||||
*
|
||||
* @var Query
|
||||
*/
|
||||
public $query = null;
|
||||
|
||||
/**
|
||||
* File class instance.
|
||||
*
|
||||
* @since 4.2.7
|
||||
*
|
||||
* @var File
|
||||
*/
|
||||
public $file = null;
|
||||
|
||||
/**
|
||||
* Image class instance.
|
||||
*
|
||||
* @since 4.2.7
|
||||
*
|
||||
* @var Image\Image
|
||||
*/
|
||||
public $image = null;
|
||||
|
||||
/**
|
||||
* Priority class instance.
|
||||
*
|
||||
* @since 4.2.7
|
||||
*
|
||||
* @var Priority
|
||||
*/
|
||||
public $priority = null;
|
||||
|
||||
/**
|
||||
* Output class instance.
|
||||
*
|
||||
* @since 4.2.7
|
||||
*
|
||||
* @var Output
|
||||
*/
|
||||
public $output = null;
|
||||
|
||||
/**
|
||||
* Helpers class instance.
|
||||
*
|
||||
* @since 4.2.7
|
||||
*
|
||||
* @var Helpers
|
||||
*/
|
||||
public $helpers = null;
|
||||
|
||||
/**
|
||||
* RequestParser class instance.
|
||||
*
|
||||
* @since 4.2.7
|
||||
*
|
||||
* @var RequestParser
|
||||
*/
|
||||
public $requestParser = null;
|
||||
|
||||
/**
|
||||
* Xsl class instance.
|
||||
*
|
||||
* @since 4.2.7
|
||||
*
|
||||
* @var Xsl
|
||||
*/
|
||||
public $xsl = null;
|
||||
|
||||
/**
|
||||
* The sitemap type (e.g. "general", "news", "video", "rss", etc.).
|
||||
*
|
||||
* @since 4.2.7
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public $type = '';
|
||||
|
||||
/**
|
||||
* Index name.
|
||||
*
|
||||
* @since 4.4.3
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public $indexName = '';
|
||||
|
||||
/**
|
||||
* Page number.
|
||||
*
|
||||
* @since 4.4.3
|
||||
*
|
||||
* @var int
|
||||
*/
|
||||
public $pageNumber = 0;
|
||||
|
||||
/**
|
||||
* Page number.
|
||||
*
|
||||
* @since 4.4.3
|
||||
*
|
||||
* @var int
|
||||
*/
|
||||
public $offset = 0;
|
||||
|
||||
/**
|
||||
* Indexes active.
|
||||
*
|
||||
* @since 4.4.3
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
public $indexes = false;
|
||||
|
||||
/**
|
||||
* Links per index.
|
||||
*
|
||||
* @since 4.4.3
|
||||
*
|
||||
* @var int
|
||||
*/
|
||||
public $linksPerIndex = PHP_INT_MAX;
|
||||
|
||||
/**
|
||||
* Is static.
|
||||
*
|
||||
* @since 4.4.3
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
public $isStatic = false;
|
||||
|
||||
/**
|
||||
* Filename.
|
||||
*
|
||||
* @since 4.4.3
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public $filename = '';
|
||||
}
|
||||
@@ -0,0 +1,145 @@
|
||||
<?php
|
||||
namespace AIOSEO\Plugin\Common\Sitemap;
|
||||
|
||||
// Exit if accessed directly.
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit;
|
||||
}
|
||||
|
||||
/**
|
||||
* Serves stylesheets for sitemaps.
|
||||
*
|
||||
* @since 4.2.1
|
||||
*/
|
||||
class Xsl {
|
||||
/**
|
||||
* Generates the XSL stylesheet for the current sitemap.
|
||||
*
|
||||
* @since 4.2.1
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function generate() {
|
||||
aioseo()->sitemap->headers();
|
||||
|
||||
$charset = aioseo()->helpers->getCharset();
|
||||
$sitemapUrl = wp_get_referer();
|
||||
$sitemapPath = aioseo()->helpers->getPermalinkPath( $sitemapUrl );
|
||||
|
||||
// Figure out which sitemap we're serving.
|
||||
preg_match( '/\/(.*?)-?sitemap([0-9]*)\.xml/', (string) $sitemapPath, $sitemapInfo );
|
||||
$sitemapName = ! empty( $sitemapInfo[1] ) ? strtoupper( $sitemapInfo[1] ) : '';
|
||||
|
||||
// Remove everything after ? from sitemapPath to avoid caching issues.
|
||||
$sitemapPath = wp_parse_url( $sitemapPath, PHP_URL_PATH ) ?: '';
|
||||
|
||||
if ( ! empty( $sitemapInfo[1] ) ) {
|
||||
switch ( $sitemapInfo[1] ) {
|
||||
case 'addl':
|
||||
$sitemapName = __( 'Additional Pages', 'all-in-one-seo-pack' );
|
||||
break;
|
||||
case 'post-archive':
|
||||
$sitemapName = __( 'Post Archive', 'all-in-one-seo-pack' );
|
||||
break;
|
||||
case 'bp-activity':
|
||||
case 'bp-group':
|
||||
case 'bp-member':
|
||||
$bpFakePostTypes = aioseo()->standalone->buddyPress->getFakePostTypes();
|
||||
$labels = array_column( wp_list_filter( $bpFakePostTypes, [ 'name' => $sitemapInfo[1] ] ), 'label' );
|
||||
$sitemapName = ! empty( $labels[0] ) ? $labels[0] : $sitemapName;
|
||||
break;
|
||||
case 'product_attributes':
|
||||
$sitemapName = __( 'Product Attributes', 'all-in-one-seo-pack' );
|
||||
break;
|
||||
default:
|
||||
if ( post_type_exists( $sitemapInfo[1] ) ) {
|
||||
$postTypeObject = get_post_type_object( $sitemapInfo[1] );
|
||||
$sitemapName = $postTypeObject->labels->singular_name;
|
||||
}
|
||||
if ( taxonomy_exists( $sitemapInfo[1] ) ) {
|
||||
$taxonomyObject = get_taxonomy( $sitemapInfo[1] );
|
||||
$sitemapName = $taxonomyObject->labels->singular_name;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
$currentPage = ! empty( $sitemapInfo[2] ) ? (int) $sitemapInfo[2] : 1;
|
||||
|
||||
// phpcs:disable VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable
|
||||
$linksPerIndex = aioseo()->options->sitemap->general->linksPerIndex;
|
||||
$advanced = aioseo()->options->sitemap->general->advancedSettings->enable;
|
||||
$excludeImages = aioseo()->options->sitemap->general->advancedSettings->excludeImages;
|
||||
$sitemapParams = aioseo()->helpers->getParametersFromUrl( $sitemapUrl );
|
||||
$xslParams = aioseo()->core->cache->get( 'aioseo_sitemap_' . aioseo()->sitemap->requestParser->cleanSlug( $sitemapPath ) );
|
||||
// phpcs:enable VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable
|
||||
|
||||
// Translators: 1 - The sitemap name, 2 - The current page.
|
||||
$title = sprintf( __( '%1$s Sitemap %2$s', 'all-in-one-seo-pack' ), $sitemapName, $currentPage > 1 ? $currentPage : '' );
|
||||
$title = trim( $title );
|
||||
|
||||
echo '<?xml version="1.0" encoding="' . esc_attr( $charset ) . '"?>';
|
||||
include_once AIOSEO_DIR . '/app/Common/Views/sitemap/xsl/default.php';
|
||||
exit;
|
||||
}
|
||||
|
||||
/**
|
||||
* Save the data to use in the XSL.
|
||||
*
|
||||
* @since 4.1.5
|
||||
*
|
||||
* @param string $fileName The sitemap file name.
|
||||
* @param array $entries The sitemap entries.
|
||||
* @param int $total The total sitemap entries count.
|
||||
* @return void
|
||||
*/
|
||||
public function saveXslData( $fileName, $entries, $total ) {
|
||||
$counts = [];
|
||||
$datetime = [];
|
||||
$dateFormat = get_option( 'date_format' );
|
||||
$timeFormat = get_option( 'time_format' );
|
||||
|
||||
$entries = aioseo()->sitemap->helpers->decodeSitemapEntries( $entries );
|
||||
|
||||
foreach ( $entries as $index ) {
|
||||
$url = ! empty( $index['guid'] ) ? $index['guid'] : $index['loc'];
|
||||
|
||||
if ( ! empty( $index['count'] ) && aioseo()->options->sitemap->general->linksPerIndex !== (int) $index['count'] ) {
|
||||
$counts[ $url ] = $index['count'];
|
||||
}
|
||||
|
||||
if ( ! empty( $index['lastmod'] ) || ! empty( $index['publicationDate'] ) || ! empty( $index['pubDate'] ) ) {
|
||||
$date = ! empty( $index['lastmod'] ) ? $index['lastmod'] : ( ! empty( $index['publicationDate'] ) ? $index['publicationDate'] : $index['pubDate'] );
|
||||
$isTimezone = ! empty( $index['isTimezone'] ) && $index['isTimezone'];
|
||||
$datetime[ $url ] = [
|
||||
'date' => $isTimezone ? date_i18n( $dateFormat, strtotime( $date ) ) : get_date_from_gmt( $date, $dateFormat ),
|
||||
'time' => $isTimezone ? date_i18n( $timeFormat, strtotime( $date ) ) : get_date_from_gmt( $date, $timeFormat )
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
$data = [
|
||||
'counts' => $counts,
|
||||
'datetime' => $datetime,
|
||||
'pagination' => [
|
||||
'showing' => count( $entries ),
|
||||
'total' => $total
|
||||
]
|
||||
];
|
||||
|
||||
// Set a high expiration date so we still have the cache for static sitemaps.
|
||||
aioseo()->core->cache->update( 'aioseo_sitemap_' . $fileName, $data, MONTH_IN_SECONDS );
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the data to use on the XSL.
|
||||
*
|
||||
* @since 4.2.1
|
||||
*
|
||||
* @param string $fileName The sitemap file name.
|
||||
* @return array The XSL data for the given file name.
|
||||
*/
|
||||
public function getXslData( $fileName ) {
|
||||
return aioseo()->core->cache->get( 'aioseo_sitemap_' . $fileName );
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user