*/
public static function defaults(): array {
return array(
'verify_google' => '',
'verify_bing' => '',
'verify_pinterest' => '',
'verify_yandex' => '',
'verify_baidu' => '',
'verify_naver' => '',
'verify_facebook' => '',
'indexing_auto' => false,
'indexing_types' => array( 'post', 'page' ),
);
}
public function register( Container $container ): void {
$container->share( Services\IndexingApi::class, static fn() => new Services\IndexingApi() );
}
public function boot( Container $container ): void {
add_action( 'rest_api_init', array( $this, 'routes' ) );
Guard::action( 'wp_head', array( $this, 'verification_tags' ), 2 );
add_action( 'transition_post_status', array( $this, 'auto_submit' ), 10, 3 );
}
/**
* Output search engine verification meta tags on the front page only —
* that is all any of these services read.
*/
public function verification_tags(): void {
if ( ! is_front_page() && ! is_home() ) {
return;
}
$settings = $this->settings();
$map = array(
'verify_google' => 'google-site-verification',
'verify_bing' => 'msvalidate.01',
'verify_pinterest' => 'p:domain_verify',
'verify_yandex' => 'yandex-verification',
// Baidu and Naver matter wherever Google is not the default engine,
// and both feed AI answers in their own markets.
'verify_baidu' => 'baidu-site-verification',
'verify_naver' => 'naver-site-verification',
'verify_facebook' => 'facebook-domain-verification',
);
foreach ( $map as $key => $name ) {
$value = trim( (string) ( $settings[ $key ] ?? '' ) );
if ( '' === $value ) {
continue;
}
printf( '' . "\n", esc_attr( $name ), esc_attr( $value ) );
}
}
/**
* Submit a URL to Google when a post is published or updated.
*
* @param string $new_status New status.
* @param string $old_status Old status.
* @param WP_Post $post The post.
*/
public function auto_submit( string $new_status, string $old_status, $post ): void {
if ( ! $post instanceof WP_Post ) {
return;
}
$settings = $this->settings();
if ( empty( $settings['indexing_auto'] ) ) {
return;
}
if ( ! in_array( $post->post_type, (array) $settings['indexing_types'], true ) ) {
return;
}
if ( wp_is_post_revision( $post ) || wp_is_post_autosave( $post ) ) {
return;
}
$api = \AiRankSuite\Kernel::service( Services\IndexingApi::class );
if ( ! $api->is_configured() ) {
return;
}
if ( 'publish' === $new_status ) {
$api->publish( (string) get_permalink( $post ), 'URL_UPDATED' );
return;
}
if ( 'publish' === $old_status && in_array( $new_status, array( 'trash', 'draft', 'private' ), true ) ) {
$api->publish( (string) get_permalink( $post ), 'URL_DELETED' );
}
}
public function routes(): void {
register_rest_route(
self::NAMESPACE,
'/settings/google',
array(
array(
'methods' => 'GET',
'callback' => array( $this, 'get_settings' ),
'permission_callback' => static fn(): bool => current_user_can( 'manage_options' ),
),
array(
'methods' => 'PUT',
'callback' => array( $this, 'update_settings' ),
'permission_callback' => static fn(): bool => current_user_can( 'manage_options' ),
'args' => array(
'verify_google' => array(
'type' => 'string',
'sanitize_callback' => 'sanitize_text_field',
),
'verify_bing' => array(
'type' => 'string',
'sanitize_callback' => 'sanitize_text_field',
),
'verify_pinterest' => array(
'type' => 'string',
'sanitize_callback' => 'sanitize_text_field',
),
'verify_yandex' => array(
'type' => 'string',
'sanitize_callback' => 'sanitize_text_field',
),
'verify_baidu' => array(
'type' => 'string',
'sanitize_callback' => 'sanitize_text_field',
),
'verify_naver' => array(
'type' => 'string',
'sanitize_callback' => 'sanitize_text_field',
),
'verify_facebook' => array(
'type' => 'string',
'sanitize_callback' => 'sanitize_text_field',
),
'indexing_auto' => array( 'type' => 'boolean' ),
'indexing_types' => array( 'type' => 'array' ),
),
),
)
);
register_rest_route(
self::NAMESPACE,
'/google/indexing/account',
array(
array(
'methods' => 'POST',
'callback' => array( $this, 'save_account' ),
'permission_callback' => static fn(): bool => current_user_can( 'manage_options' ),
'args' => array(
'json' => array(
'type' => 'string',
'required' => true,
),
),
),
array(
'methods' => 'DELETE',
'callback' => array( $this, 'clear_account' ),
'permission_callback' => static fn(): bool => current_user_can( 'manage_options' ),
),
)
);
register_rest_route(
self::NAMESPACE,
'/google/rich-results',
array(
'methods' => 'GET',
'permission_callback' => static fn(): bool => current_user_can( 'manage_options' ),
'callback' => static fn( WP_REST_Request $request ) => new WP_REST_Response(
Services\RichResults::scan(
max( 20, min( 500, (int) $request->get_param( 'limit' ) ) ),
(bool) $request->get_param( 'fresh' )
)
),
'args' => array(
'limit' => array( 'type' => 'integer', 'default' => 100 ),
'fresh' => array( 'type' => 'boolean', 'default' => false ),
),
)
);
register_rest_route(
self::NAMESPACE,
'/google/indexing/submit',
array(
'methods' => 'POST',
'callback' => array( $this, 'submit' ),
'permission_callback' => static function () {
if ( ! current_user_can( 'manage_options' ) ) {
return self::forbidden();
}
return self::spend( 'indexing', 60 );
},
'args' => array(
'post_id' => array( 'type' => 'integer' ),
'url' => array(
'type' => 'string',
'sanitize_callback' => 'esc_url_raw',
),
'type' => array(
'type' => 'string',
'default' => 'URL_UPDATED',
'enum' => array( 'URL_UPDATED', 'URL_DELETED' ),
),
),
)
);
}
public function get_settings(): WP_REST_Response {
$api = \AiRankSuite\Kernel::service( Services\IndexingApi::class );
return new WP_REST_Response(
array(
'settings' => $this->settings(),
'indexing' => array(
'configured' => $api->is_configured(),
'email' => $api->client_email(),
'used_today' => $api->used_today(),
'quota' => $api->quota(),
'recent' => $api->recent(),
),
/*
* Slug and label. The screen used to print the slug, so a site
* with custom types offered "unterkunftsarten" and "sso_places"
* as things to tick — the reader has to translate back to what
* they see in the menu. WordPress already knows the name.
*/
'post_types' => array_values(
array_map(
static function ( string $slug ): array {
$object = get_post_type_object( $slug );
return array(
'slug' => $slug,
'label' => ( $object && ! empty( $object->labels->name ) ) ? (string) $object->labels->name : $slug,
);
},
array_diff( get_post_types( array( 'public' => true ) ), array( 'attachment' ) )
)
),
)
);
}
public function update_settings( WP_REST_Request $request ): WP_REST_Response {
$defaults = self::defaults();
$values = array();
foreach ( array_keys( $defaults ) as $key ) {
if ( ! $request->has_param( $key ) ) {
continue;
}
if ( 'indexing_types' === $key ) {
$public = get_post_types( array( 'public' => true ) );
$values[ $key ] = array_values( array_intersect( (array) $request->get_param( $key ), $public ) );
continue;
}
$values[ $key ] = $request->get_param( $key );
}
$this->options()->set( 'google', $values, $defaults );
return $this->get_settings();
}
public function save_account( WP_REST_Request $request ): WP_REST_Response|WP_Error {
$api = \AiRankSuite\Kernel::service( Services\IndexingApi::class );
$result = $api->save_account( (string) $request->get_param( 'json' ) );
return $result instanceof WP_Error ? $result : $this->get_settings();
}
public function clear_account(): WP_REST_Response {
\AiRankSuite\Kernel::service( Services\IndexingApi::class )->clear_account();
return $this->get_settings();
}
public function submit( WP_REST_Request $request ): WP_REST_Response|WP_Error {
$url = (string) $request->get_param( 'url' );
if ( '' === $url && $request->get_param( 'post_id' ) ) {
// The permalink of a post this person cannot read is not theirs to
// look up, let alone to announce to Google.
if ( ! self::post_allowed( (int) $request->get_param( 'post_id' ), 'read_post' ) ) {
return self::forbidden();
}
$url = (string) get_permalink( (int) $request->get_param( 'post_id' ) );
}
if ( '' === $url ) {
return new WP_Error( 'airanksuite_indexing_no_url', __( 'No URL to submit.', 'ai-rank-suite' ), array( 'status' => 400 ) );
}
$api = \AiRankSuite\Kernel::service( Services\IndexingApi::class );
$result = $api->publish( $url, (string) $request->get_param( 'type' ) );
return $result instanceof WP_Error ? $result : $this->get_settings();
}
/** @return array */
private function settings(): array {
return $this->options()->get( 'google', self::defaults() );
}
private function options(): Options {
return \AiRankSuite\Kernel::service( Options::class );
}
}