content = new Content(); $this->root = new Root(); $this->query = new Query(); $this->file = new File(); $this->image = new Image(); $this->ping = new Ping(); $this->priority = new Priority(); $this->output = new Output(); $this->helpers = new Helpers(); // Disables the built-in WP sitemap. if ( aioseo()->options->sitemap->general->enable ) { remove_action( 'init', 'wp_sitemaps_get_server' ); add_filter( 'wp_sitemaps_enabled', '__return_false' ); add_filter( 'pre_handle_404', [ $this, 'redirectOtherSitemaps' ] ); } add_action( 'aioseo_static_sitemap_regeneration', [ $this, 'regenerateStaticSitemap' ] ); } /** * Adds our hooks. * * @since 4.0.0 * * @return void */ public function init() { // Watch for XSL requests. add_action( 'wp_loaded', [ $this, 'xsl' ] ); // Add rewrite rules. $class = new \ReflectionClass( new Rewrite ); add_action( 'wp_loaded', [ $class->getName(), 'updateRewriteRules' ] ); // Remove trailing slash if the sitemap is requested. add_filter( 'redirect_canonical', [ $this, 'untrailUrl' ], 10, 2 ); // Parse the request to see if the sitemap should be returned. // This doesn't run if a static file is requested. add_filter( 'query_vars', [ $this, 'addWhitelistParams' ] ); add_action( 'template_redirect', [ $this, 'checkUrlParams' ], 10, 1 ); // 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' ] ); } /** * 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 $index => $filename ) { if ( preg_match( '#.*sitemap.*#', $filename ) ) { // We don't want to delete the video sitemap here at all. $isVideoSitemap = preg_match( '#.*video.*#', $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(); } /** * Removes the trailing slash from the redirect URL. * * @since 4.0.0 * * @param string $redirect The redirect URL. * @param string $request The requested URL. * @return string Either the original requested URL for the sitemap or the redirect URL. */ public function untrailUrl( $redirect, $request ) { if ( preg_match( '#(.*sitemap[0-9]*?.xml|.*sitemap[0-9]*?.xml.gz|.*sitemap.rss)$#i', $request ) ) { return $request; } return $redirect; } /** * Adds our sitemap params to the query vars whitelist. * * @since 4.0.0 * * @param array $params The array of whitelisted query variable names. * @return array $params The filtered array of whitelisted query variable names. */ public function addWhitelistParams( $params ) { $params[] = 'aiosp_sitemap_path'; if ( aioseo()->options->sitemap->general->indexes && ! isset( $params['aiosp_sitemap_page'] ) ) { $params[] = 'aiosp_sitemap_page'; } foreach ( $this->addons as $addon => $classes ) { if ( ! empty( $classes['sitemap'] ) ) { $params = $classes['sitemap']->addWhitelistParams( $params ); } } return $params; } /** * Checks whether one of our sitemaps is being requested. * * @since 4.0.0 * * @return void */ public function checkUrlParams() { global $wp_query; if ( ! empty( $wp_query->query_vars['aiosp_sitemap_path'] ) ) { $this->generate(); } } /** * Generates the requested sitemap. * * @since 4.0.0 * * @return void */ private function generate() { // This is a hack to prevent WordPress from running it's default stuff during our processing. global $wp_query; $wp_query->is_home = false; // This prevents the sitemap from including terms twice when WPML is active. if ( class_exists( 'SitePress' ) ) { global $sitepress_settings; // 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; } // Sets context class properties. $this->determineContext(); // 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 ) { $this->notFoundPage(); return; } $entries = aioseo()->sitemap->content->get(); $total = aioseo()->sitemap->content->getTotal(); if ( ! $entries ) { foreach ( $this->addons as $addon => $classes ) { if ( ! empty( $classes['content'] ) ) { $entries = $classes['content']->get(); $total = count( $entries ); if ( method_exists( $classes['content'], 'getTotal' ) ) { $total = $classes['content']->getTotal(); } if ( $entries ) { break; } } } } if ( 0 === $total && empty( $entries ) ) { status_header( 404 ); } global $wp; $this->saveXslData( $wp->request, $entries, $total ); $this->headers(); aioseo()->sitemap->output->output( $entries ); foreach ( $this->addons as $addon => $classes ) { if ( ! empty( $classes['output'] ) ) { $classes['output']->output( $entries ); } } exit(); } /** * Save the data to use on 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' ); 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()->cache->update( 'aioseo_sitemap_' . $fileName, $data, MONTH_IN_SECONDS ); } /** * Determines the current sitemap context. * * @since 4.0.0 * * @return void */ protected function determineContext() { global $wp_query; $this->type = 'rss' === $wp_query->query_vars['aiosp_sitemap_path'] ? 'rss' : 'general'; $this->filename = 'sitemap'; $this->indexName = $wp_query->query_vars['aiosp_sitemap_path']; $this->pageNumber = ! empty( $wp_query->query_vars['aiosp_sitemap_page'] ) ? $wp_query->query_vars['aiosp_sitemap_page'] - 1 : 0; $this->indexes = aioseo()->options->sitemap->general->indexes; $this->linksPerIndex = aioseo()->options->sitemap->{$this->type}->linksPerIndex; $this->offset = $this->linksPerIndex * $this->pageNumber; // The sitemap isn't statically generated if we get here. $this->isStatic = false; foreach ( $this->addons as $addon => $classes ) { if ( ! empty( $classes['sitemap'] ) ) { $classes['sitemap']->determineContext(); } } if ( $this->linksPerIndex > 50000 ) { $this->linksPerIndex = 50000; } } /** * 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() { foreach ( $this->addons as $addon => $classes ) { if ( ! empty( $classes['sitemap'] ) ) { $classes['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 ( ! file_exists( get_home_path() . $_SERVER['REQUEST_URI'] ) ) { $this->scheduleRegeneration(); } } /** * Sets the HTTP headers for the sitemap. * * @since 4.0.0 * * @return void */ public function headers() { $charset = get_option( 'blog_charset' ); header( "Content-Type: text/xml; charset=$charset", true ); header( 'X-Robots-Tag: noindex, follow', true ); } /** * Redirects to a 404 Not Found page if the sitemap is disabled. * * @since 4.0.0 * * @return void */ public function notFoundPage() { global $wp_query; $wp_query->set_404(); status_header( 404 ); include( get_404_template() ); exit(); } /** * Returns the sitemap stylesheet. * * @since 4.0.0 * * @return void */ public function xsl() { $requestUrl = ! empty( $_SERVER['REQUEST_URI'] ) ? $_SERVER['REQUEST_URI'] : ''; $requestPath = wp_parse_url( $requestUrl, PHP_URL_PATH ); if ( ! empty( $requestPath ) && preg_match( '#(/default\.xsl)$#i', $requestPath ) ) { $this->headers(); $charset = get_option( 'blog_charset' ); $sitemapUrl = wp_get_referer(); $sitemapPath = wp_parse_url( $sitemapUrl, PHP_URL_PATH ); $sitemapName = strtoupper( pathinfo( $sitemapPath, PATHINFO_EXTENSION ) ); // Get Sitemap info by URL. preg_match( '/\/(.*?)-?sitemap([0-9]*)\.xml/', $sitemapPath, $sitemapInfo ); if ( ! empty( $sitemapInfo[1] ) ) { switch ( $sitemapInfo[1] ) { case 'addl': $sitemapName = __( 'Additional Pages', 'all-in-one-seo-pack' ); break; case 'post-archive': $sitemapName = __( 'Post Archive', 'all-in-one-seo-pack' ); break; default: if ( post_type_exists( $sitemapInfo[1] ) ) { $postTypeObject = get_post_type_object( $sitemapInfo[1] ); $sitemapName = $postTypeObject->labels->singular_name; } if ( taxonomy_exists( $sitemapInfo[1] ) ) { $taxonomyObject = get_taxonomy( $sitemapInfo[1] ); $sitemapName = $taxonomyObject->labels->singular_name; } break; } } $currentPage = ! empty( $sitemapInfo[2] ) ? (int) $sitemapInfo[2] : 1; // phpcs:disable VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable $linksPerIndex = aioseo()->options->sitemap->general->linksPerIndex; $advanced = aioseo()->options->sitemap->general->advancedSettings->enable; $excludeImages = aioseo()->options->sitemap->general->advancedSettings->excludeImages; $sitemapParams = aioseo()->helpers->getParametersFromUrl( $sitemapUrl ); $xslParams = aioseo()->cache->get( 'aioseo_sitemap_' . trim( $sitemapPath, '/' ) ); // phpcs:enable VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable // Translators: 1 - The sitemap name, 2 - The current page. $title = sprintf( __( '%s Sitemap %s', 'all-in-one-seo-pack' ), $sitemapName, $currentPage > 1 ? $currentPage : '' ); $title = trim( $title ); echo ''; include_once( AIOSEO_DIR . '/app/Common/Views/sitemap/xsl/default.php' ); exit; } foreach ( $this->addons as $addon => $classes ) { if ( ! empty( $classes['sitemap'] ) ) { $classes['sitemap']->xsl(); } } } /** * Redirects sitemaps from other WordPress Core/other plugins to ours. * * @since 4.1.5 * * @param bool $preempt Whether to short-circuit default header status handling. * @return bool Whether to short-circuit default header status handling. */ public function redirectOtherSitemaps( $preempt ) { $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', ] ]; foreach ( aioseo()->addons->getLoadedAddons() as $addonName => $loadedAddon ) { if ( ! empty( $loadedAddon->helpers ) && method_exists( $loadedAddon->helpers, 'getOtherSitemapPatterns' ) ) { $sitemapPatterns[ $addonName ] = $loadedAddon->helpers->getOtherSitemapPatterns(); } } $sitemapPatterns = apply_filters( 'aioseo_sitemap_redirect_filenames', $sitemapPatterns ); foreach ( $sitemapPatterns as $type => $patterns ) { foreach ( $patterns as $pattern ) { if ( preg_match( "/^$pattern$/i", $GLOBALS['wp']->request ) ) { wp_safe_redirect( aioseo()->sitemap->helpers->getUrl( $type ) ); exit(); } } } return $preempt; } /** * Registers an active sitemap addon and its classes. * * @since 4.0.0 * * @return void */ public function addAddon( $name, $classes ) { $this->addons[ $name ] = $classes; } }