476 lines
		
	
	
		
			13 KiB
		
	
	
	
		
			PHP
		
	
	
	
	
	
			
		
		
	
	
			476 lines
		
	
	
		
			13 KiB
		
	
	
	
		
			PHP
		
	
	
	
	
	
<?php
 | 
						|
/**
 | 
						|
 * Instant Indexing module.
 | 
						|
 *
 | 
						|
 * @since      1.0.56
 | 
						|
 * @package    RankMath
 | 
						|
 * @author     Rank Math <support@rankmath.com>
 | 
						|
 */
 | 
						|
 | 
						|
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'] = '<a href="' . esc_url( $link ) . '" class="rm-instant-indexing-action rm-indexnow-submit">' . __( 'Instant Indexing: Submit Page', 'rank-math' ) . '</a>';
 | 
						|
 | 
						|
		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' ) . ' <a href="' . KB::get( 'instant-indexing', 'Indexing Submit URLs' ) . '" target="_blank">' . esc_html__( 'Learn more', 'rank-math' ) . '</a>',
 | 
						|
				'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' ), '<a href="' . KB::get( 'instant-indexing', 'Indexing Settings' ) . '" target="_blank">' . esc_html__( 'Learn more', 'rank-math' ) . '</a>' ),
 | 
						|
				'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;
 | 
						|
	}
 | 
						|
 | 
						|
}
 |