URLs * * @since 2.11 * * @var null|array $image_ids_urls { * @type string $base_url The URL for the original sized image. * @type string ${$id} Contains the URLs associated to the IDs. * } */ private $image_ids_urls = null; /** * All_in_One_SEO_Pack_Sitemap constructor. * * @since ? */ public function __construct() { if ( get_class( $this ) === 'All_in_One_SEO_Pack_Sitemap' ) { // Set this up only when instantiated as this class. $this->name = __( 'XML Sitemap', 'all-in-one-seo-pack' ); // Human-readable name of the plugin. $this->prefix = 'aiosp_sitemap_'; // Option prefix. $this->file = __FILE__; // The current file. $this->extra_sitemaps = array(); $this->extra_sitemaps = apply_filters( $this->prefix . 'extra', $this->extra_sitemaps ); } parent::__construct(); // TODO This could be move up to the class field default/initial values. $this->comment_string = 'Sitemap %s generated by ' . AIOSEOP_PLUGIN_NAME . ' %s by Michael Torbert of Semper Fi Web Design on %s'; $this->default_options = array( 'rss_sitemap' => array( 'name' => __( 'Create RSS Sitemap', 'all-in-one-seo-pack' ) ), 'daily_cron' => array( 'name' => __( 'Schedule Updates', 'all-in-one-seo-pack' ), 'type' => 'select', 'initial_options' => array( 0 => __( 'No Schedule', 'all-in-one-seo-pack' ), 'daily' => __( 'Daily', 'all-in-one-seo-pack' ), 'weekly' => __( 'Weekly', 'all-in-one-seo-pack' ), 'monthly' => __( 'Monthly', 'all-in-one-seo-pack' ), ), 'default' => 0, ), 'indexes' => array( 'name' => __( 'Enable Sitemap Indexes', 'all-in-one-seo-pack' ), 'default' => 'on', ), 'max_posts' => array( 'name' => __( 'Maximum Posts Per Sitemap Page', 'all-in-one-seo-pack' ), 'type' => 'text', 'default' => 1000, 'condshow' => array( "{$this->prefix}indexes" => 'on', "{$this->prefix}indexes" => 'on', ), ), 'posttypes' => array( 'name' => __( 'Post Types', 'all-in-one-seo-pack' ), 'type' => 'multicheckbox', 'default' => 'all', ), 'taxonomies' => array( 'name' => __( 'Taxonomies', 'all-in-one-seo-pack' ), 'type' => 'multicheckbox', 'default' => 'all', ), 'archive' => array( 'name' => __( 'Include Date Archive Pages', 'all-in-one-seo-pack' ) ), 'author' => array( 'name' => __( 'Include Author Pages', 'all-in-one-seo-pack' ) ), 'images' => array( 'name' => __( 'Exclude Images', 'all-in-one-seo-pack' ) ), 'rewrite' => array( 'name' => __( 'Dynamically Generate Sitemap', 'all-in-one-seo-pack' ), 'default' => 'On', ), ); $status_options = array( 'link' => array( 'default' => '', 'type' => 'html', 'label' => 'none', 'save' => false, ), ); $this->layout = array( 'status' => array( 'name' => __( 'Sitemap Status', 'all-in-one-seo-pack' ), 'help_link' => 'https://semperplugins.com/documentation/xml-sitemaps-module/', 'options' => array_keys( $status_options ), ), 'default' => array( 'name' => $this->name, 'help_link' => 'https://semperplugins.com/documentation/xml-sitemaps-module/', 'options' => array_keys( $this->default_options ), ), ); $prio = array(); for ( $i = 0; $i <= 10; $i ++ ) { $str = sprintf( '%0.1f', $i / 10.0 ); $prio[ $str ] = $str; } $arr_no = array( 'no' => __( 'Do Not Override', 'all-in-one-seo-pack' ) ); $arr_sel = array( 'sel' => __( 'Select Individual', 'all-in-one-seo-pack' ) ); $this->prio_sel = array_merge( $arr_no, $arr_sel, $prio ); $this->prio = array_merge( $arr_no, $prio ); $freq = array(); foreach ( array( 'always', 'hourly', 'daily', 'weekly', 'monthly', 'yearly', 'never' ) as $f ) { $freq[ $f ] = $f; } $this->freq_sel = array_merge( $arr_no, $arr_sel, $freq ); $this->freq = array_merge( $arr_no, $freq ); foreach ( array( 'prio' => __( 'priority', 'all-in-one-seo-pack' ), 'freq' => __( 'frequency', 'all-in-one-seo-pack' ), ) as $k => $v ) { $s = "{$k}_options"; $$s = array(); foreach ( array( 'homepage' => __( 'homepage', 'all-in-one-seo-pack' ), 'post' => __( 'posts', 'all-in-one-seo-pack' ), 'taxonomies' => __( 'taxonomies', 'all-in-one-seo-pack' ), 'archive' => __( 'archive pages', 'all-in-one-seo-pack' ), 'author' => __( 'author pages', 'all-in-one-seo-pack' ), ) as $opt => $val ) { $arr = $$s; if ( ( 'post' === $opt ) || ( 'taxonomies' === $opt ) ) { $iopts = $this->{"{$k}_sel"}; } else { $iopts = $this->$k; } $arr[ $k . '_' . $opt ] = array( 'name' => $this->ucwords( $val ), 'type' => 'select', 'initial_options' => $iopts, 'default' => 'no', ); if ( ( 'archive' === $opt ) || ( 'author' === $opt ) ) { $arr[ $k . '_' . $opt ]['condshow'] = array( $this->prefix . $opt => 'on' ); } $$s = $arr; } } $addl_options = array( 'addl_instructions' => array( 'default' => '
' . __( 'Enter information below for any additional links for your sitemap not already managed through WordPress.', 'all-in-one-seo-pack' ) . '

', 'type' => 'html', 'label' => 'none', 'save' => false, ), 'addl_url' => array( 'name' => __( 'Page URL', 'all-in-one-seo-pack' ), 'type' => 'url', 'save' => false, ), 'addl_prio' => array( 'name' => __( 'Page Priority', 'all-in-one-seo-pack' ), 'type' => 'select', 'initial_options' => $prio, 'save' => false, ), 'addl_freq' => array( 'name' => __( 'Page Frequency', 'all-in-one-seo-pack' ), 'type' => 'select', 'initial_options' => $freq, 'save' => false, ), 'addl_mod' => array( 'name' => __( 'Last Modified', 'all-in-one-seo-pack' ), 'type' => 'date', 'save' => false, 'placeholder' => 'yyyy-mm-dd', 'class' => 'aiseop-date', ), 'addl_pages' => array( 'name' => __( 'Additional Pages', 'all-in-one-seo-pack' ), 'type' => 'custom', 'save' => true, ), 'Submit' => array( 'type' => 'submit', 'class' => 'button-primary', 'name' => __( 'Add URL', 'all-in-one-seo-pack' ) . ' »', 'style' => 'margin-left: 20px;', 'label' => 'none', 'save' => false, 'value' => 1, ), ); $excl_options = array( 'excl_terms' => array( 'name' => __( 'Excluded Terms', 'all-in-one-seo-pack' ), 'type' => 'multiselect', 'class' => 'aioseop-exclude-terms', ), 'excl_pages' => array( 'name' => __( 'Excluded Pages', 'all-in-one-seo-pack' ), 'type' => 'text', ), ); $this->layout['addl_pages'] = array( 'name' => __( 'Additional Pages', 'all-in-one-seo-pack' ), 'help_link' => 'https://semperplugins.com/documentation/xml-sitemaps-module/#additional-pages', 'options' => array_keys( $addl_options ), ); $this->layout['excl_pages'] = array( 'name' => __( 'Excluded Items', 'all-in-one-seo-pack' ), 'help_link' => 'https://semperplugins.com/documentation/xml-sitemaps-module/#excluded-items', 'options' => array_keys( $excl_options ), ); $this->layout['priorities'] = array( 'name' => __( 'Priorities', 'all-in-one-seo-pack' ), 'help_link' => 'https://semperplugins.com/documentation/xml-sitemaps-module/#priorities-and-frequencies', // TODO Fix undefined variable. 'options' => array_keys( $prio_options ), ); $this->layout['frequencies'] = array( 'name' => __( 'Frequencies', 'all-in-one-seo-pack' ), 'help_link' => 'https://semperplugins.com/documentation/xml-sitemaps-module/#priorities-and-frequencies', // TODO Fix undefined variable. 'options' => array_keys( $freq_options ), ); $this->default_options = array_merge( $status_options, $this->default_options, $addl_options, $excl_options, $prio_options, $freq_options ); add_action( 'after_doing_aioseop_updates', array( $this, 'do_sitemaps', ) ); // Update static sitemap when AIOSEOP is upgrade to new version. add_action( 'init', array( $this, 'load_sitemap_options' ) ); add_action( $this->prefix . 'settings_update', array( $this, 'do_sitemaps' ) ); add_filter( $this->prefix . 'display_settings', array( $this, 'update_post_data' ) ); add_filter( $this->prefix . 'display_options', array( $this, 'filter_display_options' ) ); add_filter( $this->prefix . 'update_options', array( $this, 'filter_options' ) ); add_filter( $this->prefix . 'output_option', array( $this, 'display_custom_options' ), 10, 2 ); add_action( $this->prefix . 'daily_update_cron', array( $this, 'daily_update' ) ); add_action( 'init', array( $this, 'make_dynamic_xsl' ) ); // TODO is this required for dynamic sitemap? add_action( 'transition_post_status', array( $this, 'update_sitemap_from_posts' ), 10, 3 ); add_action( 'after_doing_aioseop_updates', array( $this, 'scan_sitemaps' ) ); add_action( 'admin_init', array( $this, 'sitemap_notices' ) ); } /** * Sitemap Notices * * @todo Move admin notice functions. Possibly to where it is first saved & loaded (`load_sitemap_options`). * * @global AIOSEOP_Notices $aioseop_notices * * @since 2.4.1 */ public function sitemap_notices() { if ( ! current_user_can( 'aiosp_manage_seo' ) ) { return; } global $aioseop_notices; $options = $this->options; if ( ( isset( $options[ "{$this->prefix}indexes" ] ) && 'on' !== $options[ "{$this->prefix}indexes" ] ) || ( isset( $options[ "{$this->prefix}indexes" ] ) && 'on' === $options[ "{$this->prefix}indexes" ] && 1000 < $options[ "{$this->prefix}max_posts" ] ) ) { $num_terms = 0; $post_counts = $this->get_total_post_count( array( 'post_type' => $options[ "{$this->prefix}posttypes" ], 'post_status' => 'publish', ) ); $term_counts = $this->get_all_term_counts( array( 'taxonomy' => $options[ "{$this->prefix}taxonomies" ] ) ); if ( isset( $term_counts ) && is_array( $term_counts ) ) { $num_terms = array_sum( $term_counts ); } $sitemap_urls = $post_counts + $num_terms; if ( 1000 < $sitemap_urls ) { $aioseop_notices->activate_notice( 'sitemap_max_warning' ); } else { $aioseop_notices->deactivate_notice( 'sitemap_max_warning' ); } } else { $aioseop_notices->deactivate_notice( 'sitemap_max_warning' ); } } /** * Update Sitemap from Posts * * @since 2.3.6 * * @param $new_status * @param $old_status * @param $post */ public function update_sitemap_from_posts( $new_status, $old_status, $post ) { // ignore WP API requests. if ( defined( 'REST_REQUEST' ) && REST_REQUEST ) { return; } if ( $this->option_isset( 'rewrite' ) ) { // TODO if dynamic, delete transient (we currently don't do transients). return; } $posttypes = array(); if ( ! empty( $this->options[ "{$this->prefix}posttypes" ] ) ) { $posttypes = $this->options[ "{$this->prefix}posttypes" ]; } if ( ! in_array( $post->post_type, $posttypes, true ) ) { return; } $statuses_for_updating = array( 'new', 'publish', 'trash' ); if ( ! in_array( $new_status, $statuses_for_updating, true ) ) { return; } if ( defined( 'AIOSEOP_UNIT_TESTING' ) ) { $this->do_sitemaps(); } elseif ( ! has_action( 'shutdown', array( $this, 'do_sitemaps' ) ) ) { /** * Defer do_sitemaps until after everything is done. * And run it only once regardless of posts updated. */ add_action( 'shutdown', array( $this, 'do_sitemaps' ) ); } } /** * Add Cron Schedules * * Add new intervals of a week and a month. * * @since ? * * @link https://codex.wordpress.org/Plugin_API/Filter_Reference/cron_schedules * * @param $schedules * @return mixed */ public function add_cron_schedules( $schedules ) { $schedules['weekly'] = array( 'interval' => 604800, // 1 week in seconds. 'display' => __( 'Once Weekly', 'all-in-one-seo-pack' ), ); $schedules['monthly'] = array( 'interval' => 2629740, // 1 month in seconds. 'display' => __( 'Once Monthly', 'all-in-one-seo-pack' ), ); return $schedules; } /** * Cron Update * * @since ? */ public function cron_update() { add_filter( 'cron_schedules', array( $this, 'add_cron_schedules' ) ); if ( ! wp_next_scheduled( $this->prefix . 'daily_update_cron' ) ) { wp_schedule_event( time(), $this->options[ $this->prefix . 'daily_cron' ], $this->prefix . 'daily_update_cron' ); } } /** * Daily Update * * @since ? */ public function daily_update() { $last_run = get_option( $this->prefix . 'cron_last_run' ); if ( empty( $last_run ) || ( time() - $last_run > 23.5 * 60 * 60 ) ) { // Sanity check. $this->do_sitemaps( __( 'Daily scheduled sitemap check has finished.', 'all-in-one-seo-pack' ) ); } $last_run = time(); update_option( $this->prefix . 'cron_last_run', $last_run ); } /** * Admin Enqueue Scripts * * Hook function to enqueue scripts and localize data to scripts. * * @since 3.0 * * @see 'admin_enqueue_scripts' hook * @link https://developer.wordpress.org/reference/hooks/admin_enqueue_scripts/ * * @param string $hook_suffix The current admin page. */ public function admin_enqueue_scripts( $hook_suffix ) { parent::admin_enqueue_scripts( $hook_suffix ); if ( $this->pagehook !== $hook_suffix ) { return; } wp_enqueue_script( 'aioseop-selectize', 'https://cdnjs.cloudflare.com/ajax/libs/selectize.js/0.12.6/js/standalone/selectize.min.js', array( 'jquery' ), AIOSEOP_VERSION ); wp_enqueue_script( 'aioseop-search-terms', AIOSEOP_PLUGIN_URL . 'js/modules/aioseop_sitemap.js', array( 'jquery' ), AIOSEOP_VERSION, true ); } /** * Admin Enqueue Styles * * Load styles for module. * * @since 3.0 * * @see 'admin_enqueue_scripts' hook * @link https://developer.wordpress.org/reference/hooks/admin_enqueue_scripts/ * * @param string $hook_suffix The current admin page. */ public function admin_enqueue_styles( $hook_suffix ) { parent::admin_enqueue_styles( $hook_suffix ); if ( $this->pagehook !== $hook_suffix ) { return; } wp_enqueue_style( 'aioseop-selectize', 'https://cdnjs.cloudflare.com/ajax/libs/selectize.js/0.12.6/css/selectize.css', false, AIOSEOP_VERSION, false ); wp_enqueue_style( 'aioseop-selectize-default', 'https://cdnjs.cloudflare.com/ajax/libs/selectize.js/0.12.6/css/selectize.default.min.css', false, AIOSEOP_VERSION, false ); } /** * Load Sitemap Options * * Initialize options, after constructor. * * @since ? */ public function load_sitemap_options() { // Load initial options / set defaults. $this->update_options(); if ( ! empty( $this->options[ "{$this->prefix}indexes" ] ) ) { if ( $this->options[ "{$this->prefix}max_posts" ] && ( $this->options[ "{$this->prefix}max_posts" ] > 0 ) && ( $this->options[ "{$this->prefix}max_posts" ] < 50000 ) ) { $this->max_posts = $this->options[ "{$this->prefix}max_posts" ]; } } if ( is_multisite() ) { $this->options[ "{$this->prefix}rewrite" ] = 'On'; } if ( $this->options[ "{$this->prefix}rewrite" ] ) { $this->setup_rewrites(); } /** * Filters whether to display the URL to the XML Sitemap on our virtual robots.txt file. * * Defaults to true. Return __return_false in order to not display the URL. * * @since 3.0 * * @param boolean Defaults to true. */ if ( apply_filters( 'aioseop_robotstxt_sitemap_url', true ) ) { add_action( 'do_robots', array( $this, 'do_robots' ), 100 ); } if ( isset( $this->options[ $this->prefix . 'daily_cron' ] ) && $this->options[ $this->prefix . 'daily_cron' ] ) { add_action( 'wp', array( $this, 'cron_update' ) ); } else { $time = wp_next_scheduled( $this->prefix . 'daily_update_cron' ); if ( $time ) { wp_unschedule_event( $time, $this->prefix . 'daily_update_cron' ); } } } /** * Display Custom Options * * Displays boxes for add pages to sitemap option. Requires WordPress 4.1. * * @since ? * * @param $buf * @param $args * @return string */ public function display_custom_options( $buf, $args ) { if ( "{$this->prefix}addl_pages" === $args['name'] ) { $buf .= "
"; if ( ! empty( $args['value'] ) ) { $buf .= "\n"; foreach ( $args['value'] as $k => $v ) { if ( is_object( $v ) ) { $v = (array) $v; } $buf .= "\t\n"; } $buf .= "
{$k}{$v['prio']}{$v['freq']}{$v['mod']}
\n"; } } $args['options']['type'] = 'hidden'; if ( ! empty( $args['value'] ) ) { $args['value'] = wp_json_encode( $args['value'] ); } else { $args['options']['type'] = 'html'; } if ( empty( $args['value'] ) ) { $args['value'] = ''; } $buf .= $this->get_option_html( $args ); $buf .= '
'; return $buf; } /** * Add Post Types * * Add post type details for settings once post types have been registered. * * @todo This function is being used to set up option values. This could possibly be refactored to something better suited. * * @since ? * @since 3.0 Add custom taxonomy support for Excluding Terms setting. (#240) */ public function add_post_types() { $post_type_titles = $this->get_post_type_titles( array( 'public' => true ) ); $taxonomy_titles = $this->get_taxonomy_titles( array( 'public' => true ) ); if ( isset( $post_type_titles['attachment'] ) ) { $post_type_titles['attachment'] = __( 'Media / Attachments', 'all-in-one-seo-pack' ); } $this->default_options['posttypes']['initial_options'] = array_merge( array( 'all' => __( 'All Post Types', 'all-in-one-seo-pack' ) ), $post_type_titles ); $this->default_options['taxonomies']['initial_options'] = array_merge( array( 'all' => __( 'All Taxonomies', 'all-in-one-seo-pack' ) ), $taxonomy_titles ); $this->default_options['posttypes']['default'] = array_keys( $this->default_options['posttypes']['initial_options'] ); $this->default_options['taxonomies']['default'] = array_keys( $this->default_options['taxonomies']['initial_options'] ); // Exclude Terms element items. $this->default_options['excl_terms']['initial_options'] = array(); $taxonomies_active = array(); if ( is_array( $this->options[ $this->prefix . 'taxonomies' ] ) ) { $taxonomies_active = $this->options[ $this->prefix . 'taxonomies' ]; } elseif ( ! empty( $this->options[ $this->prefix . 'taxonomies' ] ) ) { $taxonomies_active = array( $this->options[ $this->prefix . 'taxonomies' ] ); } $args_taxonomy_key = array_search( 'all', $taxonomies_active, true ); if ( false !== $args_taxonomy_key ) { // Remove 'all' as an invalid post_type. Use registered post_types selected instead. unset( $taxonomies_active[ $args_taxonomy_key ] ); // Adds all the taxonomies regardless if other taxonomies are selected; ensures all taxonomies are added. $taxonomies_active = array_merge( $taxonomies_active, get_taxonomies() ); } $excl_terms_init_opts = array(); foreach ( $taxonomies_active as $v1_taxonomy ) { $args_terms = array( 'taxonomy' => $v1_taxonomy, 'hide_empty' => false, ); $taxonomy_terms_tmp = $this->get_term_titles( $args_terms ); foreach ( $taxonomy_terms_tmp as $k2_id => $v2_term ) { $excl_terms_init_opts[ $v1_taxonomy . '-' . $k2_id ] = $v2_term . ' (' . $v1_taxonomy . ')'; } } $this->default_options['excl_terms']['initial_options'] = $excl_terms_init_opts; $post_name = ' ' . __( 'Post Type', 'all-in-one-seo-pack' ); $tax_name = ' ' . __( 'Taxonomy', 'all-in-one-seo-pack' ); foreach ( $post_type_titles as $k => $v ) { $key = 'prio_post_' . $k; $this->default_options = aioseop_array_insert_after( $this->default_options, 'prio_post', array( $key => array( 'name' => $v . $post_name, 'type' => 'select', 'initial_options' => $this->prio, 'default' => 'no', 'condshow' => array( "{$this->prefix}prio_post" => 'sel' ), ), ) ); $this->layout['priorities']['options'][] = $key; $key = 'freq_post_' . $k; $this->default_options = aioseop_array_insert_after( $this->default_options, 'freq_post', array( $key => array( 'name' => $v . $post_name, 'type' => 'select', 'initial_options' => $this->freq, 'default' => 'no', 'condshow' => array( "{$this->prefix}freq_post" => 'sel' ), ), ) ); $this->layout['frequencies']['options'][] = $key; } foreach ( $taxonomy_titles as $k => $v ) { $key = 'prio_taxonomies_' . $k; $this->default_options = aioseop_array_insert_after( $this->default_options, 'prio_taxonomies', array( $key => array( 'name' => $v . $tax_name, 'type' => 'select', 'initial_options' => $this->prio, 'default' => 'no', 'condshow' => array( "{$this->prefix}prio_taxonomies" => 'sel' ), ), ) ); $this->layout['priorities']['options'][] = $key; $key = 'freq_taxonomies_' . $k; $this->default_options = aioseop_array_insert_after( $this->default_options, 'freq_taxonomies', array( $key => array( 'name' => $v . $tax_name, 'type' => 'select', 'initial_options' => $this->freq, 'default' => 'no', 'condshow' => array( "{$this->prefix}freq_taxonomies" => 'sel' ), ), ) ); $this->layout['frequencies']['options'][] = $key; } $this->update_options(); } /** * Add Page Hooks * * Set up settings, checking for sitemap conflicts, on settings page. * * @since ? */ public function add_page_hooks() { $this->flush_rules_hook(); $this->add_post_types(); parent::add_page_hooks(); add_action( $this->prefix . 'settings_header', array( $this, 'do_sitemap_scan' ), 5 ); add_filter( "{$this->prefix}submit_options", array( $this, 'filter_submit' ) ); } /** * Filter Submit * * Change settings page submit button to read "Update Sitemap". * * @since ? * * @param $submit * @return mixed */ public function filter_submit( $submit ) { $submit['Submit']['value'] = __( 'Update Sitemap', 'all-in-one-seo-pack' ) . ' »'; return $submit; } /** * Updates Post Data * * Disable writing sitemaps to the filesystem for multisite. * * @since ? * * @param $options * @return mixed */ public function update_post_data( $options ) { if ( is_multisite() ) { $options[ $this->prefix . 'rewrite' ]['disabled'] = 'disabled'; } return $options; } /** * Get Rewrite URL * * @since ? * * @param $url * @return bool */ public function get_rewrite_url( $url ) { global $wp_rewrite; $url = wp_parse_url( esc_url( $url ), PHP_URL_PATH ); $url = ltrim( $url, '/' ); if ( ! empty( $wp_rewrite ) ) { $rewrite_rules = $wp_rewrite->rewrite_rules(); foreach ( $rewrite_rules as $k => $v ) { if ( preg_match( "@^$k@", $url ) ) { return $v; } } } return false; } /** * Get Filename * * Get the filename prefix for the sitemap file. * If a value was provided when this prefix was configurable from the settings page, return that instead of the default. * * @since 2.6 * * @return string */ protected function get_filename() { $filename = 'sitemap'; if ( ! empty( $this->options[ "{$this->prefix}filename" ] ) ) { $filename = $this->options[ "{$this->prefix}filename" ]; $filename = str_replace( '/', '', $filename ); } elseif ( 'aiosp_video_sitemap_' === $this->prefix ) { $filename = 'video-sitemap'; } /** * Filters the filename: aiosp_sitemap_filename OR aiosp_video_sitemap_filename. * * @param string $filename The file name. */ return apply_filters( "{$this->prefix}filename", $filename ); } /** * Filter Display Options * * Add in options for status display on settings page, sitemap rewriting on multisite. * * @since 2.3.6 * @since 2.3.12.3 Refactored to use aioseop_home_url() for compatibility purposes. * @since 3.0 Change 'excl_terms' to include taxonomy slugs with term id. (Pro #240) * @since 3.0 Remove WP < 3.5 old Privacy Settings link * * @param $options * @return mixed */ public function filter_display_options( $options ) { if ( is_multisite() ) { $options[ $this->prefix . 'rewrite' ] = 'On'; } if ( isset( $options[ $this->prefix . 'max_posts' ] ) && ( ( $options[ $this->prefix . 'max_posts' ] <= 0 ) || ( $options[ $this->prefix . 'max_posts' ] >= 50000 ) ) ) { $options[ $this->prefix . 'max_posts' ] = 50000; } $url = aioseop_home_url( '/' . $this->get_filename() . '.xml' ); /* translators: Link to documentation. */ $options[ $this->prefix . 'link' ] = sprintf( __( 'Click here to %s.', 'all-in-one-seo-pack' ), '' . __( 'view your XML sitemap', 'all-in-one-seo-pack' ) . '' ); $options[ $this->prefix . 'link' ] .= ' ' . __( 'Your sitemap has been created with content and images.', 'all-in-one-seo-pack' ); if ( $options[ "{$this->prefix}rss_sitemap" ] ) { $url_rss = aioseop_home_url( '/' . $this->get_filename() . '.rss' ); /* translators: Link to sitemap within current site. */ $options[ $this->prefix . 'link' ] .= '

' . sprintf( __( 'Click here to %1$sview your RSS sitemap%2$s.', 'all-in-one-seo-pack' ), '', '' ) . '

'; } if ( '0' !== get_option( 'blog_public' ) ) { $options[ $this->prefix . 'link' ] .= ' ' . __( 'Changes are automatically submitted to search engines.', 'all-in-one-seo-pack' ); } if ( $this->option_isset( 'rewrite' ) ) { $rule = $this->get_rewrite_url( $url ); $rules = $this->get_rewrite_rules(); // TODO Add `true` in 3rd argument with in_array(); which changes it to a strict comparison. if ( ! in_array( $rule, $rules ) ) { $options[ $this->prefix . 'link' ] .= '

' . __( 'Dynamic sitemap generation does not appear to be using the correct rewrite rules; please disable any other sitemap plugins or functionality on your site and reset your permalinks.', 'all-in-one-seo-pack' ) . '

'; } } if ( ! get_option( 'blog_public' ) ) { $privacy_link = '' . __( 'Reading Settings', 'all-in-one-seo-pack' ) . ''; /* translators: Link to settings to disable "Discourage search engines from indexing this site". */ $options[ $this->prefix . 'link' ] .= '

' . sprintf( __( 'Warning: your privacy settings are configured to ask search engines to not index your site; you can change this under %s for your site.', 'all-in-one-seo-pack' ), $privacy_link ); } $excl_terms = array(); if ( isset( $options[ $this->prefix . 'excl_terms' ] ) && is_array( $options[ $this->prefix . 'excl_terms' ] ) ) { foreach ( $options[ $this->prefix . 'excl_terms' ] as $k1_taxonomy => $v1_tax_terms ) { foreach ( $v1_tax_terms['terms'] as $v2_term ) { $excl_terms[] = $k1_taxonomy . '-' . $v2_term; } } } $options[ $this->prefix . 'excl_terms' ] = $excl_terms; return $options; } /** * Filter Options * * Handle 'all' option for post types / taxonomies, further sanitization of filename, rewrites on for multisite, setting up addl pages option. * * @todo This needs nonce support. * * @since ? * @since 3.0 Change saving 'excl_terms' to database with tax_query format for custom taxonomy support. (Pro #240) * * @param $options * @return mixed */ public function filter_options( $options ) { if ( ! isset( $this->default_options['posttypes']['initial_options'] ) ) { $this->add_post_types(); } // TODO Add `true` in 3rd argument with in_array(); which changes it to a strict comparison. if ( is_array( $options[ "{$this->prefix}posttypes" ] ) && in_array( 'all', $options[ "{$this->prefix}posttypes" ] ) && is_array( $this->default_options['posttypes']['initial_options'] ) ) { $options[ "{$this->prefix}posttypes" ] = array_keys( $this->default_options['posttypes']['initial_options'] ); } // TODO Add `true` in 3rd argument with in_array(); which changes it to a strict comparison. if ( is_array( $options[ "{$this->prefix}taxonomies" ] ) && in_array( 'all', $options[ "{$this->prefix}taxonomies" ] ) && is_array( $this->default_options['taxonomies']['initial_options'] ) ) { $options[ "{$this->prefix}taxonomies" ] = array_keys( $this->default_options['taxonomies']['initial_options'] ); } if ( is_multisite() ) { $options[ $this->prefix . 'rewrite' ] = 'On'; } if ( ! is_array( $options[ $this->prefix . 'addl_pages' ] ) ) { $options[ $this->prefix . 'addl_pages' ] = wp_specialchars_decode( stripslashes_deep( $options[ $this->prefix . 'addl_pages' ] ), ENT_QUOTES ); $decoded = json_decode( $options[ $this->prefix . 'addl_pages' ] ); if ( null === $decoded ) { $decoded = maybe_unserialize( $options[ $this->prefix . 'addl_pages' ] ); } if ( ! is_array( $decoded ) ) { $decoded = (array) $decoded; } if ( null === $decoded ) { $decoded = $options[ $this->prefix . 'addl_pages' ]; } $options[ $this->prefix . 'addl_pages' ] = $decoded; } if ( is_array( $options[ $this->prefix . 'addl_pages' ] ) ) { foreach ( $options[ $this->prefix . 'addl_pages' ] as $k => $v ) { if ( is_object( $v ) ) { $options[ $this->prefix . 'addl_pages' ][ $k ] = (array) $v; } } } if ( isset( $options[ $this->prefix . 'addl_pages' ][0] ) ) { unset( $options[ $this->prefix . 'addl_pages' ][0] ); } // TODO Refactor all these... use a nonce, dump the incoming _Post into an array and use that. if ( ! empty( $_POST[ $this->prefix . 'addl_url' ] ) ) { foreach ( array( 'addl_url', 'addl_prio', 'addl_freq', 'addl_mod' ) as $field ) { if ( ! empty( $_POST[ $this->prefix . $field ] ) ) { // TODO Add/Change to filter_input(). $_POST[ $this->prefix . $field ] = esc_attr( wp_kses_post( $_POST[ $this->prefix . $field ] ) ); } else { // TODO Add/Change to filter_input(). $_POST[ $this->prefix . $field ] = ''; } } if ( ! is_array( $options[ $this->prefix . 'addl_pages' ] ) ) { $options[ $this->prefix . 'addl_pages' ] = array(); } if ( aiosp_common::is_url_valid( $_POST[ $this->prefix . 'addl_url' ] ) ) { $options[ $this->prefix . 'addl_pages' ][ $_POST[ $this->prefix . 'addl_url' ] ] = array( // TODO Add/Change to filter_input(). 'prio' => $_POST[ $this->prefix . 'addl_prio' ], 'freq' => $_POST[ $this->prefix . 'addl_freq' ], 'mod' => $_POST[ $this->prefix . 'addl_mod' ], ); } } if ( ! empty( $_POST[ $this->prefix . 'excl_terms' ] ) ) { $raw_excl_terms = filter_input( INPUT_POST, $this->prefix . 'excl_terms', FILTER_DEFAULT, FILTER_REQUIRE_ARRAY ); // Parse taxonomy terms {$taxonomy_slug}-{$term_id}. $excl_terms = array(); foreach ( $raw_excl_terms as $v1_tax_term ) { $term_id = explode( '-', $v1_tax_term ); $term_id = intval( end( $term_id ) ); $taxonomy_slug = sanitize_text_field( str_replace( '-' . $term_id, '', $v1_tax_term ) ); // Initialize taxonomy => terms array if not yet set. if ( ! isset( $excl_terms[ $taxonomy_slug ] ) ) { $excl_terms[ $taxonomy_slug ] = array( 'terms' => array(), ); } $excl_terms[ $taxonomy_slug ]['taxonomy'] = $taxonomy_slug; $excl_terms[ $taxonomy_slug ]['terms'][] = $term_id; } $options[ $this->prefix . 'excl_terms' ] = $excl_terms; } return $options; } /** * Get Child Sitemap URLs * * Get sitemap urls of child blogs, if any. * * @since ? * * @return array */ public function get_child_sitemap_urls() { $siteurls = array(); $blogs = $this->get_child_blogs(); if ( ! empty( $blogs ) ) { $option_name = $this->get_option_name(); foreach ( $blogs as $blog_id ) { if ( $this->is_aioseop_active_on_blog( $blog_id ) ) { $options = get_blog_option( $blog_id, $this->parent_option ); if ( ! empty( $options ) && ! empty( $options['modules'] ) && ! empty( $options['modules']['aiosp_feature_manager_options'] ) && ! empty( $options['modules']['aiosp_feature_manager_options']['aiosp_feature_manager_enable_sitemap'] ) && ! empty( $options['modules'][ $option_name ] ) ) { global $wpdb; $sitemap_options = $options['modules'][ $option_name ]; $siteurl = ''; if ( defined( 'SUNRISE' ) && SUNRISE && is_object( $wpdb ) && isset( $wpdb->dmtable ) && ! empty( $wpdb->dmtable ) ) { // @codingStandardsIgnoreStart $domain = $wpdb->get_var( "SELECT domain FROM {$wpdb->dmtable} WHERE blog_id = '$blog_id' AND active = 1 LIMIT 1" ); // @codingStandardsIgnoreEnd if ( $domain ) { if ( ! isset( $_SERVER['HTTPS'] ) ) { $_SERVER['HTTPS'] = 'Off'; } $protocol = ( 'on' === strtolower( $_SERVER['HTTPS'] ) ) ? 'https://' : 'http://'; $siteurl = untrailingslashit( $protocol . $domain ); } } if ( ! $siteurl ) { $siteurl = get_home_url( $blog_id ); } $url = $siteurl . '/' . $this->get_filename() . '.xml'; $siteurls[] = $url; } } } } $siteurls = apply_filters( $this->prefix . 'sitemap_urls', $siteurls ); // Legacy. return apply_filters( $this->prefix . 'child_urls', $siteurls ); } /** * Gets Home Path * * If we're in wp-admin, use the WordPress function, otherwise we user our own version here. * This only applies to static sitemaps. * * @since 2.3.6.1 * * @return mixed|string */ public function get_home_path() { if ( function_exists( 'get_home_path' ) ) { return get_home_path(); } $home = set_url_scheme( get_option( 'home' ), 'http' ); $siteurl = set_url_scheme( get_option( 'siteurl' ), 'http' ); if ( ! empty( $home ) && 0 !== strcasecmp( $home, $siteurl ) ) { $wp_path_rel_to_home = str_ireplace( $home, '', $siteurl ); /* $siteurl - $home */ $pos = strripos( str_replace( '\\', '/', $_SERVER['SCRIPT_FILENAME'] ), trailingslashit( $wp_path_rel_to_home ) ); $home_path = substr( $_SERVER['SCRIPT_FILENAME'], 0, $pos ); $home_path = trailingslashit( $home_path ); } else { $home_path = ABSPATH; } return str_replace( '\\', '/', $home_path ); } /** * Whitelist Static Sitemaps * * Whitelists files from static sitemap conflict warning. * For right now, this is just externally produced news sitemaps until we figure out something better. * * @since 2.3.10.2 * * @param $file * @return string */ public function whitelist_static_sitemaps( $file ) { $whitelist = array( 'sitemap_news.xml', 'sitemap-news.xml' ); if ( in_array( $file, $whitelist, true ) ) { return ''; } return $file; } /** * Scan Match Files * * Scan for sitemaps on filesystem. * * @since ? * * @return array */ public function scan_match_files() { $scan1 = ''; $files = array(); $filename = $this->get_filename(); if ( ! empty( $filename ) ) { $scan1 = get_home_path() . $filename . '*.xml'; if ( empty( $scan1 ) ) { return $files; } $home_path = get_home_path(); $filescan = $this->scandir( $home_path ); if ( ! empty( $filescan ) ) { foreach ( $filescan as $f ) { if ( ! empty( $scan1 ) && fnmatch( $scan1, $home_path . $f ) ) { $f = $this->whitelist_static_sitemaps( $f ); $files[] = $home_path . $f; continue; } } } return $files; } } /** * Do Sitemap Scan * * Handle deleting / renaming of conflicting sitemap files. * * @todo Add/Fix nonce. * * @since ? */ public function do_sitemap_scan() { $msg = ''; if ( ! empty( $this->options[ "{$this->prefix}rewrite" ] ) && ( get_option( 'permalink_structure' ) === '' ) ) { $msg = '

' . __( 'Warning: dynamic sitemap generation must have permalinks enabled.', 'all-in-one-seo-pack' ) . '

'; } if ( ! empty( $_POST['aioseop_sitemap_rename_files'] ) || ! empty( $_POST['aioseop_sitemap_delete_files'] ) ) { // TODO Add/Change to filter_input(). $nonce = $_POST['nonce-aioseop']; if ( ! wp_verify_nonce( $nonce, 'aioseop-nonce' ) ) { // TODO Change to wp_die(). die( __( 'Security Check - If you receive this in error, log out and back in to WordPress', 'all-in-one-seo-pack' ) ); } if ( ! empty( $_POST['aioseop_sitemap_conflict'] ) ) { $files = $this->scan_match_files(); foreach ( $files as $f => $file ) { $files[ $f ] = realpath( $file ); } foreach ( $_POST['aioseop_sitemap_conflict'] as $ren_file ) { $ren_file = realpath( get_home_path() . $ren_file ); // TODO Add `true` in 3rd argument with in_array(); which changes it to a strict comparison. if ( in_array( $ren_file, $files ) ) { if ( ! empty( $_POST['aioseop_sitemap_delete_files'] ) ) { if ( $this->delete_file( $ren_file ) ) { /* translators: Shows which sitemap files have been deleted. */ $msg .= '

' . sprintf( __( 'Deleted %s.', 'all-in-one-seo-pack' ), $ren_file ) . '

'; } continue; } $count = 0; do { $ren_to = $ren_file . '._' . sprintf( '%03d', $count ) . '.old'; $count ++; } while ( $this->file_exists( $ren_to ) && ( $count < 1000 ) ); if ( $count >= 1000 ) { /* translators: Shows which sitemap files couldn't be renamed. */ $msg .= '

' . sprintf( __( "Couldn't rename file %s!", 'all-in-one-seo-pack' ), $ren_file ) . '

'; } else { $ren = $this->rename_file( $ren_file, $ren_to ); if ( $ren ) { /* translators: Shows which sitemap files were renamed. */ $msg .= '

' . sprintf( __( 'Renamed %1$s to %2$s.', 'all-in-one-seo-pack' ), $ren_file, $ren_to ) . '

'; } } } else { /* translators: Shows which sitemap files couldn't be found. */ $msg .= '

' . sprintf( __( "Couldn't find file %s!", 'all-in-one-seo-pack' ), $ren_file ) . '

'; } } } } else { $msg .= $this->scan_sitemaps(); } if ( ! empty( $msg ) ) { $this->output_error( $msg ); } } /** * Scan Sitemaps * * Do the scan, return the results. * * @since ? * * @return string */ public function scan_sitemaps() { $msg = ''; $files = $this->scan_match_files(); if ( ! empty( $files ) ) { $msg = $this->sitemap_warning( $files ); } return $msg; } /** * Get Problem Files * * Get the list of potentially conflicting sitemap files, identify whether they came from us, are blank, or are of unknown origin. * * @since ? * @since 2.3.10 Add the ability to see empty sitemap files as well. * * @param $files * @param $msg * @return array */ public function get_problem_files( $files, &$msg ) { $problem_files = array(); $use_wpfs = true; $wpfs = $this->get_filesystem_object(); if ( ! is_object( $wpfs ) ) { $use_wpfs = false; } else { if ( 'direct' === $wpfs->method ) { $use_wpfs = false; } } foreach ( $files as $f ) { if ( $this->is_file( $f ) ) { $fn = $f; $compressed = false; if ( $this->substr( $f, - 3 ) === '.gz' ) { $compressed = true; } if ( $use_wpfs ) { if ( $compressed ) { // Inefficient but necessary. $file = $this->load_file( $fn ); if ( ! empty( $file ) ) { $file = gzuncompress( $file, 4096 ); } } else { $file = $this->load_file( $fn, false, null, - 1, 4096 ); } } else { if ( $compressed ) { $file_resource = gzopen( $fn, 'rb' ); $file = gzread( $file_resource, 4096 ); gzclose( $file_resource ); } else { // TODO Change to `wp_remote_get()`. $file = file_get_contents( $fn, false, null, 0, 4096 ); } } if ( ! empty( $file ) ) { $matches = array(); if ( preg_match( '//', $file, $matches ) ) { if ( ! empty( $this->options[ "{$this->prefix}rewrite" ] ) ) { /* translators: %1$s, %2$s, etc. are placeholders and should not be translated. %1$s expands to the name of a sitemap file, %2$s to the name of the plugin, All in One SEO Pack, %3$s is replaced with the plugin version number and %4$s with a date. */ $msg .= '

' . sprintf( __( "Warning: a static sitemap '%1\$s' generated by %2\$s %3\$s on %4\$s already exists that may conflict with dynamic sitemap generation.", 'all-in-one-seo-pack' ), $f, AIOSEOP_PLUGIN_NAME, $matches[2], $matches[3] ) . "

\n"; $problem_files[] = $f; } } else { /* translators: Shows which 'unknown' file is conflicting with the current sitemap settings. */ $msg .= '

' . sprintf( __( 'Potential conflict with unknown file %s.', 'all-in-one-seo-pack' ), $f ) . "

\n"; $problem_files[] = $f; } } else { /* translators: Shows which files were removed. */ $msg .= '

' . sprintf( __( 'Removed empty file %s.', 'all-in-one-seo-pack' ), $f ) . "

\n"; $problem_files[] = $f; // This is causing all problem_files to be deleted automatically; which may be the intent. // TODO Either create a separate variable for this set of problem_files, or a final loop to clean problem_files before returning. foreach ( $problem_files as $f => $file ) { $files[ $f ] = realpath( $file ); $this->delete_file( realpath( $file ) ); } $problem_files = false; // Don't return anything. If it's blank, we'll take care of it here. } } } return $problem_files; } /** * Sitemap Warning * * Display the warning and the form for conflicting sitemap files. * * @since ? * * @param $files * @return string */ public function sitemap_warning( $files ) { $msg = ''; $conflict = false; $problem_files = $this->get_problem_files( $files, $msg ); if ( ! empty( $problem_files ) ) { $conflict = true; } if ( $conflict ) { foreach ( $problem_files as $p ) { $msg .= "\n"; } $msg .= "\n"; $msg .= ""; $msg .= " "; $msg = '
' . $msg . '
'; } return $msg; } /** * Debug Message * * Updates debug log messages. * * Deprecated as of 2.3.10 in favor of WP debug log. We should eventually remove this. * * @since ? * * @param $msg */ public function debug_message( $msg ) { aiosp_log( $msg ); } /** * Setup Rewrites * * Set up hooks for rewrite rules for dynamic sitemap generation. * * @since ? */ public function setup_rewrites() { add_filter( 'rewrite_rules_array', array( $this, 'rewrite_hook' ) ); add_filter( 'query_vars', array( $this, 'query_var_hook' ) ); add_action( 'parse_query', array( $this, 'sitemap_output_hook' ) ); if ( ! get_transient( "{$this->prefix}rules_flushed" ) ) { add_action( 'wp_loaded', array( $this, 'flush_rules_hook' ) ); } } /** * Get Rewrite Rules * * Build and return our rewrite rules. * * @since ? * * @param string $prefix_removed_rules_with If rules are being removed, prefix them with this character * so that they are flushed properly and are not retained. * @return array */ public function get_rewrite_rules( $prefix_removed_rules_with = null ) { $sitemap_rules = array( $this->get_filename() . '.xml' => 'index.php?' . $this->prefix . 'path=root', '(.+)-' . $this->get_filename() . '(\d+).xml' => 'index.php?' . $this->prefix . 'path=$matches[1]&' . $this->prefix . 'page=$matches[2]', '(.+)-' . $this->get_filename() . '.xml' => 'index.php?' . $this->prefix . 'path=$matches[1]', ); if ( isset( $this->options[ "{$this->prefix}rss_sitemap" ] ) && $this->options[ "{$this->prefix}rss_sitemap" ] ) { $sitemap_rules += array( $this->get_filename() . '.rss' => 'index.php?' . $this->prefix . 'path=rss', $this->get_filename() . 'latest.rss' => 'index.php?' . $this->prefix . 'path=rss_latest', ); } elseif ( ! empty( $prefix_removed_rules_with ) ) { $sitemap_rules += array( $prefix_removed_rules_with . $this->get_filename() . '.rss' => 'index.php?' . $this->prefix . 'path=rss', $prefix_removed_rules_with . $this->get_filename() . 'latest.rss' => 'index.php?' . $this->prefix . 'path=rss_latest', ); } return $sitemap_rules; } /** * Rewrite Hook * * Add in our rewrite rules. * * @since ? * * @param $rules * @return array */ public function rewrite_hook( $rules ) { $sitemap_rules = $this->get_rewrite_rules(); if ( ! empty( $sitemap_rules ) ) { $rules = $sitemap_rules + $rules; } return $rules; } /** * Flush Rewrite Rule * * @since ? */ public function flush_rules_hook() { global $wp_rewrite; $sitemap_rules = $this->get_rewrite_rules( '|' ); if ( ! empty( $sitemap_rules ) ) { $rules = get_option( 'rewrite_rules' ); $new_rules = array_keys( $sitemap_rules ); foreach ( $new_rules as $rule ) { if ( ! isset( $rules[ $rule ] ) || ( $rules[ $rule ] !== $sitemap_rules[ $rule ] ) ) { $wp_rewrite->flush_rules(); set_transient( "{$this->prefix}rules_flushed", true, 43200 ); } } } } /** * Query Var Hook * * Add our query variable for sitemap generation. * * @since ? * * @param $vars * @return array */ public function query_var_hook( $vars ) { $vars[] = "{$this->prefix}path"; if ( ! empty( $this->options[ "{$this->prefix}indexes" ] ) ) { $vars[] = "{$this->prefix}page"; } return $vars; } /** * Log Start * * Start timing and get initial memory usage for debug info. * * @since ? */ public function log_start() { $this->start_memory_usage = memory_get_peak_usage(); timer_start(); } /** * Log Stats * * Stop timing and log memory usage for debug info. * * @since ? * @since 3.0 Removed $compressed in issue #534 * * @param string $sitemap_type * @param bool $dynamic */ public function log_stats( $sitemap_type = 'static', $dynamic = true ) { $time = timer_stop(); $end_memory_usage = memory_get_peak_usage(); $sitemap_memory_usage = $end_memory_usage - $this->start_memory_usage; $end_memory_usage = $end_memory_usage / 1024.0 / 1024.0; $sitemap_memory_usage = $sitemap_memory_usage / 1024.0 / 1024.0; $sitemap_type = __( 'static', 'all-in-one-seo-pack' ); if ( $dynamic ) { $sitemap_type = __( 'dynamic', 'all-in-one-seo-pack' ); } $this->debug_message( sprintf( ' %01.2f MB memory used generating the %s sitemap in %01.3f seconds, %01.2f MB total memory used.', $sitemap_memory_usage, $sitemap_type, $time, $end_memory_usage ) ); } /** * Sitemaps Output Hook * * Handle outputting of dynamic sitemaps, logging. * * @since ? * @since 3.0 Show 404 template for empty content. #2190 * * @param $query */ public function sitemap_output_hook( $query ) { $page = 0; if ( $this->options[ "{$this->prefix}rewrite" ] && ! empty( $query->query_vars[ "{$this->prefix}path" ] ) ) { // Make dynamic sitemap. if ( ! empty( $query->query_vars[ "{$this->prefix}page" ] ) ) { $page = $query->query_vars[ "{$this->prefix}page" ] - 1; } $this->start_memory_usage = memory_get_peak_usage(); $sitemap_type = $query->query_vars[ "{$this->prefix}path" ]; $blog_charset = get_option( 'blog_charset' ); header( "Content-Type: text/xml; charset=$blog_charset", true ); // Always follow and noindex the sitemap. header( 'X-Robots-Tag: noindex, follow', true ); do_action( $this->prefix . 'add_headers', $query, $this->options ); $content = $this->do_rewrite_sitemap( $sitemap_type, $page ); // if the sitemap has no content, it's probabaly invalid and is being called directly. // @issue ( https://github.com/semperfiwebdesign/all-in-one-seo-pack/issues/2190 ). if ( empty( $content ) ) { $query->set_404(); status_header( 404 ); header( "Content-Type: text/html; charset=$blog_charset", true ); nocache_headers(); include( get_404_template() ); exit(); } echo $content; $this->log_stats( $sitemap_type ); exit(); } } /** * Make Dynamic XSL * * @since ? */ public function make_dynamic_xsl() { // Make dynamic xsl file. if ( preg_match( '#(/sitemap\.xsl)$#i', $_SERVER['REQUEST_URI'] ) ) { $blog_charset = get_option( 'blog_charset' ); header( "Content-Type: text/xml; charset=$blog_charset", true ); include_once AIOSEOP_PLUGIN_DIR . '/inc/sitemap-xsl.php'; exit(); } } /** * Get Sitemap Data * * @since ? * * @param $sitemap_type * @param int $page * @return array */ public function get_sitemap_data( $sitemap_type, $page = 0 ) { $sitemap_data = array(); if ( 0 === strpos( $sitemap_type, 'rss' ) ) { $sitemap_data = $this->get_sitemap_without_indexes(); } elseif ( $this->options[ "{$this->prefix}indexes" ] ) { $posttypes = $this->options[ "{$this->prefix}posttypes" ]; if ( empty( $posttypes ) ) { $posttypes = array(); } $taxonomies = $this->options[ "{$this->prefix}taxonomies" ]; if ( empty( $taxonomies ) ) { $taxonomies = array(); } if ( 'root' === $sitemap_type ) { $sitemap_data = array_merge( $this->get_sitemap_index_filenames() ); } elseif ( 'addl' === $sitemap_type ) { $sitemap_data = $this->get_addl_pages(); } elseif ( 'archive' === $sitemap_type && $this->option_isset( 'archive' ) ) { $sitemap_data = $this->get_date_archive_prio_data(); } elseif ( 'author' === $sitemap_type && $this->option_isset( 'author' ) ) { $sitemap_data = $this->get_author_prio_data(); } elseif ( in_array( $sitemap_type, $posttypes ) ) { // TODO Add `true` in 3rd argument with in_array(); which changes it to a strict comparison. $sitemap_data = $this->get_all_post_priority_data( $sitemap_type, 'publish', $page ); } elseif ( in_array( $sitemap_type, $taxonomies ) ) { // TODO Add `true` in 3rd argument with in_array(); which changes it to a strict comparison. $sitemap_data = $this->get_term_priority_data( get_terms( $this->get_tax_args( (array) $sitemap_type, $page ) ) ); } else { // TODO Add `true` in 3rd argument with in_array(); which changes it to a strict comparison. if ( is_array( $this->extra_sitemaps ) && in_array( $sitemap_type, $this->extra_sitemaps ) ) { $sitemap_data = apply_filters( $this->prefix . 'custom_' . $sitemap_type, $sitemap_data, $page, $this_options ); } } } elseif ( 'root' === $sitemap_type ) { $sitemap_data = $this->get_sitemap_without_indexes(); } return apply_filters( $this->prefix . 'data', $sitemap_data, $sitemap_type, $page, $this->options ); } /** * Do Rewrite Sitemap * * Output sitemaps dynamically based on rewrite rules. * * @since ? * @since 3.0 Return a (string) value. #2190 * * @param $sitemap_type * @param int $page * @return string */ public function do_rewrite_sitemap( $sitemap_type, $page = 0 ) { $this->add_post_types(); $comment = 'dynamically'; // TODO Add esc_* or wp_kses function. return $this->do_build_sitemap( $sitemap_type, $page, '', $comment ); } /** * Get Sitemap URL * * Build a url to the sitemap. * * @since 2.3.6 * @since 2.3.12.3 Refactored to use aioseop_home_url() for compatibility purposes. * * @return string */ public function get_sitemap_url() { $url = aioseop_home_url( '/' . $this->get_filename() . '.xml' ); return $url; } /** * Do Notify * * Notify search engines, do logging. * * @since ? */ public function do_notify() { if ( '0' === get_option( 'blog_public' ) ) { // Don't ping search engines if blog is set to not public. return; } if ( apply_filters( 'aioseo_sitemap_ping', true ) === false ) { // API filter hook to disable sending sitemaps to search engines. return; } $notify_url = array( 'google' => 'https://www.google.com/ping?sitemap=', 'bing' => 'https://www.bing.com/ping?sitemap=', ); $notify_url = apply_filters( 'aioseo_sitemap_ping_urls', $notify_url ); $url = $this->get_sitemap_url(); if ( ! empty( $url ) ) { foreach ( $notify_url as $k => $v ) { // TODO Change urlencode() to rawurlencode(). // @link ( http://php.net/manual/en/function.rawurlencode.php ). // @link ( http://www.faqs.org/rfcs/rfc3986.html ). $response = wp_remote_get( $notify_url[ $k ] . urlencode( $url ) ); if ( is_array( $response ) && ! empty( $response['response'] ) && ! empty( $response['response']['code'] ) ) { if ( 200 !== intval( $response['response']['code'] ) ) { /* translators: Notifies the admin which sitemaps failed to notify with which search engine(s), and display the error code. */ $this->debug_message( sprintf( __( 'Failed to notify %1$s about changes to your sitemap at %2$s, error code %3$s.', 'all-in-one-seo-pack' ), $k, $url, $response['response']['code'] ) ); } } else { /* translators: Notifies the admin which sitemaps failed to notify with which search engine(s). */ $this->debug_message( sprintf( __( 'Failed to notify %1$s about changes to your sitemap at %2$s, unable to access via wp_remote_get().', 'all-in-one-seo-pack' ), $k, $url ) ); } } } } /** * Do Robots * * Add Sitemap parameter to virtual robots.txt file. * * @since ? */ public function do_robots() { $url = $this->get_sitemap_url(); // TODO Add esc_* or wp_kses function. echo "\nSitemap: $url\n"; } /** * Do Sitemaps * * Build static sitemaps on submit if rewrite rules are not in use, do logging. * * @since ? * * @param string $message */ public function do_sitemaps( $message = '' ) { if ( defined( 'AIOSEOP_UNIT_TESTING' ) ) { $aioseop_options = aioseop_get_options(); $this->options = $aioseop_options['modules'][ "{$this->prefix}options" ]; } if ( ! empty( $this->options[ "{$this->prefix}indexes" ] ) ) { if ( $this->options[ "{$this->prefix}max_posts" ] && ( $this->options[ "{$this->prefix}max_posts" ] > 0 ) && ( $this->options[ "{$this->prefix}max_posts" ] < 50000 ) ) { $this->max_posts = $this->options[ "{$this->prefix}max_posts" ]; } else { $this->max_posts = 50000; } } else { $this->max_posts = 50000; } if ( ! $this->options[ "{$this->prefix}rewrite" ] ) { if ( $this->options[ "{$this->prefix}indexes" ] ) { $this->do_indexed_sitemaps(); } else { $this->log_start(); $comment = sprintf( "file '%s' statically", $this->get_filename() ); $sitemap = $this->do_simple_sitemap( $comment ); $this->write_sitemaps( $this->get_filename(), $sitemap ); if ( $this->options[ "{$this->prefix}rss_sitemap" ] ) { $rss = $this->do_simple_sitemap_rss( $comment ); $this->write_sitemaps( $this->get_filename(), $rss, '.rss' ); } $this->log_stats( 'root', false ); } } else { delete_transient( "{$this->prefix}rules_flushed" ); } $this->do_notify(); if ( ! empty( $message ) && is_string( $message ) ) { $this->debug_message( $message ); } } /** * Add XML Mime Type. * * @since ? * * @param $mime * @return mixed */ public function add_xml_mime_type( $mime ) { if ( ! empty( $mime ) ) { $mime['xml'] = 'text/xml'; } return $mime; } /** * Write Sitemaps * * Write multiple sitemaps to the filesystem. * * @since ? * * @param $filename * @param $contents */ public function write_sitemaps( $filename, $contents, $extn = '.xml' ) { $this->write_sitemap( $filename . $extn, $contents ); } /** * Write Sitemap * * Write a single sitemap to the filesystem. * * @since ? * @since 3.0 Removed $gzip in issue #534 * * @param $filename * @param $contents * @return bool */ public function write_sitemap( $filename, $contents ) { add_filter( 'upload_mimes', array( $this, 'add_xml_mime_type' ) ); $filename = $this->get_home_path() . sanitize_file_name( $filename ); remove_filter( 'upload_mimes', array( $this, 'add_xml_mime_type' ) ); return $this->save_file( $filename, $contents ); } /** * Gets Default Values * * Helper function for handling default values. * * @since ? * * @param $defaults * @param $prefix * @param $cache * @param $item * @param bool $nodefaults * @param string $type * @return bool */ public function get_default_values( $defaults, $prefix, &$cache, $item, $nodefaults = false, $type = '' ) { if ( ! empty( $cache[ $item . $type ] ) ) { return $cache[ $item . $type ]; } if ( ! empty( $defaults[ $item ] ) ) { $field = $this->prefix . $prefix . $item; if ( $this->option_isset( $prefix . $item ) && 'no' !== $this->options[ $field ] ) { if ( ( 'sel' === $this->options[ $field ] ) && ! empty( $type ) && isset( $this->options[ $this->prefix . $prefix . $item . '_' . $type ] ) ) { if ( 'no' === $this->options[ $this->prefix . $prefix . $item . '_' . $type ] ) { return false; } if ( 'sel' === $this->options[ $this->prefix . $prefix . $item . '_' . $type ] ) { return false; } $cache[ $item . $type ] = $this->options[ $this->prefix . $prefix . $item . '_' . $type ]; } else { if ( 'no' === $this->options[ $field ] ) { return false; } if ( 'sel' === $this->options[ $field ] ) { return false; } $cache[ $item . $type ] = $this->options[ $field ]; } return $cache[ $item . $type ]; } if ( $nodefaults ) { return false; } return $defaults[ $item ]; } return false; } /** * Get Default Priority * * Get priority settings for sitemap entries. * * @since ? * * @param $item * @param bool $nodefaults * @param string $type * @return bool */ public function get_default_priority( $item, $nodefaults = false, $type = '' ) { $defaults = array( 'homepage' => '1.0', 'blog' => '0.9', 'sitemap' => '0.8', 'post' => '0.7', 'archive' => '0.5', 'author' => '0.3', 'taxonomies' => '0.3', ); static $cache = array(); return $this->get_default_values( $defaults, 'prio_', $cache, $item, $nodefaults, $type ); } /** * Get Default Frequency * * Get frequency settings for sitemap entries. * * @since ? * * @param $item * @param bool $nodefaults * @param string $type * @return bool */ public function get_default_frequency( $item, $nodefaults = false, $type = '' ) { $defaults = array( 'homepage' => 'always', 'blog' => 'daily', 'sitemap' => 'hourly', 'post' => 'weekly', 'archive' => 'monthly', 'author' => 'weekly', 'taxonomies' => 'monthly', ); static $cache = array(); return $this->get_default_values( $defaults, 'freq_', $cache, $item, $nodefaults, $type ); } /** * Get Sitemaps Index Filenames * * Build an index of sitemaps used. * * @since 2.3.6 * @since 2.3.12.3 Refactored to use aioseop_home_url() for compatibility purposes. * @since 3.0 Changed to exclude noindex post types. #1382 * @return array */ public function get_sitemap_index_filenames() { global $aioseop_options; $files = array(); $options = $this->options; $prefix = $this->get_filename(); $suffix = '.xml'; if ( empty( $options[ "{$this->prefix}posttypes" ] ) ) { $options[ "{$this->prefix}posttypes" ] = array(); } if ( empty( $options[ "{$this->prefix}taxonomies" ] ) ) { $options[ "{$this->prefix}taxonomies" ] = array(); } $options[ "{$this->prefix}posttypes" ] = array_diff( $options[ "{$this->prefix}posttypes" ], array( 'all' ) ); $options[ "{$this->prefix}taxonomies" ] = $this->show_or_hide_taxonomy( array_diff( $options[ "{$this->prefix}taxonomies" ], array( 'all' ) ) ); $files[] = array( 'loc' => aioseop_home_url( '/addl-' . $prefix . $suffix ) ); // Get post types selected, and NoIndex post types & Index posts. $post_types = $options[ "{$this->prefix}posttypes" ]; if ( is_array( $aioseop_options['aiosp_cpostnoindex'] ) ) { foreach ( $post_types as $index => $post_type ) { if ( in_array( $post_type, $aioseop_options['aiosp_cpostnoindex'], true ) ) { $args = array( 'post_type' => $post_type, 'fields' => 'ids', 'posts_per_page' => 1, 'meta_query' => array( 'relation' => 'OR', array( 'key' => '_aioseop_noindex', 'value' => 'off', 'compare' => '=', ), ), ); $q = new WP_Query( $args ); if ( 0 === $q->post_count ) { unset( $post_types[ $index ] ); } } } } if ( ! empty( $post_types ) ) { $prio = $this->get_default_priority( 'post' ); $freq = $this->get_default_frequency( 'post' ); // Get post counts from posts type. Exclude if NoIndex is on, and does not contain excluded terms. $args = array( 'post_type' => $post_types, 'post_status' => 'publish', 'meta_query' => array( 'relation' => 'OR', array( 'key' => '_aioseop_noindex', 'value' => 'on', 'compare' => '!=', ), array( 'key' => '_aioseop_noindex', 'compare' => 'NOT EXISTS', ), ), ); if ( $this->option_isset( 'excl_terms' ) ) { // Adds excluded terms to exclude from query. foreach ( $this->options[ $this->prefix . 'excl_terms' ] as $k1_taxonomy => $v1_tax_terms ) { if ( ! isset( $args['tax_query'] ) ) { $args['tax_query'] = array( 'relation' => 'AND', ); } $args['tax_query'][] = array( 'taxonomy' => $k1_taxonomy, 'terms' => $v1_tax_terms['terms'], 'operator' => 'NOT IN', ); } } $post_counts = $this->get_all_post_counts( $args ); foreach ( $post_types as $sm ) { if ( 0 === intval( $post_counts[ $sm ] ) ) { continue; } if ( ! empty( $this->options[ "{$this->prefix}indexes" ] ) ) { if ( $post_counts[ $sm ] > $this->max_posts ) { $count = 1; for ( $post_count = 0; $post_count < $post_counts[ $sm ]; $post_count += $this->max_posts ) { $files[] = array( 'loc' => aioseop_home_url( '/' . $sm . '-' . $prefix . ( $count ++ ) . $suffix ), 'changefreq' => $freq, 'priority' => $prio, ); } } else { $files[] = array( 'loc' => aioseop_home_url( '/' . $sm . '-' . $prefix . $suffix ), 'changefreq' => $freq, 'priority' => $prio, ); } } else { $files[] = array( 'loc' => aioseop_home_url( '/' . $sm . '-' . $prefix . $suffix ), 'changefreq' => $freq, 'priority' => $prio, ); } } } if ( $this->option_isset( 'archive' ) ) { $files[] = array( 'loc' => aioseop_home_url( '/' . 'archive-' . $prefix . $suffix ), 'changefreq' => $this->get_default_frequency( 'archive' ), 'priority' => $this->get_default_priority( 'archive' ), ); } if ( $this->option_isset( 'author' ) ) { $files[] = array( 'loc' => aioseop_home_url( '/' . 'author-' . $prefix . $suffix ), 'changefreq' => $this->get_default_frequency( 'author' ), 'priority' => $this->get_default_priority( 'author' ), ); } if ( ! empty( $options[ "{$this->prefix}taxonomies" ] ) ) { foreach ( $options[ "{$this->prefix}taxonomies" ] as $v1_taxonomy ) { $tax_args = $this->get_tax_args( array( $v1_taxonomy ) ); $tax_args['fields'] = 'count'; $term_count = get_terms( $tax_args ); if ( ! is_wp_error( $term_count ) && ( $term_count > 0 ) ) { if ( ! empty( $this->options[ "{$this->prefix}indexes" ] ) ) { if ( $term_count > $this->max_posts ) { $count = 1; for ( $tc = 0; $tc < $term_count; $tc += $this->max_posts ) { $files[] = array( 'loc' => aioseop_home_url( '/' . $v1_taxonomy . '-' . $prefix . ( $count ++ ) . $suffix ), 'changefreq' => $this->get_default_frequency( 'taxonomies' ), 'priority' => $this->get_default_priority( 'taxonomies' ), ); } } else { $files[] = array( 'loc' => aioseop_home_url( '/' . $v1_taxonomy . '-' . $prefix . $suffix ), 'changefreq' => $this->get_default_frequency( 'taxonomies' ), 'priority' => $this->get_default_priority( 'taxonomies' ), ); } } else { $files[] = array( 'loc' => aioseop_home_url( '/' . $v1_taxonomy . '-' . $prefix . $suffix ), 'changefreq' => $this->get_default_frequency( 'taxonomies' ), 'priority' => $this->get_default_priority( 'taxonomies' ), ); } } } } foreach ( $this->get_child_sitemap_urls() as $csm ) { $files[] = array( 'loc' => $csm, 'changefreq' => $this->get_default_frequency( 'sitemap' ), 'priority' => $this->get_default_priority( 'sitemap' ), ); } $files = apply_filters( 'aioseop_sitemap_index_filenames', $files, $prefix, $suffix ); if ( ! $this->should_addl_sitemap_exist() ) { $page_to_remove = array( get_site_url() . '/addl-sitemap.xml' ); $files = $this->remove_urls_from_sitemap_page( $files, $page_to_remove ); } return $files; } /** * Checks whether the addl-sitemap file should be added to the root sitemap index. * This should not happen if both the static homepage/posts page have been set and no additional pages have been specified manually. * * @since 3.2.0 * @since 3.3.5 Fix issue where addl-sitemap file is not added to root when static pages are set but not being used - #3090. * * @return bool Whether or not the addl-sitemap file should be added to the root sitemap index. */ private function should_addl_sitemap_exist() { $are_addl_pages = ! empty( $this->options['aiosp_sitemap_addl_pages'] ); $static_homepage_id = (int) get_option( 'page_on_front' ); $is_static_homepage_set = ( 0 !== $static_homepage_id ) ? true : false; $is_homepage_set_to_latest_posts = ( 'posts' === get_option( 'show_on_front' ) ) ? true : false; if ( ! $is_homepage_set_to_latest_posts && $is_static_homepage_set && ! $are_addl_pages ) { return false; } return true; } /** * Build the Sitemap * * @since ? * * @param $sitemap_type * @param int $page * @param string $filename * @param string $comment * @return string */ public function do_build_sitemap( $sitemap_type, $page = 0, $filename = '', $comment = '' ) { if ( empty( $filename ) ) { switch ( $sitemap_type ) { case 'root': // fall-through. case 'rss': // fall-through. case 'rss_latest': $filename = $this->get_filename(); break; default: $filename = $this->get_filename() . '_' . $sitemap_type; break; } } if ( empty( $comment ) ) { $comment = "file '%s' statically"; } $sitemap_data = $this->get_sitemap_data( $sitemap_type, $page ); if ( ( 'root' === $sitemap_type ) && ! empty( $this->options[ "{$this->prefix}indexes" ] ) ) { return $this->build_sitemap_index( $sitemap_data, sprintf( $comment, $filename ) ); } else { if ( empty( $sitemap_data ) ) { return ''; } return $this->build_sitemap( $sitemap_data, $sitemap_type, sprintf( $comment, $filename ) ); } } /** * Do Write Sitemaps * * Write the sitemap. * * @since ? * * @param $sitemap_type * @param int $page * @param string $filename * @param string $comment */ public function do_write_sitemap( $sitemap_type, $page = 0, $filename = '', $comment = '' ) { if ( empty( $filename ) ) { if ( 'root' === $sitemap_type ) { $filename = $this->get_filename(); } else { $filename = $sitemap_type . '-' . $this->get_filename(); } } if ( empty( $comment ) ) { $comment = "file '%s' statically"; } $this->write_sitemaps( $filename, $this->do_build_sitemap( $sitemap_type, $page, $filename, $comment ) ); } /** * Do Indexed Sitemaps * * Build all the indexes. * * @since ? */ public function do_indexed_sitemaps() { $this->start_memory_usage = memory_get_peak_usage(); $options = $this->options; $this->do_write_sitemap( 'root' ); $this->do_write_sitemap( 'addl' ); if ( $this->option_isset( 'archive' ) ) { $this->do_write_sitemap( 'archive' ); } if ( $this->option_isset( 'author' ) ) { $this->do_write_sitemap( 'author' ); } if ( ( ! isset( $options[ "{$this->prefix}posttypes" ] ) ) || ( ! is_array( $options[ "{$this->prefix}posttypes" ] ) ) ) { $options[ "{$this->prefix}posttypes" ] = array(); } if ( ( ! isset( $options[ "{$this->prefix}taxonomies" ] ) ) || ( ! is_array( $options[ "{$this->prefix}taxonomies" ] ) ) ) { $options[ "{$this->prefix}taxonomies" ] = array(); } $options[ "{$this->prefix}posttypes" ] = array_diff( $options[ "{$this->prefix}posttypes" ], array( 'all' ) ); $options[ "{$this->prefix}taxonomies" ] = array_diff( $options[ "{$this->prefix}taxonomies" ], array( 'all' ) ); if ( ! empty( $options[ "{$this->prefix}posttypes" ] ) ) { $post_counts = $this->get_all_post_counts( array( 'post_type' => $options[ "{$this->prefix}posttypes" ], 'post_status' => 'publish', ) ); foreach ( $options[ "{$this->prefix}posttypes" ] as $posttype ) { if ( 0 === $post_counts[ $posttype ] ) { continue; } if ( ! empty( $this->options[ "{$this->prefix}indexes" ] ) && ( $post_counts[ $posttype ] > $this->max_posts ) ) { $count = 1; for ( $post_count = 0; $post_count < $post_counts[ $posttype ]; $post_count += $this->max_posts ) { $this->do_write_sitemap( $posttype, $count - 1, "{$posttype}-" . $this->get_filename() . "{$count}" ); $count ++; } } else { $this->do_write_sitemap( $posttype ); } } } if ( ! empty( $options[ "{$this->prefix}taxonomies" ] ) ) { foreach ( $options[ "{$this->prefix}taxonomies" ] as $taxonomy ) { $term_count = wp_count_terms( $taxonomy, array( 'hide_empty' => true ) ); if ( ! is_wp_error( $term_count ) && ( $term_count > 0 ) ) { if ( ! empty( $this->options[ "{$this->prefix}indexes" ] ) ) { if ( $term_count > $this->max_posts ) { $count = 1; for ( $tc = 0; $tc < $term_count; $tc += $this->max_posts ) { $this->do_write_sitemap( $taxonomy, $tc, "{$taxonomy}-" . $this->get_filename() . "{$count}" ); $count ++; } } else { $this->do_write_sitemap( $taxonomy ); } } else { $this->do_write_sitemap( $taxonomy ); } } } } $this->log_stats( 'indexed', false ); } /** * Remove Posts Page * * @since 2.3.11 * * @param $postspageid * @return bool */ public function remove_posts_page( $postspageid ) { // TODO Add `true` in 3rd argument with in_array(); which changes it to a strict comparison. if ( in_array( $postspageid, $this->excludes ) ) { return true; } // TODO Add `true` in 3rd argument with in_array(); which changes it to a strict comparison. if ( in_array( get_post_field( 'post_name', $postspageid ), $this->excludes ) ) { return true; } return false; } /** * Remove Homepage * * @since 2.3.11 * * @param $homepage_id * @return bool */ public function remove_homepage( $homepage_id ) { // TODO Add `true` in 3rd argument with in_array(); which changes it to a strict comparison. if ( in_array( $homepage_id, $this->excludes ) ) { return true; } // TODO Add `true` in 3rd argument with in_array(); which changes it to a strict comparison. if ( in_array( get_post_field( 'post_name', $homepage_id ), $this->excludes ) ) { return true; } return false; } /** * The get_sitemap_without_indexes() function. * * Fetches data for sitemap without indexes. * * @since 2.3.6 * @since 2.3.12.3 Refactored to use aioseop_home_url() for compatibility purposes. * @since 3.2.0 Improved function and variable naming. * * @return array */ public function get_sitemap_without_indexes() { $child_urls = $this->get_child_sitemap_urls(); $options = $this->options; if ( is_array( $options[ "{$this->prefix}posttypes" ] ) ) { $options[ "{$this->prefix}posttypes" ] = array_diff( $options[ "{$this->prefix}posttypes" ], array( 'all' ) ); } if ( is_array( $options[ "{$this->prefix}taxonomies" ] ) ) { $options[ "{$this->prefix}taxonomies" ] = array_diff( $options[ "{$this->prefix}taxonomies" ], array( 'all' ) ); } $urls = $this->get_all_post_priority_data( $options[ "{$this->prefix}posttypes" ] ); // It's 0 if posts are on homepage, otherwise it's the id of the posts page. $posts = (int) get_option( 'page_for_posts' ); $postspageid = (int) get_option( 'page_for_posts' ); $home = array( 'loc' => aioseop_home_url(), 'changefreq' => $this->get_default_frequency( 'homepage' ), 'priority' => $this->get_default_priority( 'homepage' ), 'image:image' => $this->get_images_from_post( (int) get_option( 'page_on_front' ) ), ); if ( $posts ) { $posts = $this->get_permalink( $posts ); if ( $posts === $home['loc'] ) { $posts = null; } else { $posts = array( 'loc' => $posts, 'changefreq' => $this->get_default_frequency( 'blog' ), 'priority' => $this->get_default_priority( 'blog' ), ); } } if ( $this->option_isset( 'archive' ) ) { $urls = array_merge( $urls, $this->get_date_archive_prio_data() ); } if ( $this->option_isset( 'author' ) ) { $urls = array_merge( $urls, $this->get_author_prio_data() ); } foreach ( $urls as $k => $p ) { if ( untrailingslashit( $p['loc'] ) === untrailingslashit( $home['loc'] ) ) { $urls[ $k ]['priority'] = '1.0'; $home = null; break; } } if ( ( null !== $posts ) && isset( $posts['loc'] ) ) { foreach ( $urls as $k => $p ) { if ( $p['loc'] === $posts['loc'] ) { $urls[ $k ]['changefreq'] = $this->get_default_frequency( 'blog' ); $urls[ $k ]['priority'] = $this->get_default_priority( 'blog' ); $posts = null; break; } } } if ( is_array( $posts ) && $this->remove_posts_page( $postspageid ) !== true ) { array_unshift( $urls, $posts ); } if ( is_array( $home ) ) { array_unshift( $urls, $home ); } $terms = get_terms( $this->get_tax_args( $options[ "{$this->prefix}taxonomies" ] ) ); $urls2 = $this->get_term_priority_data( $terms ); $urls3 = $this->get_addl_pages_only(); $urls = array_merge( $child_urls, $urls, $urls2, $urls3 ); if ( is_array( $this->extra_sitemaps ) ) { foreach ( $this->extra_sitemaps as $sitemap_type ) { $sitemap_data = array(); $sitemap_data = apply_filters( $this->prefix . 'custom_' . $sitemap_type, $sitemap_data, $page, $this_options ); $urls = array_merge( $urls, $sitemap_data ); } } $urls = $this->get_homepage_timestamp( $urls ); $urls = $this->get_posts_page_timestamp( $urls ); return $urls; } /** * Do Simple Sitemap * * Build a single, stand-alone sitemap without indexes. * * @since ? * * @param string $comment * @return string */ public function do_simple_sitemap( $comment = '' ) { $sitemap_data = $this->get_sitemap_without_indexes(); $sitemap_data = apply_filters( $this->prefix . 'data', $sitemap_data, 'root', 0, $this->options ); return $this->build_sitemap( $sitemap_data, '', $comment ); } /** * Do Simple Sitemap RSS * * Build a single stand-alone RSS sitemap without indexes. * * @since 2.9 * * @param string $comment * @return string */ public function do_simple_sitemap_rss( $comment = '' ) { $sitemap_data = $this->get_sitemap_without_indexes(); $sitemap_data = apply_filters( $this->prefix . 'data', $sitemap_data, 'rss', 0, $this->options ); return $this->build_sitemap( $sitemap_data, 'rss', $comment ); } /** * Get Sitemap XSL * * Gets the sitemap URL. * * Has a filter for using something other than the dynamically generated one. * Using the filter you need the full path to the custom xsl file. * * @since 2.3.6 * @since 2.3.12.3 Refactored to use aioseop_home_url() for compatibility purposes. * * @see https://semperplugins.com/documentation/aioseop_sitemap_xsl_url/ */ public function get_sitemap_xsl() { return esc_url( apply_filters( 'aioseop_sitemap_xsl_url', aioseop_home_url( '/sitemap.xsl' ) ) ); } /** * Output RSS * * Output the RSS for a sitemap, full or latest. * * @since 2.9 * * @param $urls * @param string $sitemap_type The type of RSS sitemap viz. rss or rss_latest. * @param string $comment */ private function output_rss( $urls, $sitemap_type, $comment ) { echo '' . "\r\n\r\n"; // TODO Add esc_* function. echo '\r\n"; echo ''; if ( is_multisite() ) { // TODO Add esc_* function. echo '' . aiosp_common::make_xml_safe( 'title', get_blog_option( get_current_blog_id(), 'blogname' ) ) . '' . '' . aiosp_common::make_xml_safe( 'link', get_blog_option( get_current_blog_id(), 'siteurl' ) ) . '' . '' . aiosp_common::make_xml_safe( 'description', get_blog_option( get_current_blog_id(), 'blogdescription' ) ) . ''; } else { // TODO Add esc_* function. echo '' . aiosp_common::make_xml_safe( 'title', get_option( 'blogname' ) ) . '' . '' . aiosp_common::make_xml_safe( 'link', get_option( 'siteurl' ) ) . '' . '' . aiosp_common::make_xml_safe( 'description', get_option( 'blogdescription' ) ) . ''; } // remove urls that do not have the rss element. $urls = array_filter( $urls, array( $this, 'include_in_rss' ) ); if ( false !== strpos( $sitemap_type, 'latest' ) ) { // let's sort the array in descending order of date. uasort( $urls, array( $this, 'sort_modifed_date_descending' ) ); $urls = array_slice( $urls, 0, apply_filters( $this->prefix . 'rss_latest_limit', 20 ) ); } foreach ( $urls as $url ) { // TODO Add esc_* function. echo '' . '' . aiosp_common::make_xml_safe( 'guid', $url['loc'] ) . '' . '' . aiosp_common::make_xml_safe( 'title', $url['rss']['title'] ) . '' . '' . aiosp_common::make_xml_safe( 'link', $url['loc'] ) . '' . // TODO Add esc_* or wp_kses function. '' . '' . aiosp_common::make_xml_safe( 'pubDate', $url['rss']['pubDate'] ) . '' . ''; } echo ''; } /** * Include in RSS * * Remove elements not containing the rss element. * * @since 2.9 * * @param $array * @return bool */ public function include_in_rss( $array ) { return isset( $array['rss'] ); } /** * Sort Modified Date Descending * * Sort on the basis of modified date. * * @since 2.9 * * @param $array1 * @param $array2 * @return bool|int */ public function sort_modifed_date_descending( $array1, $array2 ) { if ( ! isset( $array1['rss'] ) || ! isset( $array2['rss'] ) ) { return 0; } return $array1['rss']['timestamp'] < $array2['rss']['timestamp']; } /** * Output Sitemap * * Output the XML for a sitemap. * * @since ? * * @param $urls * @param string $sitemap_type The type of sitemap viz. root, rss, rss_latest etc.. For static sitemaps, this would be empty. * @param string $comment * @return null */ private function output_sitemap( $urls, $sitemap_type, $comment = '' ) { if ( 0 === strpos( $sitemap_type, 'rss' ) ) { // starts with rss. $this->output_rss( $urls, $sitemap_type, $comment ); return; } $max_items = 50000; if ( ! is_array( $urls ) ) { return null; } echo '' . "\r\n\r\n"; // TODO Add esc_* function. echo '\r\n"; $plugin_path = $this->plugin_path['url']; $plugin_url = wp_parse_url( $plugin_path ); $current_host = $_SERVER['HTTP_HOST']; if ( empty( $current_host ) ) { $current_host = $_SERVER['SERVER_NAME']; } if ( ! empty( $current_host ) && ( $current_host !== $plugin_url['host'] ) ) { $plugin_url['host'] = $current_host; } // Code `unset( $plugin_url['scheme'] )`. $plugin_path = $this->unparse_url( $plugin_url ); // Using the filter you need the full path to the custom xsl file. $xsl_url = $this->get_sitemap_xsl(); $xml_header = '' . "\r\n" . 'prefix . 'xml_namespace', array( 'xmlns' => 'http://www.sitemaps.org/schemas/sitemap/0.9', 'xmlns:image' => 'http://www.google.com/schemas/sitemap-image/1.1', ) ); if ( ! empty( $namespaces ) ) { $ns = array(); foreach ( $namespaces as $k => $v ) { $ns[] = esc_attr( $k ) . '="' . esc_url( $v, array( 'http', 'https' ) ) . '"'; } $xml_header .= join( "\r\n\t", $ns ); } $xml_header .= '>' . "\r\n"; // TODO Add esc_* function. echo $xml_header; $count = 0; foreach ( $urls as $url ) { echo "\t\r\n"; if ( is_array( $url ) ) { if ( isset( $url['rss'] ) ) { unset( $url['rss'] ); } foreach ( $url as $k => $v ) { if ( ! empty( $v ) ) { $v = aiosp_common::make_xml_safe( $k, $v ); if ( is_array( $v ) ) { $buf = "\t\t\t<$k>\r\n"; foreach ( $v as $ext => $attr ) { if ( is_array( $attr ) ) { $buf = ''; // TODO Add esc_* function. echo "\t\t<$k>\r\n"; foreach ( $attr as $a => $nested ) { if ( is_array( $nested ) ) { // TODO Add esc_* function. echo "\t\t\t<$a>\r\n"; foreach ( $nested as $next => $nattr ) { $value = aiosp_common::make_xml_safe( $next, $nattr ); // TODO Add esc_* function. echo "\t\t\t\t<$next>$value\r\n"; } // TODO Add esc_* function. echo "\t\t\t\r\n"; } else { $value = aiosp_common::make_xml_safe( $a, $nested ); // TODO Add esc_* function. echo "\t\t\t<$a>$value\r\n"; } } // TODO Add esc_* function. echo "\t\t\r\n"; } else { $value = aiosp_common::make_xml_safe( $ext, $attr ); $buf .= "\t\t\t<$ext>$value\r\n"; } } if ( ! empty( $buf ) ) { // TODO Add esc_* function. echo $buf . "\t\t\r\n"; } } else { $value = aiosp_common::make_xml_safe( $k, $v ); // TODO Add esc_* function. echo "\t\t<$k>$value\r\n"; } } } } else { $value = aiosp_common::make_xml_safe( 'loc', $url ); // TODO Add esc_* function. echo "\t\t$value\r\n"; } echo "\t\r\n"; if ( $count >= $max_items ) { break; } } echo ''; } /** * Output Sitemap Index * * Output the XML for a sitemap index. * * @since ? * * @param $urls * @param string $comment * @return null */ public function output_sitemap_index( $urls, $comment = '' ) { $max_items = 50000; if ( ! is_array( $urls ) ) { return null; } echo '' . "\r\n\r\n"; // TODO Add esc_* function. echo '\r\n"; $xsl_url = $this->get_sitemap_xsl(); // TODO Add esc_* function. echo '' . "\r\n"; echo '' . "\r\n"; $count = 0; foreach ( $urls as $url ) { echo "\t\r\n"; if ( is_array( $url ) ) { foreach ( $url as $k => $v ) { // TODO Add `true` in 3rd argument with in_array(); which changes it to a strict comparison. if ( ! in_array( $k, array( 'loc', 'lastmod' ) ) ) { continue; } $v = aiosp_common::make_xml_safe( $k, $v ); // TODO Add esc_* function. echo "\t\t<$k>$v\r\n"; } } else { $value = aiosp_common::make_xml_safe( 'loc', $url ); // TODO Add esc_* function. echo "\t\t$value\r\n"; } echo "\t\r\n"; $count ++; if ( $count >= $max_items ) { break; } } echo ''; } /** * Build Sitemap Index * * Return an XML sitemap index as a string. * * @since ? * * @param $urls * @param string $comment * @return string */ public function build_sitemap_index( $urls, $comment = '' ) { ob_start(); $this->output_sitemap_index( $urls, $comment ); return ob_get_clean(); } /** * Build Sitemap * * Return an XML sitemap as a string. * * @since ? * * @param $urls * @param string $sitemap_type The type of sitemap viz. root, rss, rss_latest etc.. For static sitemaps, this would be empty. * @param string $comment * @return string */ public function build_sitemap( $urls, $sitemap_type, $comment = '' ) { ob_start(); $this->output_sitemap( $urls, $sitemap_type, $comment ); return ob_get_clean(); } /** * Get Term Priority Data * * Return sitemap data for an array of terms. * * @since ? * * @param $terms * @return array */ public function get_term_priority_data( $terms ) { $prio = array(); if ( is_array( $terms ) && ! empty( $terms ) ) { $def_prio = $this->get_default_priority( 'taxonomies' ); $def_freq = $this->get_default_frequency( 'taxonomies' ); foreach ( $terms as $term ) { $pr_info = array(); $pr_info['loc'] = $this->get_term_link( $term, $term->taxonomy ); $pr_info['lastmod'] = $this->get_tax_term_timestamp( $term ); if ( ( 'sel' === $this->options[ $this->prefix . 'freq_taxonomies' ] ) && isset( $this->options[ $this->prefix . 'freq_taxonomies_' . $term->taxonomy ] ) && ( 'no' !== $this->options[ $this->prefix . 'freq_taxonomies_' . $term->taxonomy ] ) ) { $pr_info['changefreq'] = $this->options[ $this->prefix . 'freq_taxonomies_' . $term->taxonomy ]; } else { $pr_info['changefreq'] = $def_freq; } if ( ( 'sel' === $this->options[ $this->prefix . 'prio_taxonomies' ] ) && isset( $this->options[ $this->prefix . 'prio_taxonomies_' . $term->taxonomy ] ) && ( 'no' !== $this->options[ $this->prefix . 'prio_taxonomies_' . $term->taxonomy ] ) ) { $pr_info['priority'] = $this->options[ $this->prefix . 'prio_taxonomies_' . $term->taxonomy ]; } else { $pr_info['priority'] = $def_prio; } $pr_info['image:image'] = $this->get_images_from_term( $term ); $pr_info['rss'] = array( 'title' => $term->name, 'description' => $term->description, 'pubDate' => $this->get_date_for_term( $term ), ); $prio[] = $pr_info; } } return $prio; } /** * The get_tax_term_timestamp() function. * * Gets the Last Change timestamp for a taxonomy term. * * @since 3.2.0 * * @param object $term * @return string $lastmod */ private function get_tax_term_timestamp( $term ) { $taxonomy_object = get_taxonomy( $term->taxonomy ); $lastmod = ''; // Loop through all attached post types and get timestamp of last modified assigned post. foreach ( $taxonomy_object->object_type as $object_type ) { $latest_modified_post = new WP_Query( array( 'post_type' => $object_type, 'post_status' => 'publish', 'posts_per_page' => 1, 'orderby' => 'modified', 'order' => 'DESC', 'taxonomy' => $term->taxonomy, 'term' => $term->name, ) ); if ( $latest_modified_post->have_posts() ) { $temp_lastmod = $latest_modified_post->posts[0]->post_modified_gmt; if ( '' === $lastmod || ( $temp_lastmod > $lastmod ) ) { $lastmod = $temp_lastmod; } } } $lastmod = date( 'Y-m-d\TH:i:s\Z', mysql2date( 'U', $lastmod ) ); return $lastmod; } /** * Get Date for Term * * Return the date of the latest post in the given taxonomy term. * * @since 2.9 * * @param WP_Term $term The taxonomy term. * @return string */ private function get_date_for_term( $term ) { $date = ''; $query = new WP_Query( array( 'orderby' => 'post_date', 'order' => 'DESC', 'numberposts' => 1, 'post_type' => 'any', 'post_status' => 'publish', 'tax_query' => array( array( 'taxonomy' => $term->taxonomy, 'terms' => $term->term_id, ), ), ) ); if ( $query->have_posts() ) { $timestamp = mysql2date( 'U', $query->post->post_modified_gmt ); $date = date( 'r', $timestamp ); } return $date; } /** * Get Term Permalinks * * Return a list of permalinks for an array of terms. * * @since ? * * @param $terms * @return array */ public function get_term_permalinks( $terms ) { $links = array(); if ( is_array( $terms ) ) { foreach ( $terms as $term ) { $url = $this->get_term_link( $term ); $links[] = $url; } } return $links; } /** * Get Archive Permalinks * * Return permalinks for archives. * * @since ? * * @param $posts * @return array */ public function get_archive_permalinks( $posts ) { $links = array(); $archives = array(); if ( is_array( $posts ) ) { foreach ( $posts as $post ) { $date = mysql2date( 'U', $post->post_date ); $year = date( 'Y', $date ); $month = date( 'm', $date ); $archives[ $year . '-' . $month ] = array( $year, $month ); } } $archives = array_keys( $archives ); foreach ( $archives as $d ) { $links[] = get_month_link( $d[0], $d[1] ); } return $links; } /** * Get Author Permalink * * Return permalinks for authors. * * @since ? * * @param $posts * @return array */ public function get_author_permalinks( $posts ) { $links = array(); $authors = array(); if ( is_array( $posts ) ) { foreach ( $posts as $post ) { $authors[ $post->author_id ] = 1; } } $authors = array_keys( $authors ); foreach ( $authors as $auth_id ) { $links[] = get_author_posts_url( $auth_id ); } return $links; } /** * Get Post Permalink * * Return permalinks for posts. * * @since ? * * @param $posts * @return array */ public function get_post_permalinks( $posts ) { $links = array(); if ( is_array( $posts ) ) { foreach ( $posts as $post ) { $post->filter = 'sample'; $url = $this->get_permalink( $post ); $links[] = $url; } } return $links; } /** * Unparse URL * * Convert back from parse_url. * Props to thomas at gielfeldt dot com. * * @since ? * * @link http://www.php.net/manual/en/function.parse-url.php#106731 * * @param $parsed_url * @return string */ public function unparse_url( $parsed_url ) { $scheme = isset( $parsed_url['scheme'] ) ? $parsed_url['scheme'] . '://' : ''; $host = isset( $parsed_url['host'] ) ? $parsed_url['host'] : ''; if ( ! empty( $host ) && empty( $scheme ) ) { $scheme = '//'; } $port = isset( $parsed_url['port'] ) ? ':' . $parsed_url['port'] : ''; $user = isset( $parsed_url['user'] ) ? $parsed_url['user'] : ''; $pass = isset( $parsed_url['pass'] ) ? ':' . $parsed_url['pass'] : ''; $pass = ( $user || $pass ) ? "$pass@" : ''; $path = isset( $parsed_url['path'] ) ? $parsed_url['path'] : ''; $query = isset( $parsed_url['query'] ) ? '?' . $parsed_url['query'] : ''; $fragment = isset( $parsed_url['fragment'] ) ? '#' . $parsed_url['fragment'] : ''; return "$scheme$user$pass$host$port$path$query$fragment"; } /** * Get Additional Page Only * * Return data for user entered additional pages. * * @since 2.3.6 * @since 2.3.12.3 Refactored to use aioseop_home_url() for compatibility purposes. * * @return array */ public function get_addl_pages_only() { $pages = array(); if ( ! empty( $this->options[ $this->prefix . 'addl_pages' ] ) ) { $siteurl = wp_parse_url( aioseop_home_url() ); foreach ( $this->options[ $this->prefix . 'addl_pages' ] as $k => $v ) { $k = aiosp_common::make_url_valid_smartly( $k ); $url = wp_parse_url( $k ); if ( empty( $url['host'] ) ) { $url['host'] = $siteurl['host']; } if ( ! empty( $url['path'] ) && substr( $url['path'], 0, 1 ) !== '/' ) { $url['path'] = '/' . $url['path']; } $freq = ''; $prio = ''; $mod = ''; if ( ! empty( $v['mod'] ) ) { $mod = $v['mod']; } if ( ! empty( $v['freq'] ) ) { $freq = $v['freq']; } if ( ! empty( $v['prio'] ) ) { $prio = $v['prio']; } if ( 'no' === $freq ) { $freq = ''; } if ( 'no' === $prio ) { $prio = ''; } $mod = date( 'Y-m-d\TH:i:s\Z', mysql2date( 'U', $mod ) ); $pages[] = array( 'loc' => $this->unparse_url( $url ), 'lastmod' => $mod, 'changefreq' => $freq, 'priority' => $prio, ); } } $pages = apply_filters( $this->prefix . 'addl_pages_only', $pages ); return $pages; } /** * Get Additional Pages * * Return data for user entered additional pages and extra pages. * * @since 2.3.6 * @since 2.3.12.3 Refactored to use aioseop_home_url() for compatibility purposes. * @since 3.2.0 Do not include static homepage/posts page - #2126. * * @return array */ public function get_addl_pages() { $home = array(); $home = array( 'loc' => aioseop_home_url(), 'changefreq' => $this->get_default_frequency( 'homepage' ), 'priority' => $this->get_default_priority( 'homepage' ), 'image:image' => $this->get_images_from_post( (int) get_option( 'page_on_front' ) ), ); $posts = (int) get_option( 'page_for_posts' ); if ( $posts ) { $posts = $this->get_permalink( $posts ); if ( $posts === $home['loc'] ) { $posts = array(); } else { $posts = array( 'loc' => $posts, 'changefreq' => $this->get_default_frequency( 'blog' ), 'priority' => $this->get_default_priority( 'blog' ), ); } } else { $posts = array(); } $pages = $this->get_addl_pages_only(); if ( ! empty( $home ) ) { $pages[] = $home; } if ( ! empty( $posts ) ) { $pages[] = $posts; } $pages = apply_filters( $this->prefix . 'addl_pages', $pages ); $pages = $this->get_homepage_timestamp( $pages ); $pages = $this->remove_addl_static_pages( $pages ); return $pages; } /** * Removes static pages (set under Settings > Reading) from the addl-sitemap file - #2126. * * @since 3.2.0 * @since 3.3.5 Fixed a bug where the addl-sitemap file returns a 404 error when static pages have not been cleared - #3090. * * @param array $pages Pages inside the addl-sitemap file. * @return array $pages Filtered pages without static pages. */ private function remove_addl_static_pages( $pages ) { $static_homepage_id = (int) get_option( 'page_on_front' ); $is_static_homepage_set = ( 0 !== $static_homepage_id ) ? true : false; $static_blog_page_id = (int) get_option( 'page_for_posts' ); $is_static_blog_page_set = ( 0 !== $static_blog_page_id ) ? true : false; $is_homepage_set_to_latest_posts = ( 'posts' === get_option( 'show_on_front' ) ) ? true : false; $are_addl_pages_set = ! empty( $this->options['aiosp_sitemap_addl_pages'] ); $pages_to_remove = array(); if ( $is_static_blog_page_set ) { array_push( $pages_to_remove, get_permalink( $static_blog_page_id ) ); } if ( ! $is_homepage_set_to_latest_posts && $is_static_homepage_set ) { array_push( $pages_to_remove, get_permalink( $is_static_homepage_set ) ); if ( $are_addl_pages_set ) { $homepage_url = get_site_url() . '/'; array_push( $pages_to_remove, $homepage_url ); } } return $this->remove_urls_from_sitemap_page( $pages, $pages_to_remove ); } /** * Removes URLs from a sitemap page. This is used both for indexes and pages within indexes. * * @since 3.2.0 * * @param array $pages All pages, including the ones that have to be removed. * @param array $pages_to_remove The pages that have to be removed. * @return array $pages The remaining pages. */ private function remove_urls_from_sitemap_page( $pages, $pages_to_remove ) { if ( empty( $pages ) ) { return $pages; } $page_count = count( $pages ); for ( $i = 0; $i < $page_count; $i++ ) { if ( in_array( $pages[ $i ]['loc'], $pages_to_remove, true ) ) { unset( $pages[ $i ] ); } } return $pages; } /** * The get_homepage_timestamp() function. * * Gets the Last Change timestamp for the homepage if it isn't static. * * @since 3.2.0 * * @param array $urls * @return array $urls */ private function get_homepage_timestamp( $urls ) { if ( 0 !== (int) get_option( 'page_on_front' ) ) { return $urls; } $homepage_url = get_site_url() . '/'; $urls = $this->update_static_page_timestamp( $urls, $homepage_url ); return $urls; } /** * The get_posts_page_timestamp() function. * * Gets the Last Change timestamp for the posts page. * * @since 3.2.0 * * @param array $urls * @return array $urls */ private function get_posts_page_timestamp( $urls ) { $posts_page_id = (int) get_option( 'page_for_posts' ); if ( 0 === $posts_page_id ) { return $urls; } $posts_page_url = get_permalink( $posts_page_id ); $urls = $this->update_static_page_timestamp( $urls, $posts_page_url ); return $urls; } /** * The update_static_page_timestamp() function. * * Update the timestamp attribute for a static page. * * @since 3.2.0 * * @param array $urls * @param string $static_page_url * @return array $urls */ private function update_static_page_timestamp( $urls, $static_page_url ) { $lastmod = $this->get_last_modified_post_timestamp( 'post' ); if ( false === $lastmod ) { return $urls; } $url_locs = array_combine( array_keys( $urls ), wp_list_pluck( $urls, 'loc' ) ); $index = array_search( $static_page_url, $url_locs ); if ( false === $index ) { return $urls; } $urls[ $index ] = $this->insert_timestamp_as_second_attribute( $urls[ $index ], $lastmod ); return $urls; } /** * The get_last_modified_post_timestamp() function. * * Gets the last modified post. * * @since 3.2.0 * * @param string $post_type * @return mixed Timestamp of the last modified post or false if there is none. */ private function get_last_modified_post_timestamp( $post_type ) { $last_modified_post = new WP_Query( array( 'post_type' => $post_type, 'post_status' => 'publish', 'posts_per_page' => 1, 'orderby' => 'modified', 'order' => 'DESC', ) ); if ( $last_modified_post->have_posts() ) { return $this->format_timestamp_as_lastmod_attribute( $last_modified_post ); } return false; } /** * The format_timestamp_as_lastmod_attribute() function. * * Formats the timestamp for a sitemap record in order to have valid sitemap schema. * * @since 3.2.0 * * @param object $last_modified_post WP_Query for the last modified post. * @return string $lastmod */ private function format_timestamp_as_lastmod_attribute( $last_modified_post ) { $lastmod = $last_modified_post->posts[0]->post_modified_gmt; return date( 'Y-m-d\TH:i:s\Z', mysql2date( 'U', $lastmod ) ); } /** * The insert_timestamp_as_second_attribute() function. * * Inserts the timestamp for a sitemap record as the second attribute. * The lastmod subtag has to be inserted as second attribute in order to have valid schema. * * @since 3.2.0 * * @param array $url * @param string $lastmod * @return array $url */ private function insert_timestamp_as_second_attribute( $url, $lastmod ) { return array_slice( $url, 0, 1, true ) + array( 'lastmod' => $lastmod ) + array_slice( $url, 1, null, true ); } /** * Get Additional Page Links * * Return links for user entered additional pages. * * @since ? * * @return array */ public function get_addl_page_links() { if ( ! empty( $this->options[ $this->prefix . 'addl_pages' ] ) ) { return array_keys( $this->options[ $this->prefix . 'addl_pages' ] ); } return array(); } /** * Get Priority Calculation * * Scores posts based on date and relative comment count, if any. * * @since ? * * @param $date * @param mixed $stats * @return array */ public function get_prio_calc( $date, $stats ) { static $cur_time = null; if ( null === $cur_time ) { $cur_time = time(); } $time = $cur_time - mysql2date( 'U', $date ); if ( ! empty( $stats ) && isset( $stats['max'] ) && $stats['max'] ) { $minadj = $time >> 3; $maxadj = $time >> 1; $avg = $stats['count'] / $stats['total']; $calc = ( $stats['comment_count'] - $stats['min'] ) / $stats['max']; $calc = $maxadj * $calc; if ( $avg < $stats['comment_count'] ) { $minadj = $time >> 2; } else { $maxadj = $time >> 2; } if ( $calc > $maxadj ) { $calc = $maxadj; } if ( $calc < $minadj ) { $calc = $minadj; } $time -= $calc; } $days = $time / ( 60 * 60 * 24 ); $prio_table = array( 'daily' => 7, 'weekly' => 30, 'monthly' => 210, 'yearly' => null, ); $interval = 1.0; $prev_days = 0; foreach ( $prio_table as $change => $max_days ) { $interval -= 0.3; if ( null === $max_days ) { $changefreq = $change; $prio = 0.1; break; } if ( $days < $max_days ) { $int_days_max = $max_days - $prev_days; $int_days = $days - $prev_days; $prio = $interval + ( (int) ( 3 * ( ( $max_days - $int_days ) / $int_days_max ) ) / 10.0 ); $changefreq = $change; break; } $prev_days = $max_days; } return array( 'lastmod' => $date, 'changefreq' => $changefreq, 'priority' => $prio, ); } /** * Get Date Archive Priority from Posts * * Generate sitemap priority data for date archives from an array of posts. * * @since ? * * @param $posts * @return array */ public function get_date_archive_prio_from_posts( $posts ) { $archives = array(); if ( is_array( $posts ) ) { foreach ( $posts as $p ) { if ( 'post' !== $p->post_type ) { continue; } // add the post type to the date so as to support posts of different post types created on the same date. $date = date( 'Y-m', mysql2date( 'U', $p->post_date ) ) . $p->post_type; if ( empty( $archives[ $date ] ) ) { $archives[ $date ] = $p; } else { if ( $p->post_modified > $archives[ $date ]->post_modified ) { $archives[ $date ] = $p; } } } } if ( ! empty( $archives ) ) { return $this->get_prio_from_posts( $archives, $this->get_default_priority( 'archive', true ), $this->get_default_frequency( 'archive', true ), array( $this, 'get_date_archive_link_from_post', ), 'archive' ); } return $archives; } /** * Get Archive Priority from Posts * * Generate sitemap priority data for archives from an array of posts. * * @since ? * @since 3.2.0 Don't fetch WooCommerce shop page twice - #2126 * * @param $posts * @return array */ private function get_archive_prio_from_posts( $posts ) { $posttypes = array(); if ( ! empty( $this->options[ "{$this->prefix}posttypes" ] ) ) { $posttypes = $this->options[ "{$this->prefix}posttypes" ]; } if ( aioseop_is_woocommerce_active() ) { if ( in_array( 'product', $posttypes ) ) { $index = array_search( 'product', $posttypes ); unset( $posttypes[ $index ] ); } } $types_supporting_archives = get_post_types( array( 'has_archive' => true, '_builtin' => false, ), 'names' ); $types = array(); foreach ( $posts as $p ) { if ( array_key_exists( $p->post_type, $types ) ) { continue; } $types[ $p->post_type ] = $p; } $archive_pages = array(); $types = apply_filters( "{$this->prefix}include_post_types_archives", $types ); if ( $types ) { foreach ( $types as $post_type => $p ) { // TODO Add `true` in 3rd argument with in_array(); which changes it to a strict comparison. if ( ! ( in_array( $post_type, $posttypes ) && in_array( $post_type, $types_supporting_archives ) ) ) { continue; } $temp_archive_pages = $this->get_prio_from_posts( array( $p ), $this->get_default_priority( 'archive', true ), $this->get_default_frequency( 'archive', true ), array( $this, 'get_archive_link_from_post', ) ); if ( ! empty( $temp_archive_pages ) ) { $temp_archive_pages = $this->get_archive_page_timestamp( $temp_archive_pages, $post_type ); } $archive_pages = array_merge( $archive_pages, $temp_archive_pages ); } } return $archive_pages; } /** * The get_archive_page_timestamp() function. * * Get the Last Change timestamp for archive pages. * * @since 3.2.0 * * @param array $urls * @param string $post_type * @return array $urls */ private function get_archive_page_timestamp( $urls, $post_type ) { $lastmod = $this->get_last_modified_post_timestamp( $post_type ); if ( false === $lastmod ) { return $urls; } $count = count( $urls ); for ( $i = 0; $i < $count; $i++ ) { $urls[ $i ] = $this->insert_timestamp_as_second_attribute( $urls[ $i ], $lastmod ); } return $urls; } /** * Return an archive link for a post. * * @param $post * * @return bool|string */ public function get_archive_link_from_post( $post ) { return get_post_type_archive_link( $post->post_type ); } /** * Get Date Archive Link from Post * * Return a date archive link for a post. * * @since ? * * @param $post * @return bool|string */ public function get_date_archive_link_from_post( $post ) { $extra = array(); if ( 'post' !== $post->post_type ) { return false; } $date = mysql2date( 'U', $post->post_date ); return get_month_link( date( 'Y', $date ), date( 'm', $date ) ); } /** * Get Author Priority from Posts * * Generate sitemap priority data for authors from an array of posts. * * @since ? * * @param $posts * @return array */ public function get_author_prio_from_posts( $posts ) { $authors = array(); if ( is_array( $posts ) ) { foreach ( $posts as $p ) { if ( 'post' !== $p->post_type ) { continue; } if ( empty( $authors[ $p->post_author ] ) ) { $authors[ $p->post_author ] = $p; } else { if ( $p->post_modified > $authors[ $p->post_author ]->post_modified ) { $authors[ $p->post_author ] = $p; } } } } return $this->get_prio_from_posts( $authors, $this->get_default_priority( 'author', true ), $this->get_default_frequency( 'author', true ), array( $this, 'get_author_link_from_post', ), 'author' ); } /** * Get Author Link from Post * * Return an author link from a post. * * @since ? * * @param $post * @return string */ public function get_author_link_from_post( $post ) { return get_author_posts_url( $post->post_author ); } /** * Get Comment Count Stats * * Return comment statistics on an array of posts. * * @since ? * * @param $posts * @return array|int */ public function get_comment_count_stats( $posts ) { $count = 0; $total = 0.0; $min = null; $max = 0; if ( is_array( $posts ) ) { foreach ( $posts as $post ) { if ( ! empty( $post->comment_count ) ) { $cnt = $post->comment_count; $count ++; $total += $cnt; if ( null === $min ) { $min = $cnt; } if ( $max < $cnt ) { $max = $cnt; } if ( $min > $cnt ) { $min = $cnt; } } } } if ( $count ) { return array( 'max' => $max, 'min' => $min, 'total' => $total, 'count' => $cnt, ); } else { return 0; } } /** * Get Priority from Posts * * Generate sitemap priority data from an array of posts. * * @since ? * * @param $posts * @param bool $prio_override * @param bool $freq_override * @param string $linkfunc * @param string $type Type of entity being fetched viz. author, post etc. * @return array */ public function get_prio_from_posts( $posts, $prio_override = false, $freq_override = false, $linkfunc = 'get_permalink', $type = 'post' ) { $prio = array(); $args = array( 'prio_override' => $prio_override, 'freq_override' => $freq_override, 'linkfunc' => $linkfunc, ); if ( $prio_override && $freq_override ) { $stats = 0; } else { $stats = $this->get_comment_count_stats( $posts ); } if ( is_array( $posts ) ) { foreach ( $posts as $key => $post ) { // Determine if we check the post for images. $is_single = true; $post->filter = 'sample'; $timestamp = null; if ( 'get_permalink' === $linkfunc ) { $url = $this->get_permalink( $post ); } else { $url = call_user_func( $linkfunc, $post ); $is_single = false; } if ( strpos( $url, '__trashed' ) !== false ) { // excluded trashed urls. continue; } $date = $post->post_modified_gmt; if ( '0000-00-00 00:00:00' === $date ) { $date = $post->post_date_gmt; } if ( '0000-00-00 00:00:00' !== $date ) { $timestamp = $date; $date = date( 'Y-m-d\TH:i:s\Z', mysql2date( 'U', $date ) ); } else { $date = 0; } if ( $prio_override && $freq_override ) { $pr_info = array( 'lastmod' => $date, 'changefreq' => null, 'priority' => null, ); } else { if ( empty( $post->comment_count ) ) { $stat = 0; } else { $stat = $stats; } if ( ! empty( $stat ) ) { $stat['comment_count'] = $post->comment_count; } $pr_info = $this->get_prio_calc( $date, $stat ); } if ( $freq_override ) { $pr_info['changefreq'] = $freq_override; } if ( $prio_override ) { $pr_info['priority'] = $prio_override; } if ( ( 'sel' === $this->options[ $this->prefix . 'prio_post' ] ) && isset( $this->options[ $this->prefix . 'prio_post_' . $post->post_type ] ) ) { if ( 'no' !== $this->options[ $this->prefix . 'prio_post_' . $post->post_type ] && 'sel' !== $this->options[ $this->prefix . 'prio_post_' . $post->post_type ] ) { $pr_info['priority'] = $this->options[ $this->prefix . 'prio_post_' . $post->post_type ]; } } if ( ( 'sel' === $this->options[ $this->prefix . 'freq_post' ] ) && isset( $this->options[ $this->prefix . 'freq_post_' . $post->post_type ] ) ) { if ( 'no' !== $this->options[ $this->prefix . 'freq_post_' . $post->post_type ] && 'sel' !== $this->options[ $this->prefix . 'freq_post_' . $post->post_type ] ) { $pr_info['changefreq'] = $this->options[ $this->prefix . 'freq_post_' . $post->post_type ]; } } $pr_info = array( 'loc' => $url, ) + $pr_info; // Prepend loc to the array. if ( is_float( $pr_info['priority'] ) ) { $pr_info['priority'] = sprintf( '%0.1F', $pr_info['priority'] ); } // add the rss specific data. if ( $timestamp ) { $title = null; switch ( $type ) { case 'author': $title = get_the_author_meta( 'display_name', $key ); break; default: $title = get_the_title( $post ); break; } // RSS expects the GMT date. $timestamp = mysql2date( 'U', $post->post_modified_gmt ); $pr_info['rss'] = array( 'title' => $title, 'description' => get_post_field( 'post_excerpt', $post->ID ), 'pubDate' => date( 'r', $timestamp ), 'timestamp ' => $timestamp, 'post_type' => $post->post_type, ); } $pr_info['image:image'] = $is_single ? $this->get_images_from_post( $post ) : null; $pr_info = apply_filters( $this->prefix . 'prio_item_filter', $pr_info, $post, $args ); if ( ! empty( $pr_info ) ) { $prio[] = $pr_info; } } } return $prio; } /** * Get Images from Term * * Return the images attached to the term. * * @since 2.4 * @since 3.0 remove check for WP 4.4 * * @param WP_Term $term the term object. * @return array */ private function get_images_from_term( $term ) { if ( ! aiosp_include_images() ) { return array(); } $images = array(); $thumbnail_id = get_term_meta( $term->term_id, 'thumbnail_id', true ); if ( $thumbnail_id ) { $image = wp_get_attachment_url( $thumbnail_id ); if ( $image ) { $images['image:image'] = array( 'image:loc' => $image, 'image:caption' => wp_get_attachment_caption( $thumbnail_id ), 'image:title' => get_the_title( $thumbnail_id ), ); } } return $images; } /** * Get Images from Post * * Return the images from the post. * * @todo Add ~`get_attachment_postid_to_url()` function. * @todo Benchmark `wp_get_attachment_image_src()` & `wp_get_attachment_url()`. * @todo Look into using 'wp_get_attachment_image_url()'. * * @since 2.4 * @since 2.11 Optimization #2008 - Reduce the need to convert url to id. * * @param WP_Post|int $post the post object. * @return array */ private function get_images_from_post( $post ) { if ( ! aiosp_include_images() ) { return array(); } $rtn_image_attributes = array(); $post_image_ids = array(); $post_image_urls = array(); $transient_update = false; if ( is_numeric( $post ) ) { if ( 0 === $post ) { return null; } $post = get_post( $post ); } if ( 'attachment' === $post->post_type ) { if ( false === strpos( $post->post_mime_type, 'image/' ) ) { // Ignore all attachments except images. return null; } $attributes = wp_get_attachment_image_src( $post->ID ); if ( $attributes ) { $rtn_image_attributes[] = array( 'image:loc' => $this->aioseop_clean_url( $attributes[0] ), 'image:caption' => wp_get_attachment_caption( $post->ID ), 'image:title' => get_the_title( $post->ID ), ); } return $rtn_image_attributes; } // Set Image IDs w/ URLs. if ( is_null( $this->image_ids_urls ) ) { // Get Transient/Cache data. if ( is_multisite() ) { $this->image_ids_urls = get_site_transient( 'aioseop_multisite_attachment_ids_urls' ); } else { $this->image_ids_urls = get_transient( 'aioseop_attachment_ids_urls' ); } // Set default if no data exists. if ( false === $this->image_ids_urls ) { $this->image_ids_urls = array(); } } /** * Static attachment cache, 1 query vs. n posts. * * Concepts like this should be followed; although this could possibly be improved (maybe as a wrapped function) but is still good code. */ static $post_thumbnails; if ( is_null( $post_thumbnails ) || defined( 'AIOSEOP_UNIT_TESTING' ) ) { global $wpdb; $post_thumbnails = $wpdb->get_results( "SELECT post_ID, meta_value FROM $wpdb->postmeta WHERE meta_key = '_thumbnail_id'", ARRAY_A ); if ( $post_thumbnails ) { $post_thumbnails = array_combine( wp_list_pluck( $post_thumbnails, 'post_ID' ), wp_list_pluck( $post_thumbnails, 'meta_value' ) ); } } if ( isset( $post_thumbnails[ $post->ID ] ) ) { $post_image_ids[] = intval( $post_thumbnails[ $post->ID ] ); } $post_image_ids = array_merge( $post_image_ids, $this->get_gallery_image_ids( $post ) ); $this->get_gallery_images( $post, $post_image_urls ); // Get image URLs from content. $content = $post->post_content; $content .= $this->get_content_from_galleries( $content ); $this->parse_content_for_images( $content, $post_image_urls ); if ( ! empty( $post_image_urls ) ) { // Remove any invalid/empty images. $post_image_urls = array_filter( $post_image_urls, array( $this, 'is_image_url_valid' ) ); // If possible, get ID from URL, and store the post's attachment ID => URL value. // This is to base the attachment query on the ID instead of the URL; which is less SQL intense. foreach ( $post_image_urls as $k1_index => &$v1_image_url ) { $v1_image_url = aiosp_common::absolutize_url( $v1_image_url ); $attachment_id = aiosp_common::attachment_url_to_postid( $v1_image_url ); if ( $attachment_id ) { if ( ! isset( $this->image_ids_urls[ $attachment_id ] ) ) { // Use transient/cache data. $this->image_ids_urls[ $attachment_id ] = array( $v1_image_url ); $transient_update = true; } else { // If transient/cache data is already set, and URL is not already stored. if ( ! in_array( $v1_image_url, $this->image_ids_urls[ $attachment_id ], true ) ) { $this->image_ids_urls[ $attachment_id ][] = $v1_image_url; $transient_update = true; } } // Store and use ID instead. array_push( $post_image_ids, $attachment_id ); unset( $post_image_urls[ $k1_index ] ); } } } // Site's Images. if ( $post_image_ids ) { // Filter out duplicates. $post_image_ids = array_unique( $post_image_ids ); foreach ( $post_image_ids as $v1_image_id ) { // Set base URL to display later in this instance, or later (transient/cache) instances. // Converting ID from URL can also be heavy on memory & time. if ( ! isset( $this->image_ids_urls[ $v1_image_id ] ) ) { // Sets any remaining post image IDs that weren't converted from URL. $this->image_ids_urls[ $v1_image_id ] = array( 'base_url' => $this->aioseop_clean_url( wp_get_attachment_url( $v1_image_id ) ), ); $transient_update = true; } else { if ( empty( $this->image_ids_urls[ $v1_image_id ]['base_url'] ) ) { $this->image_ids_urls[ $v1_image_id ]['base_url'] = $this->aioseop_clean_url( wp_get_attachment_url( $v1_image_id ) ); $transient_update = true; } } // Set return variable for image data/attributes. $rtn_image_attributes[] = array( 'image:loc' => $this->image_ids_urls[ $v1_image_id ]['base_url'], 'image:caption' => wp_get_attachment_caption( $v1_image_id ), 'image:title' => get_the_title( $v1_image_id ), ); } } // External/Custom images remaining. if ( ! empty( $post_image_urls ) ) { foreach ( $post_image_urls as $v1_image_url ) { $rtn_image_attributes[] = array( 'image:loc' => $v1_image_url, ); } } if ( $transient_update ) { add_action( 'shutdown', array( $this, 'set_transient_attachment_ids_urls' ) ); } return $rtn_image_attributes; } /** * Set Transient Attachment IDs => URLS * * Set Transient for Image IDs => URLs * * @since 2.11 */ public function set_transient_attachment_ids_urls() { if ( is_multisite() ) { set_site_transient( 'aioseop_multisite_attachment_ids_urls', $this->image_ids_urls, DAY_IN_SECONDS ); } else { set_transient( 'aioseop_attachment_ids_urls', $this->image_ids_urls, DAY_IN_SECONDS ); } } /** * Get Gallery Images * * Fetch images from WP, Jetpack and WooCommerce galleries. * * @since 2.4.2 * @since 2.11 Optimization #2008 - Reduce the need to convert url to id. * * @param WP_Post $post The post. * @param array $images the array of images. */ private function get_gallery_images( $post, &$images ) { if ( false === apply_filters( 'aioseo_include_images_in_wp_gallery', true ) ) { return; } // Check images galleries in the content. DO NOT run the_content filter here as it might cause issues with other shortcodes. if ( has_shortcode( $post->post_content, 'gallery' ) ) { /* * TODO Investigate other alternatives to retrieve ID instead. Specifically Jetpack data. * * Is this even necessary? Jetpack uses many of the WP functions, some of which may already be in use. * This is also limited to 1 source, and doesn't check other sources once a value is obtained. * * @link https://hayashikejinan.com/wp-content/uploads/jetpack_api/classes/Jetpack_PostImages.html */ if ( class_exists( 'Jetpack_PostImages' ) ) { // Get the jetpack gallery images. $jetpack = Jetpack_PostImages::get_images( $post->ID ); if ( $jetpack ) { foreach ( $jetpack as $jetpack_image ) { $images[] = $jetpack_image['src']; } } } } $images = array_unique( $images ); } /** * Get Gallery Image IDs * * @uses get_post_galleries() * @link https://developer.wordpress.org/reference/functions/get_post_galleries/ * * @since 2.11 * * @param WP_Post $post * @return array */ private function get_gallery_image_ids( $post ) { $rtn_image_ids = array(); if ( false === apply_filters( 'aioseo_include_images_in_wp_gallery', true ) ) { return $rtn_image_ids; } // Check images galleries in the content. DO NOT run the_content filter here as it might cause issues with other shortcodes. if ( has_shortcode( $post->post_content, 'gallery' ) ) { // Get the default WP gallery images. $galleries = get_post_galleries( $post, false ); if ( ! empty( $galleries ) ) { foreach ( $galleries as $gallery ) { $gallery_ids = explode( ',', $gallery['ids'] ); if ( ! empty( $gallery_ids ) ) { foreach ( $gallery_ids as $image_id ) { // Skip if invalid id. if ( ! is_numeric( $image_id ) ) { continue; } $image_id = intval( $image_id ); array_push( $rtn_image_ids, $image_id ); } } } } } // Check WooCommerce product gallery. if ( class_exists( 'WooCommerce' ) ) { $wc_image_ids = get_post_meta( $post->ID, '_product_image_gallery', true ); if ( ! empty( $woo_images ) ) { $wc_image_ids = array_filter( explode( ',', $wc_image_ids ) ); foreach ( $wc_image_ids as $image_id ) { if ( is_numeric( $image_id ) ) { $image_id = intval( $image_id ); array_push( $rtn_image_ids, $image_id ); } } } } return array_unique( $rtn_image_ids ); } /** * Get Content from Galleries * * Parses the content to find out if specified images galleries exist and if they do, parse them for images. * Supports NextGen. * * @since 2.4.2 * * @param string $content The post content. * @return string */ private function get_content_from_galleries( $content ) { // Support for NextGen Gallery. static $gallery_types; $gallery_types = array( 'ngg_images' ); $types = apply_filters( 'aioseop_gallery_shortcodes', $gallery_types ); $gallery_content = ''; if ( ! $types ) { return $gallery_content; } $found = array(); if ( $types ) { foreach ( $types as $type ) { if ( has_shortcode( $content, $type ) ) { $found[] = $type; } } } // If none of the shortcodes-of-interest are found, bail. if ( empty( $found ) ) { return $gallery_content; } $galleries = array(); if ( ! preg_match_all( '/' . get_shortcode_regex() . '/s', $content, $matches, PREG_SET_ORDER ) ) { return $gallery_content; } // Collect the shortcodes and their attributes. foreach ( $found as $type ) { foreach ( $matches as $shortcode ) { if ( $type === $shortcode[2] ) { $attributes = shortcode_parse_atts( $shortcode[3] ); if ( '' === $attributes ) { // Valid shortcode without any attributes. $attributes = array(); } $galleries[ $shortcode[2] ] = $attributes; } } } // Recreate the shortcodes and then render them to get the HTML content. if ( $galleries ) { foreach ( $galleries as $shortcode => $attributes ) { $code = '[' . $shortcode; foreach ( $attributes as $key => $value ) { $code .= " $key=$value"; } $code .= ']'; $gallery_content .= aioseop_do_shortcodes( $code ); } } return $gallery_content; } /** * AIOSEOP Clean URL * * Cleans the URL so that its acceptable in the sitemap. * * @since 2.4.1 * * @param string $url The image url. * @return string */ public function aioseop_clean_url( $url ) { // remove the query string. $url = strtok( $url, '?' ); // make the url XML-safe. $url = htmlspecialchars( $url, ENT_COMPAT, 'UTF-8' ); // Make the url absolute, if its relative. $url = aiosp_common::absolutize_url( $url ); return apply_filters( 'aioseop_clean_url', $url ); } /** * The is_image_url_valid() function. * * Checks whether the image URL is valid. * * @since 2.4.1 * @since 2.4.3 Compatibility with Pre v4.7 wp_parse_url(). * @since 2.11.0 Sitemap Optimization #2008 - Changed to a more appropriate name. * @since 3.0.0 Remove checks for old WP versions. * @since 3.2.0 Remove redundant code. * * @param string $image The image src. * @return bool */ public function is_image_url_valid( $image ) { // Bail if empty image. if ( empty( $image ) ) { return false; } $image = aiosp_common::absolutize_url( $image ); $extn = pathinfo( $image, PATHINFO_EXTENSION ); $allowed = apply_filters( 'aioseop_allowed_image_extensions', self::$image_extensions ); // Bail if image does not refer to an image file otherwise Google Search Console might reject the sitemap. if ( ! in_array( $extn, $allowed, true ) ) { return false; } $image_host = wp_parse_url( $image, PHP_URL_HOST ); $host = wp_parse_url( home_url(), PHP_URL_HOST ); if ( $image_host !== $host ) { // Allowed hosts will be provided in a wildcard format i.e. img.yahoo.* or *.akamai.*. // And we will convert that into a regular expression for matching. $whitelist = apply_filters( 'aioseop_images_allowed_from_hosts', array() ); $allowed = false; if ( $whitelist ) { foreach ( $whitelist as $pattern ) { if ( preg_match( '/' . str_replace( '*', '.*', $pattern ) . '/', $image_host ) === 1 ) { $allowed = true; break; } } } return $allowed; } return true; } /** * Parse Content for Images * * Parse the post for images. * * @since 2.9.1 * * @param string $content the post content. * @param array $images the array of images. */ public function parse_content_for_images( $content, &$images ) { // These tags should be WITHOUT trailing space because some plugins such as the nextgen gallery put newlines immediately after loadHTML( $content ); libxml_clear_errors(); // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase $dom->preserveWhiteSpace = false; $matches = $dom->getElementsByTagName( 'img' ); foreach ( $matches as $match ) { $images[] = $match->getAttribute( 'src' ); } } else { // Fall back to regex, but also report an error. global $img_err_msg; if ( ! isset( $img_err_msg ) ) { // we will log this error message only once, not per post. $img_err_msg = true; $this->debug_message( 'DOMDocument not found; using REGEX' ); } preg_match_all( '/options[ "{$this->prefix}indexes" ] ) ) { $args['number'] = $this->max_posts; $args['offset'] = $page * $this->max_posts; } $args['taxonomy'] = $this->show_or_hide_taxonomy( $taxonomies ); $args['exclude'] = array(); if ( $this->option_isset( 'excl_terms' ) ) { foreach ( $taxonomies as $v1_taxonomy ) { if ( isset( $this->options[ $this->prefix . 'excl_terms' ][ $v1_taxonomy ] ) ) { $args['exclude'] = array_merge( $args['exclude'], $this->options[ $this->prefix . 'excl_terms' ][ $v1_taxonomy ]['terms'] ); } } } /** * The aioseop_sitemap_exclude_tax_terms filter hook. * * Allows users to exclude (or include) taxonomy terms from the sitemap. * * @since 2.9 * @since 3.2.0 Rename filter hook & remove redundant params. * * @param array $args { * @type array $taxonomy Name of the taxonomy that is being included in the sitemap. * @type array $exclude IDs of taxonomy terms of the relevant taxonomy that need to be excluded. * } */ $args = apply_filters( 'aioseop_sitemap_exclude_tax_terms', $args ); return $args; } /** * Set Post Args * * Return excluded categories and pages for post queries. * * @since ? * @since 3.0 Change 'excl_terms' to tax_query format. (Pro #240) * * @param $args * @return mixed */ public function set_post_args( $args ) { if ( $this->option_isset( 'excl_terms' ) ) { foreach ( $this->options[ $this->prefix . 'excl_terms' ] as $k1_taxonomy => $v1_tax_terms ) { if ( ! isset( $args['tax_query'] ) ) { $args['tax_query'] = array( 'relation' => 'AND', ); } $args['tax_query'][] = array( 'taxonomy' => $k1_taxonomy, 'terms' => $v1_tax_terms['terms'], 'operator' => 'NOT IN', ); } } if ( $this->option_isset( 'excl_pages' ) ) { $args['exclude'] = $this->options[ $this->prefix . 'excl_pages' ]; } return $args; } /** * Get Data Archive Priority Data * * Return sitemap data for date archives. * * @since ? * * @return array */ public function get_date_archive_prio_data() { $args = array( 'numberposts' => 50000, 'post_type' => 'post', ); $args = $this->set_post_args( $args ); $posts = $this->get_all_post_type_data( $args ); return $this->get_date_archive_prio_from_posts( $posts ); } /** * Get Author Priority Data * * Return sitemap data for authors. * * @since ? * * @return array */ public function get_author_prio_data() { $args = array( 'numberposts' => 50000, 'post_type' => 'post', ); $args = $this->set_post_args( $args ); $posts = $this->get_all_post_type_data( $args ); return $this->get_author_prio_from_posts( $posts ); } /** * Get All Post Priority Data * * Return sitemap data for posts. * * @since ? * @since 3.2.0 Update Last Change timestamp for WooCommerce shop page. * * @param string $include * @param string $status * @param int $page * @return array */ public function get_all_post_priority_data( $include = 'any', $status = 'publish', $page = 0 ) { $posts = array(); $page_query = array(); if ( ! empty( $this->options[ "{$this->prefix}indexes" ] ) ) { $page_query = array( 'offset' => $page * $this->max_posts ); } if ( ( 'publish' === $status ) && ( 'attachment' === $include ) ) { $status = 'inherit'; } // TODO Add `true` in 3rd argument with in_array(); which changes it to a strict comparison. if ( is_array( $include ) ) { $pos = array_search( 'attachment', $include ); if ( false !== $pos ) { unset( $include[ $pos ] ); $att_args = array( 'post_type' => 'attachment', 'post_status' => 'inherit', ); $att_args = array_merge( $att_args, $page_query ); $posts = $this->get_all_post_type_data( $att_args ); } } $args = array( 'post_type' => $include, 'post_status' => $status, ); $args = array_merge( $args, $page_query ); $args = $this->set_post_args( $args ); $posts = array_merge( $this->get_all_post_type_data( $args ), $posts ); $links = $this->get_prio_from_posts( $posts, $this->get_default_priority( 'post', true ), $this->get_default_frequency( 'post', true ) ); $links = array_merge( $links, $this->get_archive_prio_from_posts( $posts ) ); $is_sitemap_indexes_disabled = empty( $this->options['aiosp_sitemap_indexes'] ); if ( $is_sitemap_indexes_disabled || ( ! $is_sitemap_indexes_disabled && 'page' === $include ) ) { $links = $this->get_posts_page_timestamp( $links ); $links = $this->get_prio_freq_static_homepage( $links ); $links = $this->update_woocommerce_shop_timestamp( $links ); } return $links; } /** * The get_prio_freq_static_homepage() function. * * Sets the priority and frequency for the homepage if it is static. * * @since 3.2.0 * * @param array $links * @return array $links */ private function get_prio_freq_static_homepage( $links ) { if ( 0 === (int) get_option( 'page_on_front' ) ) { return $links; } $prio = 'no'; $freq = 'no'; if ( isset( $this->options['aiosp_sitemap_prio_homepage'] ) ) { $prio = $this->options['aiosp_sitemap_prio_homepage']; } if ( isset( $this->options['aiosp_sitemap_freq_homepage'] ) ) { $freq = $this->options['aiosp_sitemap_freq_homepage']; } $homepage_url = get_site_url() . '/'; $homepage_index = array_search( $homepage_url, array_column( $links, 'loc' ) ); // phpcs:ignore PHPCompatibility.FunctionUse.NewFunctions.array_columnFound if ( ! $homepage_url ) { return $links; } if ( 'no' !== $prio ) { $links[ $homepage_index ]['priority'] = $prio; } if ( 'no' !== $freq ) { $links[ $homepage_index ]['changefreq'] = $freq; } return $links; } /** * The update_woocommerce_shop_timestamp() function. * * Updates the Last Change timestamp for the WooCommerce shop page based on the last modified product - #2126. * * @since 3.2.0 * * @param array $links * @return array $links */ private function update_woocommerce_shop_timestamp( $links ) { if ( ! aioseop_is_woocommerce_active() ) { return $links; } $shop_page_url = get_permalink( wc_get_page_id( 'shop' ) ); $shop_page_index = array_search( $shop_page_url, array_column( $links, 'loc' ) ); // phpcs:ignore PHPCompatibility.FunctionUse.NewFunctions.array_columnFound if ( ! $shop_page_index ) { return $links; } // TODO Use get_last_modified_post_timestamp() instead when #2721 is merged. $latest_modified_product = new WP_Query( array( 'post_type' => 'product', 'post_status' => 'publish', 'posts_per_page' => 1, 'orderby' => 'modified', 'order' => 'DESC', ) ); if ( $latest_modified_product->have_posts() ) { $timestamp = $latest_modified_product->posts[0]->post_modified_gmt; $lastmod = date( 'Y-m-d\TH:i:s\Z', mysql2date( 'U', $timestamp ) ); // Last Change timestamp needs to be inserted as second attribute in order to have valid sitemap schema. // TODO Use insert_timestamp_as_second_attribute() instead when #2721 is merged. $links[ $shop_page_index ] = array_slice( $links[ $shop_page_index ], 0, 1, true ) + array( 'lastmod' => $lastmod ) + array_slice( $links[ $shop_page_index ], 1, null, true ); } return $links; } /** * Get All Permalinks * * Return a list of all permalinks. * * @since ? * * @param string $include * @param string $status * @return array */ public function get_all_permalinks( $include = 'any', $status = 'publish' ) { $args = array( 'post_type' => $include, 'post_status' => $status, ); $args = $this->set_post_args( $args ); $posts = $this->get_all_post_type_data( $args ); $links = $this->get_post_permalinks( $posts ); if ( $this->option_isset( 'archive' ) ) { $links = array_merge( $links, $this->get_archive_permalinks( $posts ) ); } if ( $this->option_isset( 'author' ) ) { $links = array_merge( $links, $this->get_author_permalinks( $posts ) ); } return $links; } /** * Cache Structure * * Static memory cache for permalink_structure option. * * @since ? * * @param $pre * @return null */ public function cache_structure( $pre ) { return $this->cache_struct; } /** * Cache Home * * Static memory cache for home option. * * @since ? * * @param $pre * @return null */ public function cache_home( $pre ) { return $this->cache_home; } /** * Cache Options * * Cache permalink_structure and home for repeated sitemap queries. * * @since ? */ public function cache_options() { static $start = true; if ( $start ) { $this->cache_struct = get_option( 'permalink_structure' ); if ( ! empty( $this->cache_struct ) ) { add_filter( 'pre_option_permalink_structure', array( $this, 'cache_structure' ) ); } $this->cache_home = get_option( 'home' ); if ( ! empty( $this->cache_home ) ) { add_filter( 'pre_option_home', array( $this, 'cache_home' ) ); } $start = false; } } /** * Get Term Link * * Call get_term_link with caching in place. * * @since ? * * @param $term * @param string $taxonomy * @return string|WP_Error */ public function get_term_link( $term, $taxonomy = '' ) { static $start = true; if ( $start ) { $this->cache_options(); $start = false; } return get_term_link( $term, $taxonomy ); } /** * Get Permalink * * Call get_permalink with caching in place. * * @since ? * * @param $post * @return false|string */ public function get_permalink( $post ) { static $start = true; if ( $start ) { $this->cache_options(); $start = false; } return aioseop_get_permalink( $post ); } /** * Show or hide the taxonomy/taxonomies. * * @since 3.0.0 * * @param array $taxonomy The array of taxonomy slugs. * * @return array The array of taxonomy slugs that need to be shown. */ private function show_or_hide_taxonomy( $taxonomy ) { /** * Determines whether to show or hide the taxonomy/taxonomies. * * @since 3.0.0 * * @param array $taxonomy The array of taxonomy slugs. */ return apply_filters( "{$this->prefix}show_taxonomy", $taxonomy ); } /** * Get All Terms Counts * * Return term counts using wp_count_terms(). * * @since ? * * @param $args * @return array|int|mixed|null|WP_Error */ public function get_all_term_counts( $args ) { $term_counts = null; if ( ! empty( $args ) && ! empty( $args['taxonomy'] ) ) { // TODO Add `true` in 3rd argument with in_array(); which changes it to a strict comparison. if ( ! is_array( $args['taxonomy'] ) || ( count( $args['taxonomy'] ) == 1 ) ) { if ( is_array( $args['taxonomy'] ) ) { $args['taxonomy'] = array_shift( $args['taxonomy'] ); } $term_counts = wp_count_terms( $this->show_or_hide_taxonomy( $args['taxonomy'] ), array( 'hide_empty' => true ) ); } else { foreach ( $args['taxonomy'] as $taxonomy ) { if ( 'all' === $taxonomy ) { continue; } $term_counts[ $taxonomy ] = wp_count_terms( $this->show_or_hide_taxonomy( $taxonomy ), array( 'hide_empty' => true ) ); } } } $term_counts = apply_filters( $this->prefix . 'term_counts', $term_counts, $args ); return $term_counts; } /** * Get All Post Counts * * Return post counts. * * @since ? * @since 2.4.3 Refactored to use get_post_count() instead of wp_count_posts(). * * @param $args * @return array */ public function get_all_post_counts( $args ) { $post_counts = array(); $status = 'inherit'; if ( ! empty( $args['post_status'] ) ) { $status = $args['post_status']; } if ( ! empty( $args ) && ! empty( $args['post_type'] ) ) { // #884: removed hard-to-understand code here which suspected $args['post_type'] to NOT be an array. Do not see any case in which this is likely to happen. foreach ( $args['post_type'] as $post_type ) { $count_args = $args; if ( 'all' === $post_type ) { continue; } if ( 'attachment' === $post_type ) { $count_args['post_status'] = 'inherit'; } $count_args['post_type'] = $post_type; $post_counts[ $post_type ] = $this->get_post_count( $count_args ); } } $post_counts = apply_filters( $this->prefix . 'post_counts', $post_counts, $args ); return $post_counts; } /** * Modify Post Params for External Plugins * * Modify the post arguments in case third-party plugins are being used e.g. WPML. * * @since 2.4.5 * * @param $args */ public function modify_post_params_for_external_plugins( &$args ) { // if WPML is being used, do not suppress filters. if ( defined( 'ICL_SITEPRESS_VERSION' ) ) { $args['suppress_filters'] = false; } $args = apply_filters( $this->prefix . 'modify_post_params', $args ); } /** * Get Post Count * * Return post counts for the specified arguments. * * @since ? * * @param $args * @return int */ public function get_post_count( $args ) { $this->modify_post_params_for_external_plugins( $args ); // we will use WP_Query instead of get_posts here as that is more efficient. // BEWARE: since we are using WP_Query, suppress_filters is false. $args['posts_per_page'] = -1; $args['fields'] = 'ids'; $args['update_post_meta_cache'] = false; $args['update_post_term_cache'] = false; $query = new WP_Query( $args ); if ( $query->have_posts() ) { return $query->post_count; } return 0; } /** * Get total post count. * * @param $args * * @return int */ public function get_total_post_count( $args ) { $total = 0; $counts = $this->get_all_post_counts( $args ); if ( ! empty( $counts ) ) { foreach ( $counts as $count ) { $total += $count; } } return $total; } /** * Get All Post Type Data * * Return post data using get_posts(). * * @since ? * @since 3.0 Changed to exclude noindex post types & posts. #1382 * * @global array $aioseop_options * * @param array $args Query Arguments. * @return array|mixed */ public function get_all_post_type_data( $args ) { global $aioseop_options; $defaults = array( 'numberposts' => $this->max_posts, 'offset' => 0, 'category' => 0, 'orderby' => 'post_date', 'order' => 'ASC', 'include' => array(), 'exclude' => array(), 'post_type' => 'any', 'meta_query' => array(), 'cache_results' => false, 'no_found_rows' => true, ); $this->modify_post_params_for_external_plugins( $defaults ); /* * Filter to exclude password protected posts. * TODO: move to its own function and call it from here, returning whatever is appropriate. * @since 2.3.12 */ if ( apply_filters( 'aioseop_sitemap_include_password_posts', true ) === false ) { $defaults['has_password'] = false; } $args = wp_parse_args( $args, $defaults ); if ( empty( $args['post_type'] ) ) { return apply_filters( $this->prefix . 'post_filter', array(), $args ); } $exclude_slugs = array(); if ( ! empty( $args['exclude'] ) ) { $exclude = preg_split( '/[\s,]+/', trim( $args['exclude'] ) ); if ( ! empty( $exclude ) ) { foreach ( $exclude as $k => $v ) { if ( ! is_numeric( $v ) ) { $exclude_slugs[] = $v; unset( $exclude[ $k ] ); } } if ( ! empty( $exclude_slugs ) ) { $args['exclude'] = implode( ',', $exclude ); } } } if ( ! is_array( $args['exclude'] ) ) { $args['exclude'] = explode( ',', $args['exclude'] ); } // Exclude (method) query args. $ex_args = $args; $ex_args['meta_query'] = array( 'relation' => 'OR', array( 'key' => '_aioseop_sitemap_exclude', 'value' => 'on', 'compare' => '=', ), array( 'key' => '_aioseop_noindex', 'value' => 'on', 'compare' => '=', ), ); // This needs to be -1 so that excluding posts isn't restricted to affect posts to not be excluded properly. $ex_args['posts_per_page'] = -1; $ex_args['fields'] = 'ids'; // Exclude (method) query. $q_exclude = new WP_Query( $ex_args ); if ( ! empty( $q_exclude->posts ) ) { $args['exclude'] = array_merge( $args['exclude'], $q_exclude->posts ); } $this->excludes = array_merge( $args['exclude'], $exclude_slugs ); // Add excluded slugs and IDs to class var. // Avoid if possible. // Include (method) query args for including posts that may have been excluded; // for example, exclude post type, but include certain posts. // NOTE: Do NOT use this for basic including. It's best to avoid an additional query. $args_include = array( 'post_type' => array(), 'meta_query' => array( 'relation' => 'OR', array( 'key' => '_aioseop_noindex', 'value' => 'off', 'compare' => '=', ), ), 'posts_per_page' => $this->max_posts, ); // Exclude from main query, and check if a Query Include is needed. // Check for NoIndex Post Types, BUT also check for Index on NoIdex Post Type. if ( is_array( $aioseop_options['aiosp_cpostnoindex'] ) ) { // Check if wp_query_args post_type is an array or string. if ( is_array( $args['post_type'] ) ) { foreach ( $args['post_type'] as $index => $post_type ) { if ( in_array( $post_type, $aioseop_options['aiosp_cpostnoindex'], true ) ) { $args_include['post_type'][] = $post_type; unset( $args['post_type'][ $index ] ); } } } else { if ( in_array( $args['post_type'], $aioseop_options['aiosp_cpostnoindex'], true ) ) { $args_include['post_type'][] = $args['post_type']; $q_include = new WP_Query( $args_include ); // Return posts on single post type query, since no additional query is needed. return $q_include->posts; } } } // Avoid if possible. // Include (method) query. // NOTE: Do NOT use this for basic including. It's best to avoid an additional query. $posts_include = array(); if ( ! empty( $args_include['post_type'] ) ) { $q_include = new WP_Query( $args_include ); // When posts exists from the include method, add to $posts_include to add to final query. if ( ! empty( $q_include->posts ) ) { $posts_include = $q_include->posts; } } // TODO: consider using WP_Query instead of get_posts to improve efficiency. /** * {$module_prefix}post_query * * Arguments to use on get_posts(). * * @since ? * * @param array $args { * Arguments/params for get_posts. * @see get_posts() * @link https://developer.wordpress.org/reference/functions/get_posts/ * } */ $posts = get_posts( apply_filters( $this->prefix . 'post_query', $args ) ); // TODO Possibly change to exclude with post__not_in. // Hardcoded exclude concept. if ( ! empty( $exclude_slugs ) ) { foreach ( $posts as $k => $v ) { // TODO Add `true` in 3rd argument with in_array(); which changes it to a strict comparison. if ( in_array( $v->post_name, $exclude_slugs ) ) { unset( $posts[ $k ] ); } } } // Hardcoded include concept. foreach ( $posts_include as $k1_post_id => $v1_post ) { $posts[ $k1_post_id ] = $v1_post; } /** * {$module_prefix}post_filter * * Posts from finalized query for sitemap data. * * @since ? * * @param array $posts { * @type WP_Post ${$post_id} { * @see WP_Post object for more information. * @link https://codex.wordpress.org/Class_Reference/WP_Post * } * } * @param array $args { * Arguments/params for get_posts. * @see get_posts() * @link https://developer.wordpress.org/reference/functions/get_posts/ * } */ $posts = apply_filters( $this->prefix . 'post_filter', $posts, $args ); return $posts; } } }