%s

', esc_html__( 'Deactivate and delete the old RankLantern Connector (version 1.2 or earlier) first. Your structured data is kept.', 'ranklantern' ) ); } /** * REST route and fields on posts and pages. */ function ranklantern_connector_register_rest() { // Lets RankLantern detect the plugin and its version. register_rest_route( 'rankforge/v1', '/ping', array( 'methods' => 'GET', 'callback' => 'ranklantern_connector_ping', 'permission_callback' => 'ranklantern_connector_can_ping', ) ); foreach ( array( 'post', 'page' ) as $type ) { // Extra JSON-LD blocks (Event, FAQPage) printed in ; stored as post meta, so page content is untouched. register_rest_field( $type, 'rankforge_schema', array( 'get_callback' => 'ranklantern_connector_get_schema', 'update_callback' => 'ranklantern_connector_save_schema', 'schema' => array( 'type' => 'array', 'items' => array( 'type' => 'object' ), 'context' => array( 'view', 'edit' ), ), ) ); // Whether this post was opened in Elementor: RankLantern then never puts it live by itself. Read only. register_rest_field( $type, 'rankforge_builder', array( 'get_callback' => 'ranklantern_connector_get_builder', 'update_callback' => null, 'schema' => array( 'type' => 'boolean', 'context' => array( 'edit' ), 'readonly' => true, ), ) ); // Write only: the values end up in the SEO plugin's own fields. register_rest_field( $type, 'rankforge_seo', array( 'get_callback' => null, 'update_callback' => 'ranklantern_connector_save_seo', 'schema' => array( 'type' => 'object', 'context' => array( 'edit' ), 'properties' => array( 'title' => array( 'type' => 'string' ), 'description' => array( 'type' => 'string' ), 'keyword' => array( 'type' => 'string' ), ), ), ) ); } } /** * Ping is for logged-in editors only. * * @return true|WP_Error */ function ranklantern_connector_can_ping() { return current_user_can( 'edit_posts' ) ? true : ranklantern_connector_forbidden(); } /** * Ping response. * * @return array */ function ranklantern_connector_ping() { return array( 'ok' => true, 'version' => RANKLANTERN_CONNECTOR_VERSION, ); } /** * The error for a user who may not do this: 401 when not logged in, 403 when logged in without the right. * * @return WP_Error */ function ranklantern_connector_forbidden() { return new WP_Error( 'rest_forbidden', __( 'Sorry, you are not allowed to do that.', 'ranklantern' ), array( 'status' => rest_authorization_required_code() ) ); } /** * Stored JSON-LD blocks of a post. * * @param array $post Post data from the REST controller. * @return array */ function ranklantern_connector_get_schema( $post ) { $items = json_decode( (string) get_post_meta( (int) $post['id'], '_rankforge_schema', true ), true ); return is_array( $items ) ? $items : array(); } /** * Whether a post was opened in Elementor (its edit mode, or Elementor's own data on it). * * @param array $post Post data from the REST controller. * @return bool */ function ranklantern_connector_get_builder( $post ) { $id = (int) $post['id']; return 'builder' === get_post_meta( $id, '_elementor_edit_mode', true ) || ! empty( get_post_meta( $id, '_elementor_data', true ) ); } /** * Writes the SEO title, description and focus keyword into whichever SEO plugins are active. * * @param mixed $value Field value from the request. * @param WP_Post $post The post being saved. * @return true|WP_Error */ function ranklantern_connector_save_seo( $value, $post ) { if ( ! current_user_can( 'edit_post', $post->ID ) ) { return ranklantern_connector_forbidden(); } if ( ! is_array( $value ) ) { return true; } $id = (int) $post->ID; $title = isset( $value['title'] ) ? sanitize_text_field( (string) $value['title'] ) : ''; $desc = isset( $value['description'] ) ? sanitize_textarea_field( (string) $value['description'] ) : ''; $kw = isset( $value['keyword'] ) ? sanitize_text_field( (string) $value['keyword'] ) : ''; // Meta keys per SEO plugin: title, description, focus keyword. $plugins = array(); if ( defined( 'WPSEO_VERSION' ) ) { $plugins[] = array( '_yoast_wpseo_title', '_yoast_wpseo_metadesc', '_yoast_wpseo_focuskw' ); } if ( class_exists( 'RankMath' ) ) { $plugins[] = array( 'rank_math_title', 'rank_math_description', 'rank_math_focus_keyword' ); } if ( defined( 'SEOPRESS_VERSION' ) ) { $plugins[] = array( '_seopress_titles_title', '_seopress_titles_desc', '_seopress_analysis_target_kw' ); } foreach ( $plugins as $keys ) { foreach ( array( $title, $desc, $kw ) as $i => $v ) { // An empty value never clears what the site owner set. if ( '' !== $v ) { update_post_meta( $id, $keys[ $i ], wp_slash( $v ) ); } } } // All in One SEO keeps its data in its own table; only its own model writes there. if ( function_exists( 'aioseo' ) && class_exists( '\AIOSEO\Plugin\Common\Models\Post' ) ) { try { $model = \AIOSEO\Plugin\Common\Models\Post::getPost( $id ); $model->post_id = $id; if ( '' !== $title ) { $model->title = $title; } if ( '' !== $desc ) { $model->description = $desc; } if ( '' !== $kw ) { $model->keyphrases = wp_json_encode( array( 'focus' => array( 'keyphrase' => $kw ), 'additional' => array(), ) ); } $model->save(); } catch ( \Throwable $e ) { // A changed AIOSEO version: the post itself is saved, only these fields are skipped. unset( $e ); } } return true; } /** * Replaces the JSON-LD blocks of a post. An empty list removes them. * * @param mixed $value Field value from the request. * @param WP_Post $post The post being saved. * @return true|WP_Error */ function ranklantern_connector_save_schema( $value, $post ) { if ( ! current_user_can( 'edit_post', $post->ID ) ) { return ranklantern_connector_forbidden(); } if ( ! is_array( $value ) || ! $value ) { delete_post_meta( $post->ID, '_rankforge_schema' ); return true; } if ( count( $value ) > RANKLANTERN_CONNECTOR_SCHEMA_MAX_ITEMS ) { return ranklantern_connector_too_large(); } $clean = array(); foreach ( $value as $item ) { // Only schema.org objects with a type. if ( is_array( $item ) && isset( $item['@type'] ) ) { $item = ranklantern_connector_clean_schema( $item, 0 ); if ( null !== $item ) { $clean[] = $item; } } } $json = wp_json_encode( $clean ); if ( false === $json || strlen( $json ) > RANKLANTERN_CONNECTOR_SCHEMA_MAX_BYTES ) { return ranklantern_connector_too_large(); } update_post_meta( $post->ID, '_rankforge_schema', wp_slash( $json ) ); return true; } /** * The error for JSON-LD over the limits. * * @return WP_Error */ function ranklantern_connector_too_large() { return new WP_Error( 'rest_invalid_param', __( 'The structured data is too large.', 'ranklantern' ), array( 'status' => 400 ) ); } /** * Sanitizes a JSON-LD value: plain keys, text without scripts, numbers and booleans. Null when nested too deep. * * @param mixed $value A JSON-LD value. * @param int $depth Current nesting depth. * @return mixed */ function ranklantern_connector_clean_schema( $value, $depth ) { if ( is_array( $value ) ) { if ( $depth >= RANKLANTERN_CONNECTOR_SCHEMA_MAX_DEPTH ) { return null; } $out = array(); foreach ( $value as $key => $v ) { $v = ranklantern_connector_clean_schema( $v, $depth + 1 ); if ( null === $v ) { continue; } if ( is_int( $key ) ) { $out[] = $v; } elseif ( preg_match( '/^@?[A-Za-z][A-Za-z0-9_:-]{0,63}$/', $key ) ) { $out[ $key ] = $v; } } return $out; } if ( is_string( $value ) ) { $value = wp_check_invalid_utf8( $value ); // FAQ answers may contain simple HTML: what a post may contain is allowed. Plain text (URLs, dates) stays // exactly as sent, since kses would turn & into &. return false === strpos( $value, '<' ) ? $value : wp_kses_post( $value ); } if ( is_int( $value ) || is_float( $value ) || is_bool( $value ) ) { return $value; } return null; } /** * Prints the JSON-LD blocks of the current post or page in . */ function ranklantern_connector_print_schema() { if ( ! is_singular() ) { return; } $items = json_decode( (string) get_post_meta( get_queried_object_id(), '_rankforge_schema', true ), true ); if ( ! is_array( $items ) ) { return; } foreach ( $items as $item ) { if ( ! is_array( $item ) ) { continue; } if ( ! isset( $item['@context'] ) ) { $item = array( '@context' => 'https://schema.org' ) + $item; } // JSON_HEX_TAG turns < and > into < and >, so no text can close the script tag. $json = wp_json_encode( $item, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE | JSON_HEX_TAG ); if ( false !== $json ) { wp_print_inline_script_tag( $json, array( 'type' => 'application/ld+json', 'class' => 'rankforge-schema', ) ); } } }