| Current Path : /home/x/b/o/xbodynamge/namtation/wp-content/ |
| Current File : /home/x/b/o/xbodynamge/namtation/wp-content/Sitemap.tar |
Root.php 0000666 00000043317 15114670646 0006226 0 ustar 00 <?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 = %s
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;",
[
'attachment' === $postType ? 'inherit' : 'publish',
$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 = %s
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}
",
[
'attachment' === $postType ? 'inherit' : 'publish',
$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;
}
} Priority.php 0000666 00000016577 15114670646 0007134 0 ustar 00 <?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'];
}
} Image/Image.php 0000666 00000022536 15114670646 0007347 0 ustar 00 <?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;
}
// If the image URL is not external, make it relative.
// This is important for users who scan their sites in a local/staging environment and then
// push the data to production.
if ( ! aioseo()->helpers->isExternalUrl( $imageUrl ) ) {
$imageUrl = aioseo()->helpers->makeUrlRelative( $imageUrl );
}
$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;
}
} Image/ThirdParty.php 0000666 00000017641 15114670646 0010420 0 ustar 00 <?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'] );
}
}
}
} Xsl.php 0000666 00000012230 15114670646 0006037 0 ustar 00 <?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 ) ?: '';
// These variables are used in the XSL file.
// phpcs:disable VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable
$linksPerIndex = aioseo()->options->sitemap->general->linksPerIndex;
$advanced = aioseo()->options->sitemap->general->advancedSettings->enable;
$excludeImages = aioseo()->sitemap->helpers->excludeImages();
$sitemapParams = aioseo()->helpers->getParametersFromUrl( $sitemapUrl );
$xslParams = aioseo()->core->cache->get( 'aioseo_sitemap_' . aioseo()->sitemap->requestParser->cleanSlug( $sitemapPath ) );
// phpcs:enable VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable
if ( ! empty( $sitemapInfo[1] ) ) {
switch ( $sitemapInfo[1] ) {
case 'addl':
$sitemapName = __( 'Additional Pages', 'all-in-one-seo-pack' );
// phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable
$excludeImages = true;
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;
// 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 );
}
} RequestParser.php 0000666 00000016473 15114670646 0010113 0 ustar 00 <?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;
}
}
}
}
} Content.php 0000666 00000074527 15114670646 0006724 0 ustar 00 <?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 ) {
$entry = [
'loc' => $url,
'lastmod' => aioseo()->sitemap->helpers->lastModifiedPostTime( $postType ),
'changefreq' => aioseo()->sitemap->priority->frequency( 'archive' ),
'priority' => aioseo()->sitemap->priority->priority( 'archive' ),
];
// To be consistent with our other entry filters, we need to pass the entry ID as well, but as null in this case.
$entries[] = apply_filters( 'aioseo_sitemap_archive_entry', $entry, null, $postType, '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 ) {
$entry = [
'loc' => ! empty( $authorData->authorUrl )
? $authorData->authorUrl
: get_author_posts_url( $authorData->ID, $authorData->nicename ?: '' ),
'lastmod' => aioseo()->helpers->dateTimeToIso8601( $authorData->lastModified ),
'changefreq' => aioseo()->sitemap->priority->frequency( 'author' ),
'priority' => aioseo()->sitemap->priority->priority( 'author' )
];
$entries[] = apply_filters( 'aioseo_sitemap_author_entry', $entry, $authorData->ID, $authorData->nicename, '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[] = apply_filters( 'aioseo_sitemap_date_entry', $entry, $date, 'year', 'date' );
}
$entry['loc'] = get_month_link( $date->year, $date->month );
$entries[] = apply_filters( 'aioseo_sitemap_date_entry', $entry, $date, 'month', 'date' );
}
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 ) )
];
// If the entry is the homepage, we need to check if the permalink structure
// does not have a trailing slash. If so, we need to strip it because WordPress adds it
// regardless for the home_url() in get_page_link() which is used in the get_permalink() function.
static $homeId = null;
if ( null === $homeId ) {
$homeId = get_option( 'page_for_posts' );
}
if ( aioseo()->helpers->getHomePageId() === $post->ID ) {
$entry['guid'] = aioseo()->helpers->maybeRemoveTrailingSlash( $entry['guid'] );
}
$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' )
->orderBy( '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, tt.taxonomy, at.frequency, at.priority' : 'tt.term_id, tt.taxonomy';
}
$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, $term->taxonomy, 'term' );
}
return $entries;
}
} File.php 0000666 00000020235 15114670646 0006154 0 ustar 00 <?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;
}
} SitemapAbstract.php 0000666 00000004054 15114670646 0010364 0 ustar 00 <?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 = '';
} Helpers.php 0000666 00000043214 15114670646 0006701 0 ustar 00 <?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;
// phpcs:disable WordPress.PHP.DevelopmentFunctions
error_log( wp_json_encode( "$indexName index of $type sitemap generated in $time seconds using a maximum of $memory mb of memory." ) );
// phpcs:enable WordPress.PHP.DevelopmentFunctions
}
/**
* 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 aioseo()->helpers->isValidDate( $page->lastModified ) ? gmdate( 'c', strtotime( $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;
}
} Query.php 0000666 00000027332 15114670646 0006407 0 ustar 00 <?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.
$maxAge = '';
$fields = implode( ', ', [
'p.ID',
'p.post_excerpt',
'p.post_type',
'p.post_password',
'p.post_parent',
'p.post_date_gmt',
'p.post_modified_gmt',
'ap.priority',
'ap.frequency'
] );
if ( in_array( aioseo()->sitemap->type, [ 'html', 'rss' ], true ) ) {
$fields .= ', p.post_title';
}
if ( 'general' !== aioseo()->sitemap->type || ! 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->orderByRaw( $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();
}
} Html/Widget.php 0000666 00000013607 15114670646 0007431 0 ustar 00 <?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;
}
} Html/Frontend.php 0000666 00000031777 15114670646 0007775 0 ustar 00 <?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;
}
} Html/CompactArchive.php 0000666 00000005677 15114670646 0011106 0 ustar 00 <?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;
}
} Html/Sitemap.php 0000666 00000014025 15114670646 0007603 0 ustar 00 <?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 );
}
}
} Html/Query.php 0000666 00000014575 15114670646 0007320 0 ustar 00 <?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' )
->orderBy( '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;
}
} Html/Shortcode.php 0000666 00000001336 15114670646 0010134 0 ustar 00 <?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 );
}
} Html/Block.php 0000666 00000006636 15114670646 0007244 0 ustar 00 <?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 );
}
} Localization.php 0000666 00000024757 15114670646 0007742 0 ustar 00 <?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 ( apply_filters( 'aioseo_sitemap_localization_disable', false ) ) {
return;
}
if ( aioseo()->helpers->isWpmlActive() ) {
self::$wpml = [
'defaultLanguage' => apply_filters( 'wpml_default_language', null ),
'activeLanguages' => apply_filters( 'wpml_active_languages', null )
];
add_filter( 'aioseo_sitemap_term', [ $this, 'localizeWpml' ], 10, 4 );
add_filter( 'aioseo_sitemap_post', [ $this, 'localizeWpml' ], 10, 4 );
}
if ( aioseo()->helpers->isPluginActive( 'weglot' ) ) {
add_filter( 'aioseo_sitemap_term', [ $this, 'localizeWeglot' ], 10, 4 );
add_filter( 'aioseo_sitemap_post', [ $this, 'localizeWeglot' ], 10, 4 );
add_filter( 'aioseo_sitemap_author_entry', [ $this, 'localizeWeglot' ], 10, 4 );
add_filter( 'aioseo_sitemap_archive_entry', [ $this, 'localizeWeglot' ], 10, 4 );
add_filter( 'aioseo_sitemap_date_entry', [ $this, 'localizeWeglot' ], 10, 4 );
add_filter( 'aioseo_sitemap_product_attributes', [ $this, 'localizeWeglot' ], 10, 4 );
}
}
/**
* Localize the entries for Weglot.
*
* @since 4.8.3
*
* @param array $entry The entry.
* @param mixed $entryId The object ID, null or a date object.
* @param string $objectName The post type, taxonomy name or date type ('year' or 'month').
* @param string|null $entryType Whether the entry represents a post, term, author, archive or date.
* @return array The entry.
*/
public function localizeWeglot( $entry, $entryId, $objectName, $entryType = null ) {
try {
$originalLang = function_exists( 'weglot_get_original_language' ) ? weglot_get_original_language() : '';
$translations = function_exists( 'weglot_get_destination_languages' ) ? weglot_get_destination_languages() : [];
if ( empty( $originalLang ) || empty( $translations ) ) {
return $entry;
}
switch ( $entryType ) {
case 'post':
$permalink = get_permalink( $entryId );
break;
case 'term':
$permalink = get_term_link( $entryId, $objectName );
break;
case 'author':
$permalink = get_author_posts_url( $entryId, $objectName );
break;
case 'archive':
$permalink = get_post_type_archive_link( $objectName );
break;
case 'date':
$permalink = 'year' === $objectName ? get_year_link( $entryId->year ) : get_month_link( $entryId->year, $entryId->month );
break;
default:
$permalink = '';
}
$entry['languages'] = [];
foreach ( $translations as $translation ) {
// If the translation is not public we skip it.
if ( empty( $translation['public'] ) ) {
continue;
}
$l10nPermalink = $this->weglotGetLocalizedUrl( $permalink, $translation['language_to'] );
if ( ! empty( $l10nPermalink ) ) {
$entry['languages'][] = [
'language' => $translation['language_to'],
'location' => $l10nPermalink
];
}
}
// 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['languages'] ) ) {
$entry['languages'][] = [
'language' => $originalLang,
'location' => aioseo()->helpers->decodeUrl( $entry['loc'] )
];
} else {
unset( $entry['languages'] );
}
return $this->validateSubentries( $entry );
} catch ( \Exception $e ) {
// Do nothing. It only exists because some "weglot" functions above throw exceptions.
}
return $entry;
}
/**
* Localize the entries for WPML.
*
* @since 4.0.0
* @version 4.8.3 Rename from localizeEntry to localizeWpml.
*
* @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 localizeWpml( $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' => aioseo()->helpers->decodeUrl( $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' => aioseo()->helpers->decodeUrl( $entry['loc'] )
];
} else {
unset( $entry['languages'] );
}
return $this->validateSubentries( $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;
}
/**
* Retrieves the localized URL.
*
* @since 4.8.3
*
* @param string $url The page URL to localize.
* @param string $code The language code (e.g. 'br', 'en').
* @return string|false The localized URL or false if it fails.
*/
private function weglotGetLocalizedUrl( $url, $code ) {
try {
if (
! $url ||
! function_exists( 'weglot_get_service' )
) {
return false;
}
$languageService = weglot_get_service( 'Language_Service_Weglot' );
$requestUrlService = weglot_get_service( 'Request_Url_Service_Weglot' );
$wgUrl = $requestUrlService->create_url_object( $url );
$language = $languageService->get_language_from_internal( $code );
return $wgUrl->getForLanguage( $language );
} catch ( \Exception $e ) {
// Do nothing. It only exists because some "weglot" functions above throw exceptions.
}
return false;
}
} Output.php 0000666 00000011644 15114670646 0006601 0 ustar 00 <?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;
}
// Clear all output buffers to avoid conflicts.
aioseo()->helpers->clearBuffers();
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' ) ) );
}
} Sitemap.php 0000666 00000025377 15114670646 0006713 0 ustar 00 <?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() {}
}