*/ namespace RankMath\Instant_Indexing; use RankMath\KB; use RankMath\Helper; use RankMath\Module\Base; use RankMath\Traits\Hooker; use RankMath\Traits\Ajax; use RankMath\Admin\Options; use RankMath\Helpers\Param; defined( 'ABSPATH' ) || exit; /** * Instant_Indexing class. */ class Instant_Indexing extends Base { use Hooker, Ajax; /** * API Object. * * @var string */ private $api; /** * Keep log of submitted objects to avoid double submissions. * * @var array */ private $submitted = []; /** * Store previous post status that we can check agains in save_post. * * @var array */ private $previous_post_status = []; /** * Store original permalinks for when they get trashed. * * @var array */ private $previous_post_permalinks = []; /** * Restrict to one request every X seconds to a given URL. */ const THROTTLE_LIMIT = 5; /** * Constructor. */ public function __construct() { parent::__construct(); $this->action( 'admin_enqueue_scripts', 'enqueue', 20 ); if ( ! $this->is_configured() ) { Api::get()->reset_key(); } $post_types = $this->get_auto_submit_post_types(); if ( ! empty( $post_types ) ) { $this->filter( 'wp_insert_post_data', 'before_save_post', 10, 4 ); } foreach ( $post_types as $post_type ) { $this->action( 'save_post_' . $post_type, 'save_post', 10, 3 ); $this->filter( "bulk_actions-edit-{$post_type}", 'post_bulk_actions', 11 ); $this->filter( "handle_bulk_actions-edit-{$post_type}", 'handle_post_bulk_actions', 10, 3 ); } $this->filter( 'post_row_actions', 'post_row_actions', 10, 2 ); $this->filter( 'page_row_actions', 'post_row_actions', 10, 2 ); $this->filter( 'admin_init', 'handle_post_row_actions' ); $this->action( 'wp', 'serve_api_key' ); $this->action( 'rest_api_init', 'init_rest_api' ); } /** * Load the REST API endpoints. */ public function init_rest_api() { $rest = new Rest(); $rest->register_routes(); } /** * Add bulk actions for applicable posts, pages, CPTs. * * @param array $actions Actions. * @return array New actions. */ public function post_bulk_actions( $actions ) { $actions['rank_math_indexnow'] = esc_html__( 'Instant Indexing: Submit Pages', 'rank-math' ); return $actions; } /** * Action links for the post listing screens. * * @param array $actions Action links. * @param object $post Current post object. * @return array */ public function post_row_actions( $actions, $post ) { if ( ! Helper::has_cap( 'general' ) ) { return $actions; } if ( 'publish' !== $post->post_status ) { return $actions; } $post_types = $this->get_auto_submit_post_types(); if ( ! in_array( $post->post_type, $post_types, true ) ) { return $actions; } $link = wp_nonce_url( add_query_arg( [ 'action' => 'rank_math_instant_index_post', 'index_post_id' => $post->ID, 'method' => 'bing_submit', ] ), 'rank_math_instant_index_post' ); $actions['indexnow_submit'] = '' . __( 'Instant Indexing: Submit Page', 'rank-math' ) . ''; return $actions; } /** * Handle post row action link actions. * * @return void */ public function handle_post_row_actions() { if ( 'rank_math_instant_index_post' !== Param::get( 'action' ) ) { return; } $post_id = absint( Param::get( 'index_post_id' ) ); if ( ! $post_id ) { return; } if ( ! wp_verify_nonce( Param::get( '_wpnonce' ), 'rank_math_instant_index_post' ) ) { return; } if ( ! Helper::has_cap( 'general' ) ) { return; } $this->api_submit( get_permalink( $post_id ), true ); Helper::redirect( remove_query_arg( [ 'action', 'index_post_id', 'method', '_wpnonce' ] ) ); exit; } /** * Handle bulk actions for applicable posts, pages, CPTs. * * @param string $redirect Redirect URL. * @param string $doaction Performed action. * @param array $object_ids Post IDs. * * @return string New redirect URL. */ public function handle_post_bulk_actions( $redirect, $doaction, $object_ids ) { if ( 'rank_math_indexnow' !== $doaction || empty( $object_ids ) ) { return $redirect; } if ( ! Helper::has_cap( 'general' ) ) { return $redirect; } $urls = []; foreach ( $object_ids as $object_id ) { $urls[] = get_permalink( $object_id ); } $this->api_submit( $urls, true ); return $redirect; } /** * Register admin page. */ public function register_admin_page() { $tabs = [ 'url-submission' => [ 'icon' => 'rm-icon rm-icon-instant-indexing', 'title' => esc_html__( 'Submit URLs', 'rank-math' ), 'desc' => esc_html__( 'Send URLs directly to the IndexNow API.', 'rank-math' ) . ' ' . esc_html__( 'Learn more', 'rank-math' ) . '', 'classes' => 'rank-math-advanced-option', 'file' => dirname( __FILE__ ) . '/views/console.php', ], 'settings' => [ 'icon' => 'rm-icon rm-icon-settings', 'title' => esc_html__( 'Settings', 'rank-math' ), /* translators: Link to kb article */ 'desc' => sprintf( esc_html__( 'Instant Indexing module settings. %s.', 'rank-math' ), '' . esc_html__( 'Learn more', 'rank-math' ) . '' ), 'file' => dirname( __FILE__ ) . '/views/options.php', ], 'history' => [ 'icon' => 'rm-icon rm-icon-htaccess', 'title' => esc_html__( 'History', 'rank-math' ), 'desc' => esc_html__( 'The last 100 IndexNow API requests.', 'rank-math' ), 'classes' => 'rank-math-advanced-option', 'file' => dirname( __FILE__ ) . '/views/history.php', ], ]; if ( 'easy' === Helper::get_settings( 'general.setup_mode', 'advanced' ) ) { // Move ['settings'] to the top. $tabs = [ 'settings' => $tabs['settings'] ] + $tabs; } /** * Allow developers to add new sections in the IndexNow settings. * * @param array $tabs */ $tabs = $this->do_filter( 'settings/instant_indexing', $tabs ); new Options( [ 'key' => 'rank-math-options-instant-indexing', 'title' => esc_html__( 'Instant Indexing', 'rank-math' ), 'menu_title' => esc_html__( 'Instant Indexing', 'rank-math' ), 'capability' => 'rank_math_general', 'tabs' => $tabs, 'position' => 100, ] ); } /** * Store previous post status & permalink before saving the post. * * @param array $data Post data. * @param array $postarr Raw post data. * @param array $unsanitized_postarr Unsanitized post data. * @param bool $update Whether this is an existing post being updated or not. */ public function before_save_post( $data, $postarr, $unsanitized_postarr, $update = false ) { if ( ! $update ) { return $data; } $this->previous_post_status[ $postarr['ID'] ] = get_post_status( $postarr['ID'] ); $this->previous_post_permalinks[ $postarr['ID'] ] = str_replace( '__trashed', '', get_permalink( $postarr['ID'] ) ); return $data; } /** * When a post from a watched post type is published or updated, submit its URL * to the API and add notice about it. * * @param int $post_id Post ID. * @param object $post Post object. * * @return void */ public function save_post( $post_id, $post ) { // Check if already submitted. if ( in_array( $post_id, $this->submitted, true ) ) { return; } // Check if post status changed to publish or trash. if ( ! in_array( $post->post_status, [ 'publish', 'trash' ], true ) ) { return; } // If new status is trash, check if previous status was publish. if ( 'trash' === $post->post_status && 'publish' !== $this->previous_post_status[ $post_id ] ) { return; } if ( wp_is_post_revision( $post_id ) || wp_is_post_autosave( $post_id ) ) { return; } if ( ! Helper::is_post_indexable( $post_id ) ) { return; } // Check if it's a hidden product. if ( 'product' === $post->post_type && Helper::is_woocommerce_active() ) { $product = wc_get_product( $post_id ); if ( $product && ! $product->is_visible() ) { return; } } $url = get_permalink( $post ); if ( 'trash' === $post->post_status ) { $url = $this->previous_post_permalinks[ $post_id ]; } /** * Filter the URL to be submitted to IndexNow. * Returning false will prevent the URL from being submitted. * * @param string $url URL to be submitted. * @param WP_POST $post Post object. */ $send_url = $this->do_filter( 'instant_indexing/publish_url', $url, $post ); // Early exit if filter is set to false. if ( ! $send_url ) { return; } $this->api_submit( $send_url, false ); $this->submitted[] = $post_id; } /** * Is module configured. * * @return boolean */ private function is_configured() { return (bool) Helper::get_settings( 'instant_indexing.indexnow_api_key' ); } /** * Enqueue CSS & JS. * * @param string $hook Page hook name. * @return void */ public function enqueue( $hook ) { if ( 'rank-math_page_rank-math-options-instant-indexing' !== $hook && 'rank-math_page_instant-indexing' !== $hook ) { return; } $uri = untrailingslashit( plugin_dir_url( __FILE__ ) ); wp_enqueue_script( 'rank-math-instant-indexing', $uri . '/assets/js/instant-indexing.js', [ 'jquery' ], rank_math()->version, true ); Helper::add_json( 'indexNow', [ 'restUrl' => rest_url( \RankMath\Rest\Rest_Helper::BASE . '/in' ), 'refreshHistoryInterval' => 30000, 'i18n' => [ 'submitError' => esc_html__( 'An error occurred while submitting the URL.', 'rank-math' ), 'clearHistoryError' => esc_html__( 'Error: could not clear history.', 'rank-math' ), 'getHistoryError' => esc_html__( 'Error: could not get history.', 'rank-math' ), 'noHistory' => esc_html__( 'No submissions yet.', 'rank-math' ), ], ] ); } /** * Serve API key for search engines. */ public function serve_api_key() { global $wp; $api = Api::get(); $key = $api->get_key(); $key_location = $api->get_key_location( 'serve_api_key' ); $current_url = home_url( $wp->request ); if ( isset( $current_url ) && $key_location === $current_url ) { header( 'Content-Type: text/plain' ); header( 'X-Robots-Tag: noindex' ); status_header( 200 ); echo esc_html( $key ); exit(); } } /** * Submit URL to IndexNow API. * * @param string $url URL to be submitted. * @param bool $is_manual_submission Whether the URL is submitted manually by the user. * * @return bool */ private function api_submit( $url, $is_manual_submission ) { $api = Api::get(); /** * Filter the URL to be submitted to IndexNow. * Returning false will prevent the URL from being submitted. * * @param bool $is_manual_submission Whether the URL is submitted manually by the user. */ $url = $this->do_filter( 'instant_indexing/submit_url', $url, $is_manual_submission ); if ( ! $url ) { return false; } $api_logs = $api->get_log(); if ( ! $is_manual_submission && ! empty( $api_logs ) ) { $logs = array_values( array_reverse( $api_logs ) ); if ( ! empty( $logs[0] ) && $logs[0]['url'] === $url && time() - $logs[0]['time'] < self::THROTTLE_LIMIT ) { return false; } } $submitted = $api->submit( $url, $is_manual_submission ); if ( ! $is_manual_submission ) { return $submitted; } $count = is_array( $url ) ? count( $url ) : 1; $this->add_submit_message_notice( $submitted, $count ); return $submitted; } /** * Add notice after submitting one or more URLs. * * @param bool $success Whether the submission was successful. * @param int $count Number of submitted URLs. * * @return void */ private function add_submit_message_notice( $success, $count ) { $notification_type = 'error'; $notification_message = __( 'Error submitting page to IndexNow.', 'rank-math' ); if ( $success ) { $notification_type = 'success'; $notification_message = sprintf( /* translators: %s: Number of pages submitted. */ _n( '%s page submitted to IndexNow.', '%s pages submitted to IndexNow.', $count, 'rank-math' ), $count ); } Helper::add_notification( $notification_message, [ 'type' => $notification_type ] ); } /** * Get post types where auto-submit is enabled. * * @return array */ private function get_auto_submit_post_types() { $post_types = Helper::get_settings( 'instant_indexing.bing_post_types', [] ); return $post_types; } }