From 9e074ae22a07a4037f3fdb303ef6b4dbe12947ec Mon Sep 17 00:00:00 2001 From: Henrique Mouta Date: Thu, 22 Aug 2024 14:23:59 +0100 Subject: [PATCH 01/49] Add new Rest API base classes --- src/rest-api/class-base-api-controller.php | 149 ++++++++++ src/rest-api/class-base-endpoint.php | 305 +++++++++++++++++++++ src/rest-api/class-rest-api-controller.php | 68 +++++ 3 files changed, 522 insertions(+) create mode 100644 src/rest-api/class-base-api-controller.php create mode 100644 src/rest-api/class-base-endpoint.php create mode 100644 src/rest-api/class-rest-api-controller.php diff --git a/src/rest-api/class-base-api-controller.php b/src/rest-api/class-base-api-controller.php new file mode 100644 index 0000000000..8e6693ea5f --- /dev/null +++ b/src/rest-api/class-base-api-controller.php @@ -0,0 +1,149 @@ +parsely = $parsely; + $this->endpoints = array(); + + if ( static::NAMESPACE === false ) { + throw new \UnexpectedValueException( 'The API controller must define a namespace.' ); + } + } + + /** + * Initialize the API controller. + * + * This method should be overridden by child classes and used to register + * endpoints. + * + * @return void + */ + abstract protected function init(): void; + + /** + * Register a single endpoint. + * + * @since 3.17.0 + * + * @param Base_Endpoint $endpoint The endpoint to register. + */ + protected function register_endpoint( Base_Endpoint $endpoint ): void { + $this->endpoints[] = $endpoint; + $endpoint->init(); + } + + /** + * Register multiple endpoints. + * + * @since 3.17.0 + * + * @param Base_Endpoint[] $endpoints The endpoints to register. + */ + protected function register_endpoints( array $endpoints ): void { + foreach ( $endpoints as $endpoint ) { + $this->register_endpoint( $endpoint ); + } + } + + /** + * Returns the namespace for the API. + * + * If a version is defined, it will be appended to the namespace. + * + * @since 3.17.0 + * @return string + */ + public function get_namespace(): string { + $namespace = static::NAMESPACE; + + if ( false === $namespace ) { + return ''; + } + + if ( '' !== static::VERSION ) { + $namespace .= '/' . static::VERSION; + } + + return $namespace; + } + + /** + * Prefix a route with the route prefix. + * + * @since 3.17.0 + * + * @param string $route The route to prefix. + * @return string The prefixed route. + */ + public function prefix_route( string $route ): string { + if ( '' === static::ROUTE_PREFIX ) { + return $route; + } + + return static::ROUTE_PREFIX . '/' . $route; + } +} diff --git a/src/rest-api/class-base-endpoint.php b/src/rest-api/class-base-endpoint.php new file mode 100644 index 0000000000..e5cacc96fc --- /dev/null +++ b/src/rest-api/class-base-endpoint.php @@ -0,0 +1,305 @@ + + */ + protected $registered_routes = array(); + + /** + * Constructor. + * + * @since 3.17.0 + * + * @param Base_API_Controller $controller The REST API controller. + */ + public function __construct( Base_API_Controller $controller ) { + $this->api_controller = $controller; + $this->parsely = $controller->parsely; + } + + /** + * Initialize the API endpoint, by registering the routes. + * + * Allows for the endpoint to be disabled via the + * `wp_parsely_api_{endpoint}_endpoint_enabled` filter. + * + * @since 3.17.0 + * + * @throws UnexpectedValueException If the ENDPOINT constant is not defined. + */ + public function init(): void { + if ( false === static::ENDPOINT ) { + throw new UnexpectedValueException( 'ENDPOINT constant must be defined in child class.' ); + } + + /** + * Filter to enable/disable the endpoint. + * + * @return bool + */ + $filter_name = 'wp_parsely_api_' . + Utils::convert_endpoint_to_filter_key( static::ENDPOINT ) . + '_endpoint_enabled'; + if ( ! apply_filters( $filter_name, true ) ) { // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.DynamicHooknameFound + return; + } + + // Register the routes. + add_action( 'rest_api_init', array( $this, 'register_routes' ) ); + } + + /** + * Registers the routes for the endpoint. + * + * This method should be overridden by child classes and used to register + * the routes for the endpoint. + * + * @since 3.17.0 + */ + abstract public function register_routes(): void; + + /** + * Registers a REST route. + * + * @param string $route The route to register. + * @param string[] $methods Array with the allowed methods. + * @param callable $callback Callback function to call when the endpoint is hit. + * @param array $args The endpoint arguments definition. + * + * @return void + * @since 3.17.0 + */ + public function register_rest_route( string $route, array $methods, callable $callback, array $args = array() ): void { + // Trim any possible slashes from the route + // . + $route = trim( $route, '/' ); + // Store the route for later reference. + $this->registered_routes[] = $route; + + // Create the full route for the endpoint. + $route = $this->get_endpoint() . '/' . $route; + + // Register the route. + register_rest_route( + $this->api_controller->get_namespace(), + $this->api_controller->prefix_route( $route ), + array( + array( + 'methods' => $methods, + 'callback' => $callback, + 'permission_callback' => array( $this, 'is_available_to_current_user' ), + 'args' => $args, + 'show_in_index' => ! is_wp_error( $this->is_available_to_current_user() ), + ), + ) + ); + } + + /** + * Returns the endpoint name. + * + * @since 3.17.0 + * + * @return string + */ + public function get_endpoint(): string { + if ( false === static::ENDPOINT ) { + return ''; + } + return static::ENDPOINT; + } + + + /** + * Returns the full endpoint path for a given route. + * + * @since 3.17.0 + * + * @param string $route The route. + * @return string + */ + public function get_full_endpoint( string $route = '' ): string { + $route = $this->get_endpoint() . '/' . $route; + + return '/' . + $this->api_controller->get_namespace() . + '/' . + $this->api_controller->prefix_route( $route ); + } + + /** + * Returns the registered routes. + * + * @since 3.17.0 + * + * @return array + */ + public function get_registered_routes(): array { + return $this->registered_routes; + } + + /** + * Returns whether the endpoint is available for access by the current + * user. + * + * @since 3.14.0 Replaced `is_public_endpoint`, `user_capability` and `permission_callback()`. + * @since 3.16.0 Added the `$request` parameter. + * @since 3.17.0 Moved to the new API structure. + * + * @param WP_REST_Request|null $request The request object. + * @return WP_Error|bool True if the endpoint is available. + */ + public function is_available_to_current_user( ?WP_REST_Request $request = null ) { // phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter.Found + + // Validate the API key and secret. + $api_key_validation = $this->validate_apikey_and_secret(); + if ( is_wp_error( $api_key_validation ) ) { + return $api_key_validation; + } + + // Validate the user capability. + $capability = static::DEFAULT_ACCESS_CAPABILITY; + return current_user_can( + // phpcs:ignore WordPress.WP.Capabilities.Undetermined + $this->apply_capability_filters( $capability ) + ); + } + + /** + * Returns the user capability allowing access to the endpoint, after having + * applied capability filters. + * + * `DEFAULT_ACCESS_CAPABILITY` is not passed here by default, to allow for + * a more explicit declaration in child classes. + * + * @since 3.14.0 + * @since 3.17.0 Moved to the new API structure. + * + * @param string $capability The original capability allowing access. + * @return string The capability allowing access after applying the filters. + * @throws UnexpectedValueException If the ENDPOINT constant is not defined. + */ + public function apply_capability_filters( string $capability ): string { + /** + * Filter to change the default user capability for all private endpoints. + * + * @var string + */ + $default_user_capability = apply_filters( + 'wp_parsely_user_capability_for_all_private_apis', + $capability + ); + + if ( false === static::ENDPOINT ) { + throw new UnexpectedValueException( 'ENDPOINT constant must be defined in child class.' ); + } + + /** + * Filter to change the user capability for the specific endpoint. + * + * @var string + */ + $endpoint_specific_user_capability = apply_filters( + 'wp_parsely_user_capability_for_' . + Utils::convert_endpoint_to_filter_key( static::ENDPOINT ) . + '_api', + $default_user_capability + ); + + return $endpoint_specific_user_capability; + } + + /** + * Validates that the Site ID and secret are set. + * If the API secret is not required, it will not be validated. + * + * @since 3.13.0 + * + * @param bool $require_api_secret Specifies if the API Secret is required. + * @return WP_Error|bool + */ + public function validate_apikey_and_secret( bool $require_api_secret = true ) { + if ( false === $this->parsely->site_id_is_set() ) { + return new WP_Error( + 'parsely_site_id_not_set', + __( 'A Parse.ly Site ID must be set in site options to use this endpoint', 'wp-parsely' ), + array( 'status' => 403 ) + ); + } + + if ( $require_api_secret && false === $this->parsely->api_secret_is_set() ) { + return new WP_Error( + 'parsely_api_secret_not_set', + __( 'A Parse.ly API Secret must be set in site options to use this endpoint', 'wp-parsely' ), + array( 'status' => 403 ) + ); + } + + return true; + } +} diff --git a/src/rest-api/class-rest-api-controller.php b/src/rest-api/class-rest-api-controller.php new file mode 100644 index 0000000000..b47850f421 --- /dev/null +++ b/src/rest-api/class-rest-api-controller.php @@ -0,0 +1,68 @@ +parsely ), + ); + + // Initialize the controllers. + foreach ( $controllers as $controller ) { + $controller->init(); + } + + $this->controllers = $controllers; + } +} From 6ad9d44df95871395dc140a513ca5d25ccc5c78c Mon Sep 17 00:00:00 2001 From: Henrique Mouta Date: Thu, 22 Aug 2024 14:24:58 +0100 Subject: [PATCH 02/49] Implement the Content Helper API namespace --- src/Models/class-smart-link.php | 4 +- .../class-content-helper-controller.php | 47 ++ .../class-endpoint-excerpt-generator.php | 162 ++++ .../class-endpoint-smart-linking.php | 713 ++++++++++++++++++ .../class-endpoint-title-suggestions.php | 160 ++++ .../trait-content-helper-feature.php | 69 ++ wp-parsely.php | 6 + 7 files changed, 1160 insertions(+), 1 deletion(-) create mode 100644 src/rest-api/content-helper/class-content-helper-controller.php create mode 100644 src/rest-api/content-helper/class-endpoint-excerpt-generator.php create mode 100644 src/rest-api/content-helper/class-endpoint-smart-linking.php create mode 100644 src/rest-api/content-helper/class-endpoint-title-suggestions.php create mode 100644 src/rest-api/content-helper/trait-content-helper-feature.php diff --git a/src/Models/class-smart-link.php b/src/Models/class-smart-link.php index 009998a633..469a00bee6 100644 --- a/src/Models/class-smart-link.php +++ b/src/Models/class-smart-link.php @@ -127,7 +127,9 @@ public function __construct( int $offset, int $post_id = 0 ) { - $this->set_href( $href ); + if ( '' !== $href ) { + $this->set_href( $href ); + } // Set the title to be the destination post title if the destination post ID is set. if ( 0 !== $this->destination_post_id ) { diff --git a/src/rest-api/content-helper/class-content-helper-controller.php b/src/rest-api/content-helper/class-content-helper-controller.php new file mode 100644 index 0000000000..5e58f9a0eb --- /dev/null +++ b/src/rest-api/content-helper/class-content-helper-controller.php @@ -0,0 +1,47 @@ +register_endpoints( $endpoints ); + } +} diff --git a/src/rest-api/content-helper/class-endpoint-excerpt-generator.php b/src/rest-api/content-helper/class-endpoint-excerpt-generator.php new file mode 100644 index 0000000000..f2fbaeb2a5 --- /dev/null +++ b/src/rest-api/content-helper/class-endpoint-excerpt-generator.php @@ -0,0 +1,162 @@ +suggest_brief_api = new Suggest_Brief_API( $this->parsely ); + } + + /** + * Returns the name of the feature associated with the current endpoint. + * + * @since 3.17.0 + * + * @return string The feature name. + */ + public function get_pch_feature_name(): string { + return 'excerpt_suggestions'; + } + + /** + * Registers the routes for the endpoint. + * + * This method should be overridden by child classes and used to register + * the routes for the endpoint. + * + * @since 3.17.0 + */ + public function register_routes(): void { + /** + * POST /excerpt-generator/generate + * Generates an excerpt for the given content. + */ + $this->register_rest_route( + 'generate', + array( 'POST' ), + array( $this, 'generate_excerpt' ), + array( + 'content' => array( + 'description' => __( 'The text to generate the excerpt from.', 'wp-parsely' ), + 'type' => 'string', + 'required' => true, + ), + 'title' => array( + 'description' => __( 'The title of the content.', 'wp-parsely' ), + 'type' => 'string', + 'required' => true, + ), + 'persona' => array( + 'description' => __( 'The persona of the content.', 'wp-parsely' ), + 'type' => 'string', + 'required' => false, + 'default' => 'journalist', + ), + 'style' => array( + 'description' => __( 'The style of the content.', 'wp-parsely' ), + 'type' => 'string', + 'required' => false, + 'default' => 'neutral', + ), + ) + ); + } + + /** + * API Endpoint: POST /excerpt-generator/generate + * + * Generates an excerpt for the passed content. + * + * @since 3.17.0 + * + * @param WP_REST_Request $request The request object. + * @return WP_REST_Response|WP_Error The response object. + */ + public function generate_excerpt( WP_REST_Request $request ) { + /** + * The post content to be sent to the API. + * + * @var string $post_content + */ + $post_content = $request->get_param( 'content' ); + + /** + * The post title to be sent to the API. + * + * @var string $post_title + */ + $post_title = $request->get_param( 'title' ); + + /** + * The persona to be sent to the API. + * + * @var string $persona + */ + $persona = $request->get_param( 'persona' ); + + /** + * The style to be sent to the API. + * + * @var string $style + */ + $style = $request->get_param( 'style' ); + + $response = $this->suggest_brief_api->get_suggestion( $post_title, $post_content, $persona, $style ); + + if ( is_wp_error( $response ) ) { + return $response; + } + + return new WP_REST_Response( array( 'data' => $response ), 200 ); + } +} diff --git a/src/rest-api/content-helper/class-endpoint-smart-linking.php b/src/rest-api/content-helper/class-endpoint-smart-linking.php new file mode 100644 index 0000000000..902bc8d476 --- /dev/null +++ b/src/rest-api/content-helper/class-endpoint-smart-linking.php @@ -0,0 +1,713 @@ +suggest_linked_reference_api = new Suggest_Linked_Reference_API( $this->parsely ); + } + + /** + * Returns the name of the feature associated with the current endpoint. + * + * @since 3.17.0 + * + * @return string The feature name. + */ + public function get_pch_feature_name(): string { + return 'smart_linking'; + } + + /** + * Registers the routes for the endpoint. + * + * @since 3.17.0 + */ + public function register_routes(): void { + /** + * GET /smart-linking/generate + * Generates smart links for a post. + */ + $this->register_rest_route( + 'generate', + array( 'POST' ), + array( $this, 'generate_smart_links' ), + array( + 'content' => array( + 'required' => true, + 'type' => 'string', + 'description' => __( 'The text to generate smart links for.', 'wp-parsely' ), + ), + 'max_links' => array( + 'type' => 'integer', + 'description' => __( 'The maximum number of smart links to generate.', 'wp-parsely' ), + 'default' => 10, + ), + 'url_exclusion_list' => array( + 'type' => 'array', + 'description' => __( 'The list of URLs to exclude from the smart links.', 'wp-parsely' ), + 'validate_callback' => array( $this, 'validate_url_exclusion_list' ), + ), + ) + ); + + + /** + * GET /smart-linking/{post_id}/get + * Gets the smart links for a post. + */ + $this->register_rest_route( + '(?P\d+)/get', + array( 'GET' ), + array( $this, 'get_smart_links' ), + array( + 'post_id' => array( + 'required' => true, + 'description' => __( 'The post ID.', 'wp-parsely' ), + 'validate_callback' => array( $this, 'validate_post_id' ), + ), + ) + ); + + /** + * POST /smart-linking/{post_id}/add + * Adds a smart link to a post. + */ + $this->register_rest_route( + '(?P\d+)/add', + array( 'POST' ), + array( $this, 'add_smart_link' ), + array( + 'post_id' => array( + 'required' => true, + 'description' => __( 'The post ID.', 'wp-parsely' ), + 'validate_callback' => array( $this, 'validate_post_id' ), + ), + 'link' => array( + 'required' => true, + 'type' => 'object', + 'description' => __( 'The smart link data to add.', 'wp-parsely' ), + 'validate_callback' => array( $this, 'validate_smart_link_params' ), + ), + 'update' => array( + 'type' => 'boolean', + 'description' => __( 'Whether to update the existing smart link.', 'wp-parsely' ), + 'default' => false, + ), + ) + ); + + /** + * POST /smart-linking/{post_id}/add-multiple + * Adds multiple smart links to a post. + */ + $this->register_rest_route( + '(?P\d+)/add-multiple', + array( 'POST' ), + array( $this, 'add_multiple_smart_links' ), + array( + 'post_id' => array( + 'required' => true, + 'description' => __( 'The post ID.', 'wp-parsely' ), + 'validate_callback' => array( $this, 'validate_post_id' ), + ), + 'links' => array( + 'required' => true, + 'type' => 'array', + 'description' => __( 'The multiple smart links data to add.', 'wp-parsely' ), + 'validate_callback' => array( $this, 'validate_multiple_smart_links' ), + ), + 'update' => array( + 'type' => 'boolean', + 'description' => __( 'Whether to update the existing smart links.', 'wp-parsely' ), + 'default' => false, + ), + ) + ); + + /** + * POST /smart-linking/{post_id}/set + * Updates the smart links of a given post and removes the ones that are not in the request. + */ + $this->register_rest_route( + '(?P\d+)/set', + array( 'POST' ), + array( $this, 'set_smart_links' ), + array( + 'post_id' => array( + 'required' => true, + 'description' => __( 'The post ID.', 'wp-parsely' ), + 'validate_callback' => array( $this, 'validate_post_id' ), + ), + 'links' => array( + 'required' => true, + 'type' => 'array', + 'description' => __( 'The smart links data to set.', 'wp-parsely' ), + 'validate_callback' => array( $this, 'validate_multiple_smart_links' ), + ), + ) + ); + + /** + * POST /smart-linking/url-to-post-type + * Converts a URL to a post type. + */ + $this->register_rest_route( + 'url-to-post-type', + array( 'POST' ), + array( $this, 'url_to_post_type' ) + ); + } + + /** + * API Endpoint: GET /smart-linking/generate. + * + * Generates smart links for a post. + * + * @since 3.16.0 + * + * @param WP_REST_Request $request The request object. + * @return WP_REST_Response|WP_Error The response object. + */ + public function generate_smart_links( WP_REST_Request $request ) { + /** + * The text to generate smart links for. + * + * @var string $post_content + */ + $post_content = $request->get_param( 'content' ); + + /** + * The maximum number of smart links to generate. + * + * @var int $max_links + */ + $max_links = $request->get_param( 'max_links' ); + + /** + * The URL exclusion list. + * + * @var array $url_exclusion_list + */ + $url_exclusion_list = $request->get_param( 'url_exclusion_list' ) ?? array(); + + $response = $this->suggest_linked_reference_api->get_links( + $post_content, + 4, // TODO: will be removed after API refactoring. + $max_links, + $url_exclusion_list + ); + + if ( is_wp_error( $response ) ) { + return $response; + } + + $smart_links = array_map( + function ( Smart_Link $link ) { + return $link->to_array(); + }, + $response + ); + + return new WP_REST_Response( array( 'data' => $smart_links ), 200 ); + } + + /** + * API Endpoint: GET /smart-linking/{post_id}/get. + * + * Gets the smart links for a post. + * + * @since 3.16.0 + * + * @param WP_REST_Request $request The request object. + * @return WP_REST_Response The response object. + */ + public function get_smart_links( WP_REST_Request $request ): WP_REST_Response { + /** + * The post object. + * + * @var WP_Post $post + */ + $post = $request->get_param( 'post' ); + + $outbound_links = Smart_Link::get_outbound_smart_links( $post->ID ); + $inbound_links = Smart_Link::get_inbound_smart_links( $post->ID ); + + $response = array( + 'outbound' => $this->serialize_smart_links( $outbound_links ), + 'inbound' => $this->serialize_smart_links( $inbound_links ), + ); + + return new WP_REST_Response( array( 'data' => $response ), 200 ); + } + + + /** + * API Endpoint: POST /smart-linking/{post_id}/add. + * + * Adds a smart link to a post. + * If the update parameter is set to true, the existing smart link will be updated. + * + * @since 3.16.0 + * + * @param WP_REST_Request $request The request object. + * @return WP_REST_Response The response object. + */ + public function add_smart_link( WP_REST_Request $request ): WP_REST_Response { + /** + * The Smart Link model. + * + * @var Smart_Link $smart_link + */ + $smart_link = $request->get_param( 'smart_link' ); + $should_update = $request->get_param( 'update' ) === true; + + if ( $smart_link->exists() && ! $should_update ) { + return new WP_REST_Response( + array( + 'error' => array( + 'name' => 'smart_link_exists', + 'message' => __( 'Smart link already exists.', 'wp-parsely' ), + ), + ), + 409 // HTTP Conflict. + ); + } + + // The smart link properties are set in the validate callback. + $saved = $smart_link->save(); + if ( ! $saved ) { + return new WP_REST_Response( + array( + 'error' => array( + 'name' => 'add_smart_link_failed', + 'message' => __( 'Failed to add the smart link.', 'wp-parsely' ), + ), + ), + 500 + ); + } + + return new WP_REST_Response( + array( + 'data' => json_decode( $smart_link->serialize() ), + ), + 200 + ); + } + + /** + * API Endpoint: POST /smart-linking/{post_id}/add_multiple. + * + * Adds multiple smart links to a post. + * + * @since 3.16.0 + * + * @param WP_REST_Request $request The request object. + * @return WP_REST_Response The response object. + */ + public function add_multiple_smart_links( WP_REST_Request $request ): WP_REST_Response { + /** + * Array of Smart Link models. + * + * @var Smart_Link[] $smart_links + */ + $smart_links = $request->get_param( 'smart_links' ); + $should_update = $request->get_param( 'update' ) === true; + + $added_links = array(); + $updated_links = array(); + $failed_links = array(); + + foreach ( $smart_links as $smart_link ) { + if ( $smart_link->exists() && ! $should_update ) { + $failed_links[] = $smart_link; + continue; + } + + $updated_link = $smart_link->exists() && $should_update; + + // The smart link properties are set in the validate callback. + $saved = $smart_link->save(); + + if ( ! $saved ) { + $failed_links[] = $smart_link; + continue; + } + + if ( $updated_link ) { + $updated_links[] = $smart_link; + } else { + $added_links[] = $smart_link; + } + } + + // If no link was added, return an error response. + if ( count( $added_links ) === 0 && count( $updated_links ) === 0 ) { + return new WP_REST_Response( + array( + 'error' => array( + 'name' => 'add_smart_link_failed', + 'message' => __( 'Failed to add all the smart links.', 'wp-parsely' ), + ), + ), + 500 + ); + } + + $response = array(); + if ( count( $added_links ) > 0 ) { + $response['added'] = $this->serialize_smart_links( $added_links ); + } + if ( count( $failed_links ) > 0 ) { + $response['failed'] = $this->serialize_smart_links( $failed_links ); + } + if ( count( $updated_links ) > 0 ) { + $response['updated'] = $this->serialize_smart_links( $updated_links ); + } + + return new WP_REST_Response( array( 'data' => $response ), 200 ); + } + + /** + * API Endpoint: POST /smart-linking/{post_id}/set. + * + * Updates the smart links of a given post and removes the ones that are not in the request. + * + * @since 3.16.0 + * + * @param WP_REST_Request $request The request object. + * @return WP_REST_Response The response object. + */ + public function set_smart_links( WP_REST_Request $request ): WP_REST_Response { + /** + * The post object. + * + * @var WP_Post $post + */ + $post = $request->get_param( 'post' ); + + /** + * Array of Smart Link models provided in the request. + * + * @var Smart_Link[] $smart_links + */ + $smart_links = $request->get_param( 'smart_links' ); + + // Get the current stored smart links. + $existing_links = Smart_Link::get_outbound_smart_links( $post->ID ); + $removed_links = array(); + + foreach ( $existing_links as $existing_link ) { + $found = false; + foreach ( $smart_links as $smart_link ) { + if ( $smart_link->get_uid() === $existing_link->get_uid() ) { + $found = true; + break; + } + } + + if ( ! $found ) { + $removed_links[] = $existing_link; + $existing_link->delete(); + } + } + + $saved_links = array(); + $failed_links = array(); + + foreach ( $smart_links as $smart_link ) { + // The smart link properties are set in the validate callback. + $saved = $smart_link->save(); + + if ( ! $saved ) { + $failed_links[] = $smart_link; + continue; + } + + $saved_links[] = $smart_link; + } + + $response = array( + 'saved' => $this->serialize_smart_links( $saved_links ), + 'removed' => $this->serialize_smart_links( $removed_links ), + ); + + if ( count( $failed_links ) > 0 ) { + $response['failed'] = $this->serialize_smart_links( $failed_links ); + } + + return new WP_REST_Response( array( 'data' => $response ), 200 ); + } + + + /** + * API Endpoint: POST /smart-linking/url-to-post-type. + * + * Converts a URL to a post type. + * + * @since 3.16.0 + * + * @param WP_REST_Request $request The request object. + * @return WP_REST_Response The response object. + */ + public function url_to_post_type( WP_REST_Request $request ): WP_REST_Response { + $url = $request->get_param( 'url' ); + + if ( ! is_string( $url ) ) { + return new WP_REST_Response( + array( + 'error' => array( + 'name' => 'invalid_request', + 'message' => __( 'Invalid request body.', 'wp-parsely' ), + ), + ), + 400 + ); + } + + $post_id = 0; + $cache = wp_cache_get( $url, 'wp_parsely_smart_link_url_to_postid' ); + + if ( is_integer( $cache ) ) { + $post_id = $cache; + } elseif ( function_exists( 'wpcom_vip_url_to_postid' ) ) { + $post_id = wpcom_vip_url_to_postid( $url ); + } else { + // phpcs:ignore WordPressVIPMinimum.Functions.RestrictedFunctions.url_to_postid_url_to_postid + $post_id = url_to_postid( $url ); + wp_cache_set( $url, $post_id, 'wp_parsely_smart_link_url_to_postid' ); + } + + $response = array( + 'data' => array( + 'post_id' => false, + 'post_type' => false, + ), + ); + + if ( 0 !== $post_id ) { + $response['data']['post_id'] = $post_id; + $response['data']['post_type'] = get_post_type( $post_id ); + } + + return new WP_REST_Response( $response, 200 ); + } + + /** + * Validates the post ID parameter. + * + * The callback sets the post object in the request object if the parameter is valid. + * + * @since 3.16.0 + * @access private + * + * @param string $param The parameter value. + * @param WP_REST_Request $request The request object. + * @return bool Whether the parameter is valid. + */ + public function validate_post_id( string $param, WP_REST_Request $request ): bool { + if ( ! is_numeric( $param ) ) { + return false; + } + + $param = filter_var( $param, FILTER_VALIDATE_INT ); + + if ( false === $param ) { + return false; + } + + // Validate if the post ID exists. + $post = get_post( $param ); + // Set the post attribute in the request. + $request->set_param( 'post', $post ); + + return null !== $post; + } + + /** + * Validates the URL exclusion list parameter. + * + * The callback sets the URL exclusion list in the request object if the parameter is valid. + * + * @since 3.16.0 + * @access private + * + * @param mixed $param The parameter value. + * @param WP_REST_Request $request The request object. + * @return true|WP_Error Whether the parameter is valid. + */ + public function validate_url_exclusion_list( $param, WP_REST_Request $request ) { + if ( ! is_array( $param ) ) { + return new WP_Error( 'invalid_url_exclusion_list', __( 'The URL exclusion list must be an array.', 'wp-parsely' ) ); + } + + $valid_urls = array_filter( + $param, + function ( $url ) { + return is_string( $url ) && false !== filter_var( $url, FILTER_VALIDATE_URL ); + } + ); + + $request->set_param( 'url_exclusion_list', $valid_urls ); + + return true; + } + + /** + * Validates the smart link parameters. + * + * The callback sets the smart link object in the request object if the parameters are valid. + * + * @since 3.16.0 + * @access private + * + * @param array $params The parameters. + * @param WP_REST_Request $request The request object. + * @return bool Whether the parameters are valid. + */ + public function validate_smart_link_params( array $params, WP_REST_Request $request ): bool { + $required_params = array( 'uid', 'href', 'title', 'text', 'offset' ); + + foreach ( $required_params as $param ) { + if ( ! isset( $params[ $param ] ) ) { + return false; + } + } + + $encoded_data = wp_json_encode( $params ); + if ( false === $encoded_data ) { + return false; + } + + $post_id = $request->get_param( 'post_id' ); + if ( ! is_numeric( $post_id ) ) { + return false; + } + + if ( ! is_string( $params['uid'] ) ) { + return false; + } + + // Try to get the smart link from the UID. + $smart_link = Smart_Link::get_smart_link( $params['uid'], intval( $post_id ) ); + if ( $smart_link->exists() ) { + // Update the smart link with the new data. + $smart_link->set_href( $params['href'] ); + $smart_link->title = $params['title']; + $smart_link->text = $params['text']; + $smart_link->offset = $params['offset']; + } else { + /** + * The Smart Link model. + * + * @var Smart_Link $smart_link + */ + $smart_link = Smart_Link::deserialize( $encoded_data ); + $smart_link->set_source_post_id( intval( $post_id ) ); + } + + // Set the smart link attribute in the request. + $request->set_param( 'smart_link', $smart_link ); + + return true; + } + + /** + * Validates the multiple smart link parameters. + * + * The callback sets the smart links object in the request object if the parameters are valid. + * + * @since 3.16.0 + * @access private + * + * @param array> $param The parameter value. + * @param WP_REST_Request $request The request object. + * @return bool Whether the parameter is valid. + */ + public function validate_multiple_smart_links( array $param, WP_REST_Request $request ): bool { + $smart_links = array(); + + foreach ( $param as $link ) { + if ( $this->validate_smart_link_params( $link, $request ) ) { + $smart_link = $request->get_param( 'smart_link' ); + $smart_links[] = $smart_link; + } else { + return false; + } + } + $request->set_param( 'smart_link', null ); + $request->set_param( 'smart_links', $smart_links ); + + return true; + } + + /** + * Serializes an array of Smart Links. + * + * @since 3.16.0 + * + * @param Smart_Link[] $links The Smart Links to serialize. + * @return array The serialized Smart Links. + */ + private function serialize_smart_links( array $links ): array { + return array_map( + function ( Smart_Link $link ) { + return json_decode( $link->serialize(), true ); + }, + $links + ); + } +} diff --git a/src/rest-api/content-helper/class-endpoint-title-suggestions.php b/src/rest-api/content-helper/class-endpoint-title-suggestions.php new file mode 100644 index 0000000000..1f05f4411e --- /dev/null +++ b/src/rest-api/content-helper/class-endpoint-title-suggestions.php @@ -0,0 +1,160 @@ +suggest_headline_api = new Suggest_Headline_API( $this->parsely ); + } + + /** + * Returns the name of the feature associated with the current endpoint. + * + * @since 3.17.0 + * + * @return string + */ + public function get_pch_feature_name(): string { + return 'title_suggestions'; + } + + /** + * Registers the routes for the endpoint. + * + * This method should be overridden by child classes and used to register + * the routes for the endpoint. + * + * @since 3.17.0 + */ + public function register_routes(): void { + /** + * POST /title-suggestions/generate + * Generates titles for the given content. + */ + $this->register_rest_route( + 'generate', + array( 'POST' ), + array( $this, 'generate_titles' ), + array( + 'content' => array( + 'description' => __( 'The content for which to generate titles.', 'wp-parsely' ), + 'required' => true, + 'type' => 'string', + ), + 'limit' => array( + 'description' => __( 'The maximum number of titles to generate.', 'wp-parsely' ), + 'required' => false, + 'type' => 'integer', + 'default' => 3, + ), + 'style' => array( + 'description' => __( 'The style of the titles to generate.', 'wp-parsely' ), + 'required' => false, + 'type' => 'string', + 'default' => 'neutral', + ), + 'persona' => array( + 'description' => __( 'The persona of the titles to generate.', 'wp-parsely' ), + 'required' => false, + 'type' => 'string', + 'default' => 'journalist', + ), + ) + ); + } + + /** + * API Endpoint: POST /title-suggestions/generate + * + * Generates titles for the given content. + * + * @since 3.17.0 + * + * @param WP_REST_Request $request The request object. + * @return WP_REST_Response|\WP_Error The response object or a WP_Error object on failure. + */ + public function generate_titles( WP_REST_Request $request ) { + /** + * The post content to be sent to the API. + * + * @var string $post_content + */ + $post_content = $request->get_param( 'content' ); + + /** + * The maximum number of titles to generate. + * + * @var int $limit + */ + $limit = $request->get_param( 'limit' ); + + /** + * The style of the titles to generate. + * + * @var string $style + */ + $style = $request->get_param( 'style' ); + + /** + * The tone of the titles to generate. + * + * @var string $persona + */ + $persona = $request->get_param( 'persona' ); + + if ( 0 === $limit ) { + $limit = 3; + } + + $response = $this->suggest_headline_api->get_titles( $post_content, $limit, $persona, $style ); + + if ( is_wp_error( $response ) ) { + return $response; + } + + return new WP_REST_Response( array( 'data' => $response ), 200 ); + } +} diff --git a/src/rest-api/content-helper/trait-content-helper-feature.php b/src/rest-api/content-helper/trait-content-helper-feature.php new file mode 100644 index 0000000000..b303383656 --- /dev/null +++ b/src/rest-api/content-helper/trait-content-helper-feature.php @@ -0,0 +1,69 @@ +get_pch_feature_name(), + $this->parsely->get_options()['content_helper'] + ); + } + + /** + * Checks if the endpoint is available to the current user. + * + * Overrides the method in the Base_Endpoint class to check if the + * current user has permission to use the feature. + * + * @param WP_REST_Request|null $request The request object. + * + * @return bool|WP_Error True if the endpoint is available. + * @since 3.17.0 + */ + public function is_available_to_current_user( WP_REST_Request $request = null ) { + $can_use_feature = $this->is_pch_feature_enabled_for_user(); + + if ( ! $can_use_feature ) { + return new WP_Error( 'ch_access_to_feature_disabled', '', array( 'status' => 403 ) ); + } + + return parent::is_available_to_current_user( $request ); + } +} diff --git a/wp-parsely.php b/wp-parsely.php index ab26ee73ca..bff6aad664 100644 --- a/wp-parsely.php +++ b/wp-parsely.php @@ -54,6 +54,7 @@ use Parsely\RemoteAPI\Related_API; use Parsely\RemoteAPI\Remote_API_Cache; use Parsely\RemoteAPI\WordPress_Cache; +use Parsely\REST_API\REST_API_Controller; use Parsely\UI\Admin_Bar; use Parsely\UI\Admin_Warning; use Parsely\UI\Metadata_Renderer; @@ -124,6 +125,11 @@ function parsely_wp_admin_early_register(): void { $network_admin_sites_list = new Network_Admin_Sites_List( $GLOBALS['parsely'] ); $network_admin_sites_list->run(); + + // START OF PARSE.LY REST API REFACTOR (TODO: make the rest of the plugin use this). + $rest_api_controller = new REST_API_Controller( $GLOBALS['parsely'] ); + $rest_api_controller->init(); + // END OF PARSE.LY REST API REFACTOR. } add_action( 'rest_api_init', __NAMESPACE__ . '\\parsely_rest_api_init' ); From 73302360fcd30bad3ee4bfd37555232ebb6a92c2 Mon Sep 17 00:00:00 2001 From: Henrique Mouta Date: Thu, 22 Aug 2024 14:25:40 +0100 Subject: [PATCH 03/49] Add tests for the new REST API classes --- .../ContentHelperDashboardWidgetTest.php | 2 +- .../ContentHelperFeatureTest.php | 2 +- .../ContentHelperPostListStatsTest.php | 2 +- .../RestAPI/BaseAPIControllerTest.php | 182 +++++++ .../Integration/RestAPI/BaseEndpointTest.php | 483 ++++++++++++++++++ .../ContentHelperControllerTest.php | 99 ++++ .../ContentHelperFeatureTestTrait.php | 293 +++++++++++ .../EndpointExcerptGeneratorTest.php | 193 +++++++ .../EndpointSmartLinkingTest.php | 420 +++++++++++++++ .../EndpointTitleSuggestionsTest.php | 194 +++++++ .../RestAPI/RestAPIControllerTest.php | 58 +++ tests/Integration/ScriptsTest.php | 10 +- tests/Integration/TestCase.php | 12 +- 13 files changed, 1936 insertions(+), 14 deletions(-) create mode 100644 tests/Integration/RestAPI/BaseAPIControllerTest.php create mode 100644 tests/Integration/RestAPI/BaseEndpointTest.php create mode 100644 tests/Integration/RestAPI/ContentHelper/ContentHelperControllerTest.php create mode 100644 tests/Integration/RestAPI/ContentHelper/ContentHelperFeatureTestTrait.php create mode 100644 tests/Integration/RestAPI/ContentHelper/EndpointExcerptGeneratorTest.php create mode 100644 tests/Integration/RestAPI/ContentHelper/EndpointSmartLinkingTest.php create mode 100644 tests/Integration/RestAPI/ContentHelper/EndpointTitleSuggestionsTest.php create mode 100644 tests/Integration/RestAPI/RestAPIControllerTest.php diff --git a/tests/Integration/ContentHelper/ContentHelperDashboardWidgetTest.php b/tests/Integration/ContentHelper/ContentHelperDashboardWidgetTest.php index cfb9f5d8eb..fb63354ccb 100644 --- a/tests/Integration/ContentHelper/ContentHelperDashboardWidgetTest.php +++ b/tests/Integration/ContentHelper/ContentHelperDashboardWidgetTest.php @@ -45,7 +45,7 @@ protected function assert_enqueued_status( array $additional_args = array() ): void { $feature = new Dashboard_Widget( $GLOBALS['parsely'] ); - $this->set_current_user_to( $user_login, $user_role ); + self::set_current_user_to( $user_login, $user_role ); parent::set_filters( $feature::get_feature_filter_name(), diff --git a/tests/Integration/ContentHelper/ContentHelperFeatureTest.php b/tests/Integration/ContentHelper/ContentHelperFeatureTest.php index 977a968ba2..da6dd0e315 100644 --- a/tests/Integration/ContentHelper/ContentHelperFeatureTest.php +++ b/tests/Integration/ContentHelper/ContentHelperFeatureTest.php @@ -61,7 +61,7 @@ protected function assert_enqueued_status_default( string $user_login, string $user_role ): void { - $this->set_current_user_to( $user_login, $user_role ); + self::set_current_user_to( $user_login, $user_role ); self::set_filters( $feature::get_feature_filter_name(), diff --git a/tests/Integration/ContentHelper/ContentHelperPostListStatsTest.php b/tests/Integration/ContentHelper/ContentHelperPostListStatsTest.php index 7ea57fc8cd..9b817413e1 100644 --- a/tests/Integration/ContentHelper/ContentHelperPostListStatsTest.php +++ b/tests/Integration/ContentHelper/ContentHelperPostListStatsTest.php @@ -85,7 +85,7 @@ protected function assert_enqueued_status( string $user_role, array $additional_args = array() ): void { - $this->set_current_user_to( $user_login, $user_role ); + self::set_current_user_to( $user_login, $user_role ); parent::set_filters( Post_List_Stats::get_feature_filter_name(), diff --git a/tests/Integration/RestAPI/BaseAPIControllerTest.php b/tests/Integration/RestAPI/BaseAPIControllerTest.php new file mode 100644 index 0000000000..cc9835bcef --- /dev/null +++ b/tests/Integration/RestAPI/BaseAPIControllerTest.php @@ -0,0 +1,182 @@ +test_controller = new class($parsely) extends Base_API_Controller { + public const NAMESPACE = 'test'; + public const VERSION = 'v1'; + + /** + * Initialize the test controller. + * + * @since 3.17.0 + */ + protected function init(): void {} + + /** + * Expose the protected method for testing. + * + * @param Base_Endpoint[] $endpoints The endpoints to register. + */ + public function testable_register_endpoints( array $endpoints ): void { + $this->register_endpoints( $endpoints ); + } + + /** + * Expose the protected method for testing. + * + * @param Base_Endpoint $endpoint The endpoint to register. + */ + public function testable_register_endpoint( Base_Endpoint $endpoint ): void { + $this->register_endpoint( $endpoint ); + } + }; + } + + /** + * Test that the constructor throws an exception if the NAMESPACE is not defined. + * + * @since 3.17.0 + * + * @covers \Parsely\REST_API\Base_API_Controller::__construct + */ + public function test_constructor_throws_exception_without_namespace(): void { + self::expectException( \UnexpectedValueException::class ); + self::expectExceptionMessage( 'The API controller must define a namespace.' ); + + $parsely = self::createMock( Parsely::class ); + + // Use an anonymous class to avoid implementing abstract methods. + new class($parsely) extends Base_API_Controller { + /** + * Initialize the test controller. + * + * @since 3.17.0 + */ + protected function init(): void {} + }; + } + + /** + * Test the get_namespace method. + * + * @since 3.17.0 + * + * @covers \Parsely\REST_API\Base_API_Controller::get_namespace + * @uses \Parsely\REST_API\Base_API_Controller::__construct + */ + public function test_get_namespace(): void { + self::assertEquals( 'test/v1', $this->test_controller->get_namespace() ); + } + + /** + * Test the prefix_route method. + * + * @since 3.17.0 + * + * @covers \Parsely\REST_API\Base_API_Controller::prefix_route + * @uses \Parsely\REST_API\Base_API_Controller::__construct + */ + public function test_prefix_route(): void { + self::assertEquals( 'my-route', $this->test_controller->prefix_route( 'my-route' ) ); + $parsely = self::createMock( Parsely::class ); + + $controller_with_prefix = new class($parsely) extends Base_API_Controller { + public const NAMESPACE = 'test'; + public const ROUTE_PREFIX = 'prefix'; + + /** + * Initialize the test controller. + * + * @since 3.17.0 + */ + protected function init(): void {} + }; + + self::assertEquals( 'prefix/my-route', $controller_with_prefix->prefix_route( 'my-route' ) ); + } + + /** + * Test that endpoints are registered correctly. + * + * @since 3.17.0 + * + * @covers \Parsely\REST_API\Base_API_Controller::register_endpoint + * @uses \Parsely\REST_API\Base_API_Controller::__construct + * @uses \Parsely\REST_API\Base_API_Controller::register_endpoint + */ + public function test_register_endpoint(): void { + $parsely = self::createMock( Parsely::class ); + $endpoint = self::createMock( Base_Endpoint::class ); + $endpoint->expects( self::once() )->method( 'init' ); + + $this->test_controller->testable_register_endpoint( $endpoint ); // @phpstan-ignore-line + + self::assertCount( 1, $this->test_controller->endpoints ); + self::assertSame( $endpoint, $this->test_controller->endpoints[0] ); + } + + /** + * Test that multiple endpoints are registered correctly using a helper method. + * + * @since 3.17.0 + * + * @covers \Parsely\REST_API\Base_API_Controller::register_endpoints + * @uses \Parsely\REST_API\Base_API_Controller::__construct + * @uses \Parsely\REST_API\Base_API_Controller::register_endpoint + * @uses \Parsely\REST_API\Base_API_Controller::register_endpoints + */ + public function test_register_multiple_endpoints(): void { + $endpoint1 = self::createMock( Base_Endpoint::class ); + $endpoint1->expects( self::once() )->method( 'init' ); + + $endpoint2 = self::createMock( Base_Endpoint::class ); + $endpoint2->expects( self::once() )->method( 'init' ); + + $this->test_controller->testable_register_endpoints( array( $endpoint1, $endpoint2 ) ); // @phpstan-ignore-line + + self::assertCount( 2, $this->test_controller->endpoints ); + self::assertSame( $endpoint1, $this->test_controller->endpoints[0] ); + self::assertSame( $endpoint2, $this->test_controller->endpoints[1] ); + } +} diff --git a/tests/Integration/RestAPI/BaseEndpointTest.php b/tests/Integration/RestAPI/BaseEndpointTest.php new file mode 100644 index 0000000000..e9b1e6f85a --- /dev/null +++ b/tests/Integration/RestAPI/BaseEndpointTest.php @@ -0,0 +1,483 @@ +parsely ) { + $this->parsely = new Parsely(); + } + + // Create API controller, if not already created (by an inherited class). + if ( null === $this->api_controller ) { + $this->api_controller = new REST_API_Controller( $this->parsely ); + } + + parent::__construct(); + } + + /** + * Set up the test environment. + * + * @since 3.17.0 + */ + public function set_up(): void { + parent::set_up(); + TestCase::set_options(); + $this->set_current_user_to_admin(); + + $this->wp_rest_server_global_backup = $GLOBALS['wp_rest_server'] ?? null; + + // Create a concrete class for testing purposes. + $this->test_endpoint = new class($this->api_controller) extends Base_Endpoint { + protected const ENDPOINT = 'test-endpoint'; + + /** + * Register the test route. + * + * @since 3.17.0 + */ + public function register_routes(): void { + $this->register_rest_route( + '/test-route', + array( 'GET' ), + array( $this, 'get_test_data' ) + ); + } + + /** + * Get test data. + * + * @since 3.17.0 + * + * @param WP_REST_Request $request The request object. + * @return array + */ + public function get_test_data( WP_REST_Request $request ): array { + return array( 'data' => 'test' ); + } + }; + + $this->initialize_rest_endpoint(); + } + + /** + * Tear down the test environment. + * + * @since 3.17.0 + */ + public function tear_down(): void { + remove_action( 'plugins_loaded', $this->rest_api_init_proxy ); + // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedVariableFound + $GLOBALS['wp_rest_server'] = $this->wp_rest_server_global_backup; + + parent::tear_down(); + } + + /** + * Return the test endpoint instance. + * + * @since 3.17.0 + * + * @return Base_Endpoint + */ + public function get_endpoint(): Base_Endpoint { + return $this->test_endpoint; + } + + /** + * Test the init method throws an exception if ENDPOINT is not defined. + * + * @since 3.17.0 + * + * @covers \Parsely\REST_API\Base_Endpoint::init + * @uses \Parsely\REST_API\Base_Endpoint::__construct + * @uses \Parsely\REST_API\Base_API_Controller::__construct + * @uses \Parsely\Parsely::__construct + * @uses \Parsely\Parsely::allow_parsely_remote_requests + * @uses \Parsely\Parsely::are_credentials_managed + * @uses \Parsely\Parsely::set_managed_options + * @uses \Parsely\Endpoints\Base_Endpoint::__construct + * @uses \Parsely\Utils\Utils::convert_endpoint_to_filter_key + */ + public function test_init_throws_exception_without_endpoint(): void { + self::expectException( UnexpectedValueException::class ); + self::expectExceptionMessage( 'ENDPOINT constant must be defined in child class.' ); + + // Create an endpoint with no ENDPOINT constant defined. + $endpoint_without_endpoint = new class($this->api_controller) extends Base_Endpoint { + /** + * Register the routes. + * + * @since 3.17.0 + */ + public function register_routes(): void { + // Mock method for testing. + } + }; + + $endpoint_without_endpoint->init(); + } + + /** + * Test that the route is correctly registered in WordPress. + * + * @since 3.17.0 + * + * @covers \Parsely\REST_API\Base_Endpoint::init + * @covers \Parsely\REST_API\Base_Endpoint::register_routes + * @covers \Parsely\REST_API\Base_Endpoint::get_full_endpoint + * @uses \Parsely\REST_API\Base_API_Controller::__construct + * @uses \Parsely\REST_API\Base_API_Controller::get_namespace + * @uses \Parsely\REST_API\Base_API_Controller::prefix_route + * @uses \Parsely\Utils\Utils::convert_endpoint_to_filter_key + * @uses \Parsely\Parsely::__construct + * @uses \Parsely\Parsely::allow_parsely_remote_requests + * @uses \Parsely\Parsely::api_secret_is_set + * @uses \Parsely\Parsely::are_credentials_managed + * @uses \Parsely\Parsely::get_managed_credentials + * @uses \Parsely\Parsely::get_options + * @uses \Parsely\Parsely::set_default_content_helper_settings_values + * @uses \Parsely\Parsely::set_default_full_metadata_in_non_posts + * @uses \Parsely\Parsely::set_managed_options + * @uses \Parsely\Parsely::site_id_is_set + * @uses \Parsely\Permissions::build_pch_permissions_settings_array + * @uses \Parsely\Permissions::get_user_roles_with_edit_posts_cap + */ + public function test_route_is_registered(): void { + $routes = rest_get_server()->get_routes(); + + // Check that the namespace route is registered. + $expected_namespace = '/' . $this->api_controller->get_namespace(); + self::assertArrayHasKey( $expected_namespace, $routes ); + + // Check that the test route is registered. + $expected_route = $this->get_endpoint()->get_full_endpoint( 'test-route' ); + self::assertArrayHasKey( $expected_route, $routes ); + + // Check that the route is associated with the correct method. + $route_data = $routes[ $expected_route ]; + self::assertArrayHasKey( 'GET', $route_data[0]['methods'] ); + } + + /** + * Test that the route is correctly registered in WordPress, depending on the filter. + * + * @since 3.17.0 + * + * @covers \Parsely\REST_API\Base_Endpoint::init + * @covers \Parsely\REST_API\Base_Endpoint::register_routes + * @covers \Parsely\REST_API\Base_Endpoint::get_full_endpoint + * @covers \Parsely\REST_API\Base_Endpoint::get_registered_routes + * @uses \Parsely\Endpoints\Base_Endpoint::__construct + * @uses \Parsely\Parsely::__construct + * @uses \Parsely\Parsely::allow_parsely_remote_requests + * @uses \Parsely\Parsely::api_secret_is_set + * @uses \Parsely\Parsely::are_credentials_managed + * @uses \Parsely\Parsely::get_managed_credentials + * @uses \Parsely\Parsely::get_options + * @uses \Parsely\Parsely::set_default_content_helper_settings_values + * @uses \Parsely\Parsely::set_default_full_metadata_in_non_posts + * @uses \Parsely\Parsely::set_managed_options + * @uses \Parsely\Parsely::site_id_is_set + * @uses \Parsely\Permissions::build_pch_permissions_settings_array + * @uses \Parsely\Permissions::current_user_can_use_pch_feature + * @uses \Parsely\Permissions::get_user_roles_with_edit_posts_cap + * @uses \Parsely\REST_API\Base_API_Controller::__construct + * @uses \Parsely\REST_API\Base_API_Controller::get_namespace + * @uses \Parsely\REST_API\Base_API_Controller::prefix_route + * @uses \Parsely\REST_API\Base_Endpoint::__construct + * @uses \Parsely\REST_API\Base_Endpoint::get_endpoint + * @uses \Parsely\REST_API\Base_Endpoint::is_available_to_current_user + * @uses \Parsely\REST_API\Base_Endpoint::register_rest_route + * @uses \Parsely\REST_API\Base_Endpoint::validate_apikey_and_secret + * @uses \Parsely\Utils\Utils::convert_endpoint_to_filter_key + */ + public function test_endpoint_is_registered_based_on_filter(): void { + $filter_name = 'wp_parsely_api_' . + \Parsely\Utils\Utils::convert_endpoint_to_filter_key( $this->get_endpoint()->get_endpoint() ) . + '_endpoint_enabled'; + + // Test when the filter allows the endpoint to be enabled. + add_filter( $filter_name, '__return_true' ); + $this->get_endpoint()->init(); + $routes = rest_get_server()->get_routes(); + $registered_routes = $this->get_endpoint()->get_registered_routes(); + + // Assert that the routes are registered when the filter returns true. + foreach ( $registered_routes as $route ) { + self::assertArrayHasKey( $this->get_endpoint()->get_full_endpoint( $route ), $routes ); + } + + // Reset the environment. + $this->tear_down(); + + // Now test when the filter disables the endpoint. + remove_all_filters( $filter_name ); + add_filter( $filter_name, '__return_false' ); + $this->get_endpoint()->init(); + $routes = rest_get_server()->get_routes(); + $registered_routes = $this->get_endpoint()->get_registered_routes(); + + // Assert that the route is NOT registered when the filter returns false. + foreach ( $registered_routes as $route ) { + self::assertArrayNotHasKey( $this->get_endpoint()->get_full_endpoint( $route ), $routes ); + } + } + + /** + * Test is_available_to_current_user returns WP_Error if API key or secret is not set. + * + * @since 3.17.0 + * + * @covers \Parsely\REST_API\Base_Endpoint::is_available_to_current_user + * @covers \Parsely\REST_API\Base_Endpoint::validate_apikey_and_secret + * @uses \Parsely\REST_API\Base_API_Controller::__construct + * @uses \Parsely\Parsely::__construct + * @uses \Parsely\Parsely::allow_parsely_remote_requests + * @uses \Parsely\Parsely::are_credentials_managed + * @uses \Parsely\Parsely::get_managed_credentials + * @uses \Parsely\Parsely::get_options + * @uses \Parsely\Parsely::set_default_content_helper_settings_values + * @uses \Parsely\Parsely::set_default_full_metadata_in_non_posts + * @uses \Parsely\Parsely::set_managed_options + * @uses \Parsely\Parsely::site_id_is_set + * @uses \Parsely\Permissions::build_pch_permissions_settings_array + * @uses \Parsely\Permissions::get_user_roles_with_edit_posts_cap + * @uses \Parsely\Endpoints\Base_Endpoint::__construct + * @uses \Parsely\Permissions::current_user_can_use_pch_feature + * @uses \Parsely\REST_API\Base_Endpoint::__construct + * @uses \Parsely\REST_API\Base_Endpoint::init + * @uses \Parsely\Utils\Utils::convert_endpoint_to_filter_key + */ + public function test_is_available_to_current_user_returns_error_site_id_not_set(): void { + TestCase::set_options( + array( + 'apikey' => '', + 'api_secret' => '', + ) + ); + + $result = $this->get_endpoint()->is_available_to_current_user(); + + self::assertInstanceOf( WP_Error::class, $result ); + self::assertEquals( 'parsely_site_id_not_set', $result->get_error_code() ); + } + + /** + * Test is_available_to_current_user returns WP_Error if API key or secret is not set. + * + * @since 3.17.0 + * + * @covers \Parsely\REST_API\Base_Endpoint::is_available_to_current_user + * @covers \Parsely\REST_API\Base_Endpoint::validate_apikey_and_secret + * @uses \Parsely\REST_API\Base_API_Controller::__construct + * @uses \Parsely\Parsely::__construct + * @uses \Parsely\Parsely::allow_parsely_remote_requests + * @uses \Parsely\Parsely::api_secret_is_set + * @uses \Parsely\Parsely::are_credentials_managed + * @uses \Parsely\Parsely::get_managed_credentials + * @uses \Parsely\Parsely::get_options + * @uses \Parsely\Parsely::set_default_content_helper_settings_values + * @uses \Parsely\Parsely::set_default_full_metadata_in_non_posts + * @uses \Parsely\Parsely::set_managed_options + * @uses \Parsely\Parsely::site_id_is_set + * @uses \Parsely\Permissions::build_pch_permissions_settings_array + * @uses \Parsely\Permissions::get_user_roles_with_edit_posts_cap + * @uses \Parsely\Endpoints\Base_Endpoint::__construct + * @uses \Parsely\Permissions::current_user_can_use_pch_feature + * @uses \Parsely\REST_API\Base_Endpoint::__construct + * @uses \Parsely\REST_API\Base_Endpoint::init + * @uses \Parsely\Utils\Utils::convert_endpoint_to_filter_key + */ + public function test_is_available_to_current_user_returns_error_api_secret_not_set(): void { + TestCase::set_options( + array( + 'apikey' => 'test-apikey', + 'api_secret' => '', + ) + ); + + $result = $this->get_endpoint()->is_available_to_current_user(); + + self::assertInstanceOf( WP_Error::class, $result ); + self::assertEquals( 'parsely_api_secret_not_set', $result->get_error_code() ); + } + + /** + * Test apply_capability_filters method. + * + * @covers \Parsely\REST_API\Base_Endpoint::apply_capability_filters + * @uses \Parsely\REST_API\Base_API_Controller::__construct + * @uses \Parsely\Utils\Utils::convert_endpoint_to_filter_key + * @uses \Parsely\Parsely::__construct + * @uses \Parsely\Parsely::allow_parsely_remote_requests + * @uses \Parsely\Parsely::are_credentials_managed + * @uses \Parsely\Parsely::set_managed_options + * @uses \Parsely\Endpoints\Base_Endpoint::__construct + * @uses \Parsely\REST_API\Base_Endpoint::__construct + * @uses \Parsely\REST_API\Base_Endpoint::init + */ + public function test_apply_capability_filters(): void { + add_filter( + 'wp_parsely_user_capability_for_all_private_apis', + function () { + return 'edit_posts'; + } + ); + + $result = $this->get_endpoint()->apply_capability_filters( 'publish_posts' ); + self::assertEquals( 'edit_posts', $result ); + } + + /** + * Test validate_apikey_and_secret returns true when API key and secret are set. + * + * @covers \Parsely\REST_API\Base_Endpoint::validate_apikey_and_secret + * @uses \Parsely\REST_API\Base_API_Controller::__construct + * @uses \Parsely\Parsely::__construct + * @uses \Parsely\Parsely::allow_parsely_remote_requests + * @uses \Parsely\Parsely::api_secret_is_set + * @uses \Parsely\Parsely::are_credentials_managed + * @uses \Parsely\Parsely::get_managed_credentials + * @uses \Parsely\Parsely::get_options + * @uses \Parsely\Parsely::set_default_content_helper_settings_values + * @uses \Parsely\Parsely::set_default_full_metadata_in_non_posts + * @uses \Parsely\Parsely::set_managed_options + * @uses \Parsely\Parsely::site_id_is_set + * @uses \Parsely\Permissions::build_pch_permissions_settings_array + * @uses \Parsely\Permissions::get_user_roles_with_edit_posts_cap + * @uses \Parsely\Endpoints\Base_Endpoint::__construct + * @uses \Parsely\REST_API\Base_Endpoint::__construct + * @uses \Parsely\REST_API\Base_Endpoint::init + * @uses \Parsely\Utils\Utils::convert_endpoint_to_filter_key + */ + public function test_validate_api_key_and_secret_returns_true(): void { + TestCase::set_options( + array( + 'apikey' => 'test-apikey', + 'api_secret' => 'test-secret', + ) + ); + + $result = $this->get_endpoint()->validate_apikey_and_secret(); + + self::assertTrue( $result ); + } + + /** + * Sets the value of a protected or private property on a given object using reflection. + * + * This method is useful for testing purposes where you need to modify or inject dependencies + * into protected or private properties of a class. + * + * @since 3.17.0 + * + * @param object $obj The object instance on which the property should be set. + * @param string $property_name The name of the property to be set. + * @param mixed $value The value to set on the property. + * + * @throws ReflectionException If the property does not exist. + */ + protected function set_protected_property( $obj, string $property_name, $value ): void { + $reflection = new \ReflectionClass( $obj ); + $property = $reflection->getProperty( $property_name ); + $property->setAccessible( true ); + $property->setValue( $obj, $value ); + } + + /** + * Initialize the REST endpoint. + * + * @since 3.17.0 + */ + protected function initialize_rest_endpoint(): void { + // Initialize the endpoint when the plugins are loaded. + $this->rest_api_init_proxy = function () { + $this->get_endpoint()->init(); + }; + add_action( 'plugins_loaded', $this->rest_api_init_proxy ); + + // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound + do_action( 'plugins_loaded' ); + } +} diff --git a/tests/Integration/RestAPI/ContentHelper/ContentHelperControllerTest.php b/tests/Integration/RestAPI/ContentHelper/ContentHelperControllerTest.php new file mode 100644 index 0000000000..87380d31d4 --- /dev/null +++ b/tests/Integration/RestAPI/ContentHelper/ContentHelperControllerTest.php @@ -0,0 +1,99 @@ +content_helper_controller = new Content_Helper_Controller( $parsely ); + } + + /** + * Test the constructor sets up the correct namespace and version. + * + * @since 3.17.0 + * + * @covers \Parsely\REST_API\Content_Helper\Content_Helper_Controller::__construct + * @uses \Parsely\REST_API\Content_Helper\Content_Helper_Controller::get_namespace + */ + public function test_constructor_sets_up_namespace_and_version(): void { + self::assertEquals( 'wp-parsely/v2', $this->content_helper_controller->get_namespace() ); + } + + /** + * Test that the route prefix is set correctly. + * + * @since 3.17.0 + * + * @covers \Parsely\REST_API\Content_Helper\Content_Helper_Controller::ROUTE_PREFIX + */ + public function test_route_prefix(): void { + self::assertEquals( 'content-helper', Content_Helper_Controller::ROUTE_PREFIX ); + } + + /** + * Test that the init method registers the correct endpoints. + * + * @since 3.17.0 + * + * @covers \Parsely\REST_API\Content_Helper\Content_Helper_Controller::init + * @uses \Parsely\REST_API\Content_Helper\Content_Helper_Controller::register_endpoints + * @uses Parsely\Endpoints\Base_Endpoint::__construct + * @uses Parsely\REST_API\Base_API_Controller::__construct + * @uses Parsely\REST_API\Base_API_Controller::register_endpoint + * @uses Parsely\REST_API\Base_Endpoint::__construct + * @uses Parsely\REST_API\Base_Endpoint::init + * @uses Parsely\REST_API\Content_Helper\Excerpt_Generator_Endpoint::__construct + * @uses Parsely\REST_API\Content_Helper\Smart_Linking_Endpoint::__construct + * @uses Parsely\REST_API\Content_Helper\Title_Suggestions_Endpoint::__construct + * @uses Parsely\Utils\Utils::convert_endpoint_to_filter_key + */ + public function test_init_registers_endpoints(): void { + $this->content_helper_controller->init(); + + self::assertCount( 3, $this->content_helper_controller->endpoints ); + self::assertInstanceOf( Endpoint_Smart_Linking::class, $this->content_helper_controller->endpoints[0] ); + self::assertInstanceOf( Endpoint_Excerpt_Generator::class, $this->content_helper_controller->endpoints[1] ); + self::assertInstanceOf( Endpoint_Title_Suggestions::class, $this->content_helper_controller->endpoints[2] ); + } +} diff --git a/tests/Integration/RestAPI/ContentHelper/ContentHelperFeatureTestTrait.php b/tests/Integration/RestAPI/ContentHelper/ContentHelperFeatureTestTrait.php new file mode 100644 index 0000000000..47615307f8 --- /dev/null +++ b/tests/Integration/RestAPI/ContentHelper/ContentHelperFeatureTestTrait.php @@ -0,0 +1,293 @@ +enable_feature(); + $this->set_current_user_to_admin(); + + // Assert that the endpoint is available to the current user. + self::assertTrue( $this->get_endpoint()->is_available_to_current_user( new WP_REST_Request() ) ); + } + + /** + * Test that the endpoint is not available to the current user. + * + * @since 3.17.0 + * + * @covers \Parsely\REST_API\Content_Helper\Content_Helper_Feature::is_available_to_current_user + * @uses Parsely\Endpoints\Base_Endpoint::__construct + * @uses Parsely\Parsely::__construct + * @uses Parsely\Parsely::allow_parsely_remote_requests + * @uses Parsely\Parsely::api_secret_is_set + * @uses Parsely\Parsely::are_credentials_managed + * @uses Parsely\Parsely::get_managed_credentials + * @uses Parsely\Parsely::get_options + * @uses Parsely\Parsely::set_default_content_helper_settings_values + * @uses Parsely\Parsely::set_default_full_metadata_in_non_posts + * @uses Parsely\Parsely::set_managed_options + * @uses Parsely\Parsely::site_id_is_set + * @uses Parsely\Permissions::build_pch_permissions_settings_array + * @uses Parsely\Permissions::current_user_can_use_pch_feature + * @uses Parsely\Permissions::get_user_roles_with_edit_posts_cap + * @uses Parsely\REST_API\Base_API_Controller::__construct + * @uses Parsely\REST_API\Base_Endpoint::__construct + * @uses Parsely\REST_API\Base_Endpoint::init + * @uses Parsely\REST_API\Base_Endpoint::apply_capability_filters + * @uses Parsely\REST_API\Base_Endpoint::is_available_to_current_user + * @uses Parsely\REST_API\Base_Endpoint::validate_apikey_and_secret + * @uses Parsely\Utils\Utils::convert_endpoint_to_filter_key + */ + public function test_is_available_to_current_user_returns_error_if_feature_disabled(): void { + $this->disable_feature(); + $this->set_current_user_to_admin(); + + // Assert that the endpoint is not available to the current user. + self::assertInstanceOf( WP_Error::class, $this->get_endpoint()->is_available_to_current_user( new WP_REST_Request() ) ); + } + + /** + * Test that the endpoint is available to the current user, since the user has the required role. + * + * @since 3.17.0 + * + * @covers \Parsely\REST_API\Content_Helper\Content_Helper_Feature::is_available_to_current_user + * @uses Parsely\Endpoints\Base_Endpoint::__construct + * @uses Parsely\Parsely::__construct + * @uses Parsely\Parsely::allow_parsely_remote_requests + * @uses Parsely\Parsely::api_secret_is_set + * @uses Parsely\Parsely::are_credentials_managed + * @uses Parsely\Parsely::get_managed_credentials + * @uses Parsely\Parsely::get_options + * @uses Parsely\Parsely::set_default_content_helper_settings_values + * @uses Parsely\Parsely::set_default_full_metadata_in_non_posts + * @uses Parsely\Parsely::set_managed_options + * @uses Parsely\Parsely::site_id_is_set + * @uses Parsely\Permissions::build_pch_permissions_settings_array + * @uses Parsely\Permissions::current_user_can_use_pch_feature + * @uses Parsely\Permissions::get_user_roles_with_edit_posts_cap + * @uses Parsely\REST_API\Base_API_Controller::__construct + * @uses Parsely\REST_API\Base_Endpoint::__construct + * @uses Parsely\REST_API\Base_Endpoint::init + * @uses Parsely\REST_API\Base_Endpoint::apply_capability_filters + * @uses Parsely\REST_API\Base_Endpoint::is_available_to_current_user + * @uses Parsely\REST_API\Base_Endpoint::validate_apikey_and_secret + * @uses Parsely\Utils\Utils::convert_endpoint_to_filter_key + */ + public function test_is_available_to_current_user_returns_true_if_has_permissions(): void { + $this->set_feature_options( + array( + 'enabled' => true, + 'allowed_user_roles' => array( 'administrator' ), + ) + ); + + // Assert that the endpoint is available to the current user. + $this->set_current_user_to_admin(); + self::assertTrue( $this->get_endpoint()->is_available_to_current_user( new WP_REST_Request() ) ); + + // Assert that the endpoint is not available to the current user. + $this->set_current_user_to_contributor(); + self::assertInstanceOf( WP_Error::class, $this->get_endpoint()->is_available_to_current_user( new WP_REST_Request() ) ); + } + + /** + * Test that the endpoint is not available to the current user, since the user does not have the + * required role. + * + * @since 3.17.0 + * + * @covers \Parsely\REST_API\Content_Helper\Content_Helper_Feature::is_available_to_current_user + * @uses Parsely\Endpoints\Base_Endpoint::__construct + * @uses Parsely\Parsely::__construct + * @uses Parsely\Parsely::allow_parsely_remote_requests + * @uses Parsely\Parsely::api_secret_is_set + * @uses Parsely\Parsely::are_credentials_managed + * @uses Parsely\Parsely::get_managed_credentials + * @uses Parsely\Parsely::get_options + * @uses Parsely\Parsely::set_default_content_helper_settings_values + * @uses Parsely\Parsely::set_default_full_metadata_in_non_posts + * @uses Parsely\Parsely::set_managed_options + * @uses Parsely\Parsely::site_id_is_set + * @uses Parsely\Permissions::build_pch_permissions_settings_array + * @uses Parsely\Permissions::current_user_can_use_pch_feature + * @uses Parsely\Permissions::get_user_roles_with_edit_posts_cap + * @uses Parsely\REST_API\Base_API_Controller::__construct + * @uses Parsely\REST_API\Base_Endpoint::__construct + * @uses Parsely\REST_API\Base_Endpoint::init + * @uses Parsely\REST_API\Base_Endpoint::apply_capability_filters + * @uses Parsely\REST_API\Base_Endpoint::is_available_to_current_user + * @uses Parsely\REST_API\Base_Endpoint::validate_apikey_and_secret + * @uses Parsely\Utils\Utils::convert_endpoint_to_filter_key + */ + public function test_is_available_to_current_user_returns_error_if_no_permissions(): void { + $this->set_current_user_to_contributor(); + + $this->set_feature_options( + array( + 'enabled' => true, + 'allowed_user_roles' => array( 'administrator' ), + ) + ); + + // Assert that the endpoint is not available to the current user. + self::assertInstanceOf( WP_Error::class, $this->get_endpoint()->is_available_to_current_user( new WP_REST_Request() ) ); + } + + + /** + * Test that the endpoint is not available to the current user, since the user is not logged in. + * + * @since 3.17.0 + * + * @covers \Parsely\REST_API\Content_Helper\Content_Helper_Feature::is_available_to_current_user + * @uses \Parsely\Endpoints\Base_Endpoint::__construct + * @uses \Parsely\Parsely::__construct + * @uses \Parsely\Parsely::allow_parsely_remote_requests + * @uses \Parsely\Parsely::are_credentials_managed + * @uses \Parsely\Parsely::get_managed_credentials + * @uses \Parsely\Parsely::get_options + * @uses \Parsely\Parsely::set_default_full_metadata_in_non_posts + * @uses \Parsely\Parsely::set_managed_options + * @uses \Parsely\Permissions::current_user_can_use_pch_feature + * @uses \Parsely\Permissions::get_user_roles_with_edit_posts_cap + * @uses \Parsely\REST_API\Base_API_Controller::__construct + * @uses \Parsely\REST_API\Base_Endpoint::__construct + * @uses \Parsely\REST_API\Base_Endpoint::init + * @uses \Parsely\Utils\Utils::convert_endpoint_to_filter_key + */ + public function test_is_available_to_current_user_returns_error_if_no_user(): void { + $this->enable_feature(); + // Set the current user to a non-logged in user. + wp_set_current_user( 0 ); + + // Assert that the endpoint is not available to the current user. + self::assertInstanceOf( WP_Error::class, $this->get_endpoint()->is_available_to_current_user( new WP_REST_Request() ) ); + } + + /** + * You need to implement this method in your test class + * to return the endpoint instance being tested. + * + * @since 3.17.0 + * + * @return Base_Endpoint + */ + abstract protected function get_endpoint(): Base_Endpoint; + + /** + * Set the specific feature options. + * + * @since 3.17.0 + * + * @param array $options The options to set. + */ + private function set_feature_options( array $options ): void { + $feature_name = $this->get_endpoint()->get_pch_feature_name(); + + TestCase::set_options( + array( + 'apikey' => 'test', + 'api_secret' => 'test', + 'content_helper' => array( + 'ai_features_enabled' => true, + $feature_name => $options, + ), + ) + ); + } + + /** + * Disable the specific feature. + * + * @since 3.17.0 + */ + private function disable_feature(): void { + $this->set_feature_options( + array( + 'enabled' => false, + 'allowed_user_roles' => array(), + ) + ); + } + + /** + * Enable the specific feature. + * + * @since 3.17.0 + */ + private function enable_feature(): void { + $valid_roles = array_keys( Permissions::get_user_roles_with_edit_posts_cap() ); + + $this->set_feature_options( + array( + 'enabled' => true, + 'allowed_user_roles' => $valid_roles, + ) + ); + } + + /** + * Set the current user to an administrator. + * + * @since 3.17.0 + */ + abstract protected function set_current_user_to_admin(): void; + + /** + * Set the current user to a contributor. + * + * @since 3.17.0 + */ + abstract protected function set_current_user_to_contributor(): void; +} diff --git a/tests/Integration/RestAPI/ContentHelper/EndpointExcerptGeneratorTest.php b/tests/Integration/RestAPI/ContentHelper/EndpointExcerptGeneratorTest.php new file mode 100644 index 0000000000..2fe8e904c7 --- /dev/null +++ b/tests/Integration/RestAPI/ContentHelper/EndpointExcerptGeneratorTest.php @@ -0,0 +1,193 @@ +api_controller = new Content_Helper_Controller( $this->parsely ); + $this->endpoint = new Endpoint_Excerpt_Generator( $this->api_controller ); + + parent::set_up(); + } + + /** + * Get the test endpoint instance. + * + * @since 3.17.0 + * + * @return Endpoint_Excerpt_Generator + */ + public function get_endpoint(): \Parsely\REST_API\Base_Endpoint { + return $this->endpoint; + } + + /** + * Test that the endpoint is correctly registered. + * + * @since 3.17.0 + * + * @covers \Parsely\REST_API\Content_Helper\Endpoint_Excerpt_Generator::register_routes + * @uses \Parsely\Endpoints\Base_Endpoint::__construct + * @uses \Parsely\Parsely::__construct + * @uses \Parsely\Parsely::allow_parsely_remote_requests + * @uses \Parsely\Parsely::api_secret_is_set + * @uses \Parsely\Parsely::are_credentials_managed + * @uses \Parsely\Parsely::get_managed_credentials + * @uses \Parsely\Parsely::get_options + * @uses \Parsely\Parsely::set_default_content_helper_settings_values + * @uses \Parsely\Parsely::set_default_full_metadata_in_non_posts + * @uses \Parsely\Parsely::set_managed_options + * @uses \Parsely\Parsely::site_id_is_set + * @uses \Parsely\Permissions::build_pch_permissions_settings_array + * @uses \Parsely\Permissions::current_user_can_use_pch_feature + * @uses \Parsely\Permissions::get_user_roles_with_edit_posts_cap + * @uses \Parsely\REST_API\Base_API_Controller::__construct + * @uses \Parsely\REST_API\Base_API_Controller::get_namespace + * @uses \Parsely\REST_API\Base_API_Controller::prefix_route + * @uses \Parsely\REST_API\Base_Endpoint::__construct + * @uses \Parsely\REST_API\Base_Endpoint::get_endpoint + * @uses \Parsely\REST_API\Base_Endpoint::get_full_endpoint + * @uses \Parsely\REST_API\Base_Endpoint::init + * @uses \Parsely\REST_API\Base_Endpoint::is_available_to_current_user + * @uses \Parsely\REST_API\Base_Endpoint::register_rest_route + * @uses \Parsely\REST_API\Base_Endpoint::validate_apikey_and_secret + * @uses \Parsely\Utils\Utils::convert_endpoint_to_filter_key + */ + public function test_route_is_registered(): void { + // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound + do_action( 'plugins_loaded' ); + $routes = rest_get_server()->get_routes(); + + // Check that the excerpt-generator/generate route is registered. + $expected_route = $this->get_endpoint()->get_full_endpoint( 'generate' ); + self::assertArrayHasKey( $expected_route, $routes ); + + // Check that the route is associated with the POST method. + $route_data = $routes[ $expected_route ]; + self::assertArrayHasKey( 'POST', $route_data[0]['methods'] ); + } + + /** + * Test that the generate_excerpt method returns a valid response. + * + * @since 3.17.0 + * + * @covers \Parsely\REST_API\Content_Helper\Endpoint_Excerpt_Generator::generate_excerpt + * @uses \Parsely\Endpoints\Base_Endpoint::__construct + * @uses \Parsely\Parsely::__construct + * @uses \Parsely\Parsely::allow_parsely_remote_requests + * @uses \Parsely\Parsely::are_credentials_managed + * @uses \Parsely\Parsely::set_managed_options + * @uses \Parsely\REST_API\Base_API_Controller::__construct + * @uses \Parsely\REST_API\Base_Endpoint::__construct + */ + public function test_generate_excerpt_returns_valid_response(): void { + // Mock the Suggest_Brief_API to control the response. + $mock_suggest_api = $this->createMock( Suggest_Brief_API::class ); + $mock_suggest_api->expects( self::once() ) + ->method( 'get_suggestion' ) + ->willReturn( array( 'summary' => 'This is a test excerpt.' ) ); + + $this->set_protected_property( $this->get_endpoint(), 'suggest_brief_api', $mock_suggest_api ); + + // Create a mock request. + $request = new WP_REST_Request( 'POST', '/excerpt-generator/generate' ); + $request->set_param( 'content', 'Test content' ); + $request->set_param( 'title', 'Test title' ); + $request->set_param( 'persona', 'journalist' ); + $request->set_param( 'style', 'neutral' ); + + // Call the generate_excerpt method. + $response = $this->get_endpoint()->generate_excerpt( $request ); + + // Assert that the response is a WP_REST_Response and contains the correct data. + self::assertInstanceOf( WP_REST_Response::class, $response ); + + /** + * The response data. + * + * @var array> $data The response data. + */ + $data = $response->get_data(); + self::assertArrayHasKey( 'data', $data ); + self::assertEquals( 'This is a test excerpt.', $data['data']['summary'] ); + } + + /** + * Test that the generate_excerpt method returns an error if Suggest_Brief_API fails. + * + * @since 3.17.0 + * + * @covers \Parsely\REST_API\Content_Helper\Endpoint_Excerpt_Generator::generate_excerpt + * @uses \Parsely\Endpoints\Base_Endpoint::__construct + * @uses \Parsely\Parsely::__construct + * @uses \Parsely\Parsely::allow_parsely_remote_requests + * @uses \Parsely\Parsely::are_credentials_managed + * @uses \Parsely\Parsely::set_managed_options + * @uses \Parsely\REST_API\Base_API_Controller::__construct + * @uses \Parsely\REST_API\Base_Endpoint::__construct + */ + public function test_generate_excerpt_returns_error_on_failure(): void { + // Mock the Suggest_Brief_API to simulate a failure. + $mock_suggest_api = $this->createMock( Suggest_Brief_API::class ); + $mock_suggest_api->expects( self::once() ) + ->method( 'get_suggestion' ) + ->willReturn( new WP_Error( 'api_error', 'API request failed' ) ); + + $this->set_protected_property( $this->get_endpoint(), 'suggest_brief_api', $mock_suggest_api ); + + // Create a mock request. + $request = new WP_REST_Request( 'POST', '/excerpt-generator/generate' ); + $request->set_param( 'content', 'Test content' ); + $request->set_param( 'title', 'Test title' ); + $request->set_param( 'persona', 'journalist' ); + $request->set_param( 'style', 'neutral' ); + + // Call the generate_excerpt method. + $response = $this->get_endpoint()->generate_excerpt( $request ); + + // Assert that the response is a WP_Error. + self::assertInstanceOf( WP_Error::class, $response ); + self::assertEquals( 'api_error', $response->get_error_code() ); + } +} diff --git a/tests/Integration/RestAPI/ContentHelper/EndpointSmartLinkingTest.php b/tests/Integration/RestAPI/ContentHelper/EndpointSmartLinkingTest.php new file mode 100644 index 0000000000..476f3a8c7d --- /dev/null +++ b/tests/Integration/RestAPI/ContentHelper/EndpointSmartLinkingTest.php @@ -0,0 +1,420 @@ +api_controller = new Content_Helper_Controller( $this->parsely ); + $this->endpoint = new Endpoint_Smart_Linking( $this->api_controller ); + + parent::set_up(); + + // Setup fake API key and secret. + TestCase::set_options( + array( + 'apikey' => 'test-apikey', + 'api_secret' => 'test-secret', + ) + ); + } + + /** + * Get the test endpoint instance. + * + * @since 3.17.0 + * + * @return Endpoint_Smart_Linking + */ + public function get_endpoint(): \Parsely\REST_API\Base_Endpoint { + return $this->endpoint; + } + + /** + * Test that the endpoint is correctly registered. + * + * @since 3.17.0 + * + * @covers \Parsely\REST_API\Content_Helper\Endpoint_Smart_Linking::register_routes + * @uses \Parsely\Endpoints\Base_Endpoint::__construct + * @uses \Parsely\Parsely::__construct + * @uses \Parsely\Parsely::allow_parsely_remote_requests + * @uses \Parsely\Parsely::api_secret_is_set + * @uses \Parsely\Parsely::are_credentials_managed + * @uses \Parsely\Parsely::get_managed_credentials + * @uses \Parsely\Parsely::get_options + * @uses \Parsely\Parsely::set_default_content_helper_settings_values + * @uses \Parsely\Parsely::set_default_full_metadata_in_non_posts + * @uses \Parsely\Parsely::set_managed_options + * @uses \Parsely\Parsely::site_id_is_set + * @uses \Parsely\Permissions::build_pch_permissions_settings_array + * @uses \Parsely\Permissions::current_user_can_use_pch_feature + * @uses \Parsely\Permissions::get_user_roles_with_edit_posts_cap + * @uses \Parsely\REST_API\Base_API_Controller::__construct + * @uses \Parsely\REST_API\Base_API_Controller::get_namespace + * @uses \Parsely\REST_API\Base_API_Controller::prefix_route + * @uses \Parsely\REST_API\Base_Endpoint::__construct + * @uses \Parsely\REST_API\Base_Endpoint::get_endpoint + * @uses \Parsely\REST_API\Base_Endpoint::get_full_endpoint + * @uses \Parsely\REST_API\Base_Endpoint::init + * @uses \Parsely\REST_API\Base_Endpoint::is_available_to_current_user + * @uses \Parsely\REST_API\Base_Endpoint::register_rest_route + * @uses \Parsely\REST_API\Base_Endpoint::validate_apikey_and_secret + * @uses \Parsely\Utils\Utils::convert_endpoint_to_filter_key + */ + public function test_route_is_registered(): void { + // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound + do_action( 'plugins_loaded' ); + $routes = rest_get_server()->get_routes(); + + // Check that the smart-linking/generate route is registered. + $expected_route = $this->get_endpoint()->get_full_endpoint( 'generate' ); + self::assertArrayHasKey( $expected_route, $routes ); + + // Check that the route is associated with the POST method. + $route_data = $routes[ $expected_route ]; + self::assertArrayHasKey( 'POST', $route_data[0]['methods'] ); + } + + /** + * Test that the generate_smart_links method returns a valid response. + * + * @since 3.17.0 + * + * @covers \Parsely\REST_API\Content_Helper\Endpoint_Smart_Linking::generate_smart_links + * @uses \Parsely\Endpoints\Base_Endpoint::__construct + * @uses \Parsely\Parsely::__construct + * @uses \Parsely\Parsely::allow_parsely_remote_requests + * @uses \Parsely\Parsely::are_credentials_managed + * @uses \Parsely\Parsely::set_managed_options + * @uses \Parsely\REST_API\Base_API_Controller::__construct + * @uses \Parsely\REST_API\Base_Endpoint::__construct + */ + public function test_generate_smart_links_returns_valid_response(): void { + // Mock the Suggest_Linked_Reference_API to control the response. + $mock_suggest_api = $this->createMock( Suggest_Linked_Reference_API::class ); + $mock_suggest_api->expects( self::once() ) + ->method( 'get_links' ) + ->willReturn( + array( + new Smart_Link( 'link1', 'http://example.com/1', 'Example 1', 0, 0 ), + new Smart_Link( 'link2', 'http://example.com/2', 'Example 2', 0, 1 ), + ) + ); + + $this->set_protected_property( $this->get_endpoint(), 'suggest_linked_reference_api', $mock_suggest_api ); + + // Create a mock request. + $request = new WP_REST_Request( 'POST', '/smart-linking/generate' ); + $request->set_param( 'content', 'Test content' ); + $request->set_param( 'max_links', 2 ); + $request->set_param( 'url_exclusion_list', array( 'http://excluded.com' ) ); + + // Call the generate_smart_links method. + $response = $this->get_endpoint()->generate_smart_links( $request ); + + // Assert that the response is a WP_REST_Response and contains the correct data. + self::assertInstanceOf( WP_REST_Response::class, $response ); + + /** + * The response data. + * + * @var array> $data The response data. + */ + $data = $response->get_data(); + self::assertArrayHasKey( 'data', $data ); + self::assertCount( 2, $data['data'] ); + } + + /** + * Test that the generate_smart_links method returns an error if Suggest_Linked_Reference_API fails. + * + * @since 3.17.0 + * + * @covers \Parsely\REST_API\Content_Helper\Endpoint_Smart_Linking::generate_smart_links + * @uses \Parsely\Endpoints\Base_Endpoint::__construct + * @uses \Parsely\Parsely::__construct + * @uses \Parsely\Parsely::allow_parsely_remote_requests + * @uses \Parsely\Parsely::are_credentials_managed + * @uses \Parsely\Parsely::set_managed_options + * @uses \Parsely\REST_API\Base_API_Controller::__construct + * @uses \Parsely\REST_API\Base_Endpoint::__construct + */ + public function test_generate_smart_links_returns_error_on_failure(): void { + // Mock the Suggest_Linked_Reference_API to simulate a failure. + $mock_suggest_api = $this->createMock( Suggest_Linked_Reference_API::class ); + $mock_suggest_api->expects( self::once() ) + ->method( 'get_links' ) + ->willReturn( new WP_Error( 'api_error', 'API request failed' ) ); + + $this->set_protected_property( $this->get_endpoint(), 'suggest_linked_reference_api', $mock_suggest_api ); + + // Create a mock request. + $request = new WP_REST_Request( 'POST', '/smart-linking/generate' ); + $request->set_param( 'content', 'Test content' ); + $request->set_param( 'max_links', 2 ); + $request->set_param( 'url_exclusion_list', array( 'http://excluded.com' ) ); + + // Call the generate_smart_links method. + $response = $this->get_endpoint()->generate_smart_links( $request ); + + // Assert that the response is a WP_Error. + self::assertInstanceOf( WP_Error::class, $response ); + self::assertEquals( 'api_error', $response->get_error_code() ); + } + + /** + * Test that the add_smart_link method returns a valid response when adding a new smart link. + * + * @since 3.17.0 + * + * @covers \Parsely\REST_API\Content_Helper\Endpoint_Smart_Linking::add_smart_link + * @uses \Parsely\Endpoints\Base_Endpoint::__construct + * @uses \Parsely\Models\Base_Model::__construct + * @uses \Parsely\Models\Base_Model::serialize + * @uses \Parsely\Models\Smart_Link::__construct + * @uses \Parsely\Models\Smart_Link::deserialize + * @uses \Parsely\Models\Smart_Link::exists + * @uses \Parsely\Models\Smart_Link::generate_uid + * @uses \Parsely\Models\Smart_Link::get_post_id_by_url + * @uses \Parsely\Models\Smart_Link::get_smart_link + * @uses \Parsely\Models\Smart_Link::get_smart_link_object_by_uid + * @uses \Parsely\Models\Smart_Link::load + * @uses \Parsely\Models\Smart_Link::save + * @uses \Parsely\Models\Smart_Link::set_href + * @uses \Parsely\Models\Smart_Link::set_source_post_id + * @uses \Parsely\Models\Smart_Link::set_uid + * @uses \Parsely\Models\Smart_Link::to_array + * @uses \Parsely\Parsely::api_secret_is_set + * @uses \Parsely\Parsely::get_managed_credentials + * @uses \Parsely\Parsely::get_options + * @uses \Parsely\Parsely::set_default_content_helper_settings_values + * @uses \Parsely\Parsely::set_default_full_metadata_in_non_posts + * @uses \Parsely\Parsely::site_id_is_set + * @uses \Parsely\Permissions::build_pch_permissions_settings_array + * @uses \Parsely\Permissions::current_user_can_use_pch_feature + * @uses \Parsely\Permissions::get_user_roles_with_edit_posts_cap + * @uses \Parsely\REST_API\Base_API_Controller::__construct + * @uses \Parsely\REST_API\Base_API_Controller::get_namespace + * @uses \Parsely\REST_API\Base_API_Controller::prefix_route + * @uses \Parsely\REST_API\Base_Endpoint::__construct + * @uses \Parsely\REST_API\Base_Endpoint::apply_capability_filters + * @uses \Parsely\REST_API\Base_Endpoint::get_endpoint + * @uses \Parsely\REST_API\Base_Endpoint::get_full_endpoint + * @uses \Parsely\REST_API\Base_Endpoint::init + * @uses \Parsely\REST_API\Base_Endpoint::is_available_to_current_user + * @uses \Parsely\REST_API\Base_Endpoint::register_rest_route + * @uses \Parsely\REST_API\Base_Endpoint::validate_apikey_and_secret + * @uses \Parsely\Utils\Utils::convert_endpoint_to_filter_key + */ + public function test_add_smart_link_returns_valid_response(): void { + // Create mocked post. + $post = WP_UnitTestCase_Base::factory()->post->create_and_get(); + self::assertNotWPError( $post ); + $post_id = $post->ID; // @phpstan-ignore-line + + // Create a mock request. + $request = new WP_REST_Request( 'POST', $this->get_endpoint()->get_full_endpoint( $post_id . '/add' ) ); + + $smart_link_data = array( + 'uid' => md5( 'link1' ), + 'href' => 'http://example.com/1', + 'title' => 'Example 1', + 'text' => 'Example 1', + 'offset' => 0, + ); + $request->set_param( 'link', $smart_link_data ); + + // Dispatch the request. + $response = rest_get_server()->dispatch( $request ); + + // Assert that the response is a WP_REST_Response and contains the correct data. + self::assertInstanceOf( WP_REST_Response::class, $response ); + + /** + * The response data. + * + * @var array> $data The response data. + */ + $data = $response->get_data(); + + self::assertNotTrue( $response->is_error() ); + + self::assertArrayHasKey( 'data', $data ); + self::assertIsObject( $data['data'] ); + + $smart_link_attributes = array( + 'smart_link_id', + 'uid', + 'href', + 'text', + 'offset', + 'applied', + 'source', + 'destination', + ); + + foreach ( $smart_link_attributes as $attribute ) { + self::assertObjectHasProperty( $attribute, $data['data'] ); + } + } + + /** + * Tests that the add_multiple_smart_links method returns a valid response when adding multiple smart links. + * + * @since 3.17.0 + * + * @covers \Parsely\REST_API\Content_Helper\Endpoint_Smart_Linking::add_multiple_smart_links + * + * @uses \Parsely\Endpoints\Base_Endpoint::__construct + * @uses \Parsely\Models\Base_Model::__construct + * @uses \Parsely\Models\Base_Model::serialize + * @uses \Parsely\Models\Smart_Link::__construct + * @uses \Parsely\Models\Smart_Link::deserialize + * @uses \Parsely\Models\Smart_Link::exists + * @uses \Parsely\Models\Smart_Link::generate_uid + * @uses \Parsely\Models\Smart_Link::get_post_id_by_url + * @uses \Parsely\Models\Smart_Link::get_smart_link + * @uses \Parsely\Models\Smart_Link::get_smart_link_object_by_uid + * @uses \Parsely\Models\Smart_Link::load + * @uses \Parsely\Models\Smart_Link::save + * @uses \Parsely\Models\Smart_Link::set_href + * @uses \Parsely\Models\Smart_Link::set_source_post_id + * @uses \Parsely\Models\Smart_Link::set_uid + * @uses \Parsely\Models\Smart_Link::to_array + * @uses \Parsely\Parsely::api_secret_is_set + * @uses \Parsely\Parsely::get_managed_credentials + * @uses \Parsely\Parsely::get_options + * @uses \Parsely\Parsely::set_default_content_helper_settings_values + * @uses \Parsely\Parsely::set_default_full_metadata_in_non_posts + * @uses \Parsely\Parsely::site_id_is_set + * @uses \Parsely\Permissions::build_pch_permissions_settings_array + * @uses \Parsely\Permissions::current_user_can_use_pch_feature + * @uses \Parsely\Permissions::get_user_roles_with_edit_posts_cap + * @uses \Parsely\REST_API\Base_API_Controller::__construct + * @uses \Parsely\REST_API\Base_API_Controller::get_namespace + * @uses \Parsely\REST_API\Base_API_Controller::prefix_route + * @uses \Parsely\REST_API\Base_Endpoint::__construct + * @uses \Parsely\REST_API\Base_Endpoint::apply_capability_filters + * @uses \Parsely\REST_API\Base_Endpoint::get_endpoint + * @uses \Parsely\REST_API\Base_Endpoint::get_full_endpoint + * @uses \Parsely\REST_API\Base_Endpoint::init + * @uses \Parsely\REST_API\Base_Endpoint::is_available_to_current_user + * @uses \Parsely\REST_API\Base_Endpoint::register_rest_route + * @uses \Parsely\REST_API\Base_Endpoint::validate_apikey_and_secret + * @uses \Parsely\Utils\Utils::convert_endpoint_to_filter_key + */ + public function test_add_multiple_smart_links_returns_valid_response(): void { + // Create mocked post. + $post = WP_UnitTestCase_Base::factory()->post->create_and_get(); + self::assertNotWPError( $post ); + $post_id = $post->ID; // @phpstan-ignore-line + + // Create a mock request. + $request = new WP_REST_Request( 'POST', $this->get_endpoint()->get_full_endpoint( $post_id . '/add-multiple' ) ); + + $smart_links_data = array( + array( + 'uid' => md5( 'link1' ), + 'href' => 'http://example.com/1', + 'title' => 'Example 1', + 'text' => 'Example 1', + 'offset' => 0, + ), + array( + 'uid' => md5( 'link2' ), + 'href' => 'http://example.com/2', + 'title' => 'Example 2', + 'text' => 'Example 2', + 'offset' => 0, + ), + array( + 'uid' => md5( 'link3' ), + 'href' => 'http://example.com/3', + 'title' => 'Example 3', + 'text' => 'Example 3', + 'offset' => 0, + ), + ); + $request->set_param( 'links', $smart_links_data ); + + // Dispatch the request. + $response = rest_get_server()->dispatch( $request ); + + // Assert that the response is a WP_REST_Response and contains the correct data. + self::assertInstanceOf( WP_REST_Response::class, $response ); + + /** + * The response data. + * + * @var array> $data The response data. + */ + $data = $response->get_data(); + + self::assertNotTrue( $response->is_error() ); + + self::assertArrayHasKey( 'data', $data ); + self::assertArrayHasKey( 'added', $data['data'] ); + self::assertIsArray( $data['data']['added'] ); + self::assertCount( 3, $data['data']['added'] ); + + $smart_link_attributes = array( + 'smart_link_id', + 'uid', + 'href', + 'text', + 'offset', + 'applied', + 'source', + 'destination', + ); + + foreach ( $data['data']['added'] as $smart_link ) { + foreach ( $smart_link_attributes as $attribute ) { + self::assertArrayHasKey( $attribute, $smart_link ); + } + } + } +} diff --git a/tests/Integration/RestAPI/ContentHelper/EndpointTitleSuggestionsTest.php b/tests/Integration/RestAPI/ContentHelper/EndpointTitleSuggestionsTest.php new file mode 100644 index 0000000000..9e94182cf1 --- /dev/null +++ b/tests/Integration/RestAPI/ContentHelper/EndpointTitleSuggestionsTest.php @@ -0,0 +1,194 @@ +api_controller = new Content_Helper_Controller( $this->parsely ); + $this->endpoint = new Endpoint_Title_Suggestions( $this->api_controller ); + + parent::set_up(); + } + + /** + * Get the test endpoint instance. + * + * @return Endpoint_Title_Suggestions + * @since 3.17.0 + */ + public function get_endpoint(): \Parsely\REST_API\Base_Endpoint { + return $this->endpoint; + } + + /** + * Test that the endpoint is correctly registered. + * + * @since 3.17.0 + * + * @covers \Parsely\REST_API\Content_Helper\Endpoint_Title_Suggestions::register_routes + * @uses \Parsely\Endpoints\Base_Endpoint::__construct + * @uses \Parsely\Parsely::__construct + * @uses \Parsely\Parsely::allow_parsely_remote_requests + * @uses \Parsely\Parsely::api_secret_is_set + * @uses \Parsely\Parsely::are_credentials_managed + * @uses \Parsely\Parsely::get_managed_credentials + * @uses \Parsely\Parsely::get_options + * @uses \Parsely\Parsely::set_default_content_helper_settings_values + * @uses \Parsely\Parsely::set_default_full_metadata_in_non_posts + * @uses \Parsely\Parsely::set_managed_options + * @uses \Parsely\Parsely::site_id_is_set + * @uses \Parsely\Permissions::build_pch_permissions_settings_array + * @uses \Parsely\Permissions::current_user_can_use_pch_feature + * @uses \Parsely\Permissions::get_user_roles_with_edit_posts_cap + * @uses \Parsely\REST_API\Base_API_Controller::__construct + * @uses \Parsely\REST_API\Base_API_Controller::get_namespace + * @uses \Parsely\REST_API\Base_API_Controller::prefix_route + * @uses \Parsely\REST_API\Base_Endpoint::__construct + * @uses \Parsely\REST_API\Base_Endpoint::get_endpoint + * @uses \Parsely\REST_API\Base_Endpoint::get_full_endpoint + * @uses \Parsely\REST_API\Base_Endpoint::init + * @uses \Parsely\REST_API\Base_Endpoint::is_available_to_current_user + * @uses \Parsely\REST_API\Base_Endpoint::register_rest_route + * @uses \Parsely\REST_API\Base_Endpoint::validate_apikey_and_secret + * @uses \Parsely\Utils\Utils::convert_endpoint_to_filter_key + */ + public function test_route_is_registered(): void { + $routes = rest_get_server()->get_routes(); + + // Check that the title-suggestions/generate route is registered. + $expected_route = $this->get_endpoint()->get_full_endpoint( 'generate' ); + self::assertArrayHasKey( $expected_route, $routes ); + + // Check that the route is associated with the POST method. + $route_data = $routes[ $expected_route ]; + self::assertArrayHasKey( 'POST', $route_data[0]['methods'] ); + } + + /** + * Test that the generate_titles method returns a valid response. + * + * @since 3.17.0 + * + * @covers \Parsely\REST_API\Content_Helper\Endpoint_Title_Suggestions::generate_titles + * @uses \Parsely\Endpoints\Base_Endpoint::__construct + * @uses \Parsely\Parsely::__construct + * @uses \Parsely\Parsely::allow_parsely_remote_requests + * @uses \Parsely\Parsely::are_credentials_managed + * @uses \Parsely\Parsely::set_managed_options + * @uses \Parsely\REST_API\Base_API_Controller::__construct + * @uses \Parsely\REST_API\Base_Endpoint::__construct + * @uses \Parsely\REST_API\Base_Endpoint::init + * @uses \Parsely\Utils\Utils::convert_endpoint_to_filter_key + */ + public function test_generate_titles_returns_valid_response(): void { + // Mock the Suggest_Headline_API to control the response. + $mock_suggest_api = $this->createMock( Suggest_Headline_API::class ); + $mock_suggest_api->expects( self::once() ) + ->method( 'get_titles' ) + ->willReturn( array( 'title1', 'title2', 'title3' ) ); + + $this->set_protected_property( $this->get_endpoint(), 'suggest_headline_api', $mock_suggest_api ); + + // Create a mock request. + $request = new WP_REST_Request( 'POST', '/title-suggestions/generate' ); + $request->set_param( 'content', 'Test content' ); + $request->set_param( 'limit', 3 ); + $request->set_param( 'style', 'neutral' ); + $request->set_param( 'persona', 'journalist' ); + + // Call the generate_titles method. + $response = $this->get_endpoint()->generate_titles( $request ); + + // Assert that the response is a WP_REST_Response and contains the correct data. + self::assertInstanceOf( WP_REST_Response::class, $response ); + + /** + * The response data. + * + * @var array $data The response data. + */ + $data = $response->get_data(); + self::assertArrayHasKey( 'data', $data ); + self::assertEquals( array( 'title1', 'title2', 'title3' ), $data['data'] ); + } + + /** + * Test that the generate_titles method returns an error if Suggest_Headline_API fails. + * + * @since 3.17.0 + * + * @covers \Parsely\REST_API\Content_Helper\Endpoint_Title_Suggestions::generate_titles + * @uses \Parsely\Endpoints\Base_Endpoint::__construct + * @uses \Parsely\Parsely::__construct + * @uses \Parsely\Parsely::allow_parsely_remote_requests + * @uses \Parsely\Parsely::are_credentials_managed + * @uses \Parsely\Parsely::set_managed_options + * @uses \Parsely\REST_API\Base_API_Controller::__construct + * @uses \Parsely\REST_API\Base_Endpoint::__construct + * @uses \Parsely\REST_API\Base_Endpoint::init + * @uses \Parsely\Utils\Utils::convert_endpoint_to_filter_key + */ + public function test_generate_titles_returns_error_on_failure(): void { + // Mock the Suggest_Headline_API to simulate a failure. + $mock_suggest_api = $this->createMock( Suggest_Headline_API::class ); + $mock_suggest_api->expects( self::once() ) + ->method( 'get_titles' ) + ->willReturn( new WP_Error( 'api_error', 'API request failed' ) ); + + $this->set_protected_property( $this->get_endpoint(), 'suggest_headline_api', $mock_suggest_api ); + + // Create a mock request. + $request = new WP_REST_Request( 'POST', '/title-suggestions/generate' ); + $request->set_param( 'content', 'Test content' ); + $request->set_param( 'limit', 3 ); + $request->set_param( 'style', 'neutral' ); + $request->set_param( 'persona', 'journalist' ); + + // Call the generate_titles method. + $response = $this->get_endpoint()->generate_titles( $request ); + + // Assert that the response is a WP_Error. + self::assertInstanceOf( WP_Error::class, $response ); + self::assertEquals( 'api_error', $response->get_error_code() ); + } +} diff --git a/tests/Integration/RestAPI/RestAPIControllerTest.php b/tests/Integration/RestAPI/RestAPIControllerTest.php new file mode 100644 index 0000000000..0a8968691d --- /dev/null +++ b/tests/Integration/RestAPI/RestAPIControllerTest.php @@ -0,0 +1,58 @@ +test_controller = new REST_API_Controller( $parsely ); + } + + /** + * Test the constructor sets up the correct namespace and version. + * + * @since 3.17.0 + * + * @covers \Parsely\REST_API\REST_API_Controller::__construct + * @uses \Parsely\REST_API\REST_API_Controller::get_namespace + */ + public function test_constructor_sets_up_namespace_and_version(): void { + self::assertEquals( 'wp-parsely/v2', $this->test_controller->get_namespace() ); + } +} diff --git a/tests/Integration/ScriptsTest.php b/tests/Integration/ScriptsTest.php index 7f598df639..7eabab3f68 100644 --- a/tests/Integration/ScriptsTest.php +++ b/tests/Integration/ScriptsTest.php @@ -347,7 +347,7 @@ public function test_track_logged_in_users(): void { 'track_authenticated_users' => true, ) ); - $new_user_id = $this->create_test_user( 'bill_brasky' ); + $new_user_id = self::create_test_user( 'bill_brasky' ); wp_set_current_user( $new_user_id ); $this->go_to_new_post(); self::$scripts->register_scripts(); @@ -390,10 +390,10 @@ public function test_do_not_track_logged_in_users_multisite(): void { } // Set up users and blogs. - $first_blog_admin = $this->create_test_user( 'optimus_prime' ); - $second_blog_admin = $this->create_test_user( 'megatron' ); - $first_blog = $this->create_test_blog( 'autobots', $first_blog_admin ); - $second_blog = $this->create_test_blog( 'decepticons', $second_blog_admin ); + $first_blog_admin = self::create_test_user( 'optimus_prime' ); + $second_blog_admin = self::create_test_user( 'megatron' ); + $first_blog = self::create_test_blog( 'autobots', $first_blog_admin ); + $second_blog = self::create_test_blog( 'decepticons', $second_blog_admin ); // These custom options will be used for both blogs. $custom_options = array( diff --git a/tests/Integration/TestCase.php b/tests/Integration/TestCase.php index c2bc790582..1f0ede3471 100644 --- a/tests/Integration/TestCase.php +++ b/tests/Integration/TestCase.php @@ -100,7 +100,7 @@ public function create_test_category( string $name ): int { * @param string $user_role The user's role. Default is subscriber. * @return int The newly created user's ID. */ - public function create_test_user( string $user_login, string $user_role = 'subscriber' ): int { + public static function create_test_user( string $user_login, string $user_role = 'subscriber' ): int { /** @var int */ return self::factory()->user->create( array( @@ -427,15 +427,15 @@ public function go_to_new_post( string $post_status = 'publish' ): int { * @param string $user_login The user's login. * @param string $user_role The user's role. */ - public function set_current_user_to( string $user_login, string $user_role ): void { + public static function set_current_user_to( string $user_login, string $user_role ): void { $user = get_user_by( 'login', $user_login ); if ( false === $user ) { - $user_id = $this->create_test_user( $user_login, $user_role ); + $user_id = self::create_test_user( $user_login, $user_role ); $user = get_user_by( 'id', $user_id ); } if ( false === $user ) { - $this::fail( 'Invalid user.' ); + self::fail( 'Invalid user.' ); } wp_set_current_user( $user->ID ); @@ -445,14 +445,14 @@ public function set_current_user_to( string $user_login, string $user_role ): vo * Changes the current user to the built-in admin account. */ public function set_current_user_to_admin(): void { - $this->set_current_user_to( 'admin', 'administrator' ); + self::set_current_user_to( 'admin', 'administrator' ); } /** * Changes the current user to a contributor account. */ public function set_current_user_to_contributor(): void { - $this->set_current_user_to( 'test_contributor', 'contributor' ); + self::set_current_user_to( 'test_contributor', 'contributor' ); } /** From e5000322ff87303be3c8b2e7f7db2b7803d6c474 Mon Sep 17 00:00:00 2001 From: Henrique Mouta Date: Thu, 22 Aug 2024 14:33:17 +0100 Subject: [PATCH 04/49] Update UI providers to use the new API endpoints --- build/content-helper/editor-sidebar.asset.php | 2 +- build/content-helper/editor-sidebar.js | 4 ++-- build/content-helper/excerpt-generator.asset.php | 2 +- build/content-helper/excerpt-generator.js | 2 +- .../editor-sidebar/smart-linking/provider.ts | 14 +++++++------- .../editor-sidebar/title-suggestions/provider.ts | 2 +- src/content-helper/excerpt-generator/provider.ts | 2 +- 7 files changed, 14 insertions(+), 14 deletions(-) diff --git a/build/content-helper/editor-sidebar.asset.php b/build/content-helper/editor-sidebar.asset.php index a585d85b14..9aedc45478 100644 --- a/build/content-helper/editor-sidebar.asset.php +++ b/build/content-helper/editor-sidebar.asset.php @@ -1 +1 @@ - array('react', 'wp-api-fetch', 'wp-block-editor', 'wp-blocks', 'wp-components', 'wp-compose', 'wp-core-data', 'wp-data', 'wp-dom-ready', 'wp-editor', 'wp-element', 'wp-hooks', 'wp-i18n', 'wp-plugins', 'wp-primitives', 'wp-url'), 'version' => 'cd18a7b42cbf7a260800'); + array('react', 'wp-api-fetch', 'wp-block-editor', 'wp-blocks', 'wp-components', 'wp-compose', 'wp-core-data', 'wp-data', 'wp-dom-ready', 'wp-editor', 'wp-element', 'wp-hooks', 'wp-i18n', 'wp-plugins', 'wp-primitives', 'wp-url'), 'version' => '6a5829c6e4ff7bf83951'); diff --git a/build/content-helper/editor-sidebar.js b/build/content-helper/editor-sidebar.js index b096ce75ce..c44b4e6933 100644 --- a/build/content-helper/editor-sidebar.js +++ b/build/content-helper/editor-sidebar.js @@ -1,5 +1,5 @@ !function(){"use strict";var e={20:function(e,t,n){var r=n(609),i=Symbol.for("react.element"),s=Symbol.for("react.fragment"),o=Object.prototype.hasOwnProperty,a=r.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED.ReactCurrentOwner,l={key:!0,ref:!0,__self:!0,__source:!0};function c(e,t,n){var r,s={},c=null,u=null;for(r in void 0!==n&&(c=""+n),void 0!==t.key&&(c=""+t.key),void 0!==t.ref&&(u=t.ref),t)o.call(t,r)&&!l.hasOwnProperty(r)&&(s[r]=t[r]);if(e&&e.defaultProps)for(r in t=e.defaultProps)void 0===s[r]&&(s[r]=t[r]);return{$$typeof:i,type:e,key:c,ref:u,props:s,_owner:a.current}}t.Fragment=s,t.jsx=c,t.jsxs=c},848:function(e,t,n){e.exports=n(20)},609:function(e){e.exports=window.React}},t={};function n(r){var i=t[r];if(void 0!==i)return i.exports;var s=t[r]={exports:{}};return e[r](s,s.exports,n),s.exports}n.n=function(e){var t=e&&e.__esModule?function(){return e.default}:function(){return e};return n.d(t,{a:t}),t},n.d=function(e,t){for(var r in t)n.o(t,r)&&!n.o(e,r)&&Object.defineProperty(e,r,{enumerable:!0,get:t[r]})},n.o=function(e,t){return Object.prototype.hasOwnProperty.call(e,t)},function(){n.d({},{_:function(){return ur}});var e,t,r,i,s,o,a,l,c,u,d,p=n(848),f=window.wp.components,h=window.wp.data,v=window.wp.domReady,g=n.n(v);void 0!==window.wp&&(null!==(t=null===(e=window.wp.editor)||void 0===e?void 0:e.PluginDocumentSettingPanel)&&void 0!==t||(null!==(i=null===(r=window.wp.editPost)||void 0===r?void 0:r.PluginDocumentSettingPanel)&&void 0!==i||(null===(s=window.wp.editSite)||void 0===s||s.PluginDocumentSettingPanel)),d=null!==(a=null===(o=window.wp.editor)||void 0===o?void 0:o.PluginSidebar)&&void 0!==a?a:null!==(c=null===(l=window.wp.editPost)||void 0===l?void 0:l.PluginSidebar)&&void 0!==c?c:null===(u=window.wp.editSite)||void 0===u?void 0:u.PluginSidebar);var y,m,w,b=window.wp.element,_=window.wp.i18n,x=window.wp.primitives,k=(0,p.jsx)(x.SVG,{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 24 24",children:(0,p.jsx)(x.Path,{fillRule:"evenodd",d:"M11.25 5h1.5v15h-1.5V5zM6 10h1.5v10H6V10zm12 4h-1.5v6H18v-6z",clipRule:"evenodd"})}),S=window.wp.plugins,P=function(){function e(){this._tkq=[],this.isLoaded=!1,this.isEnabled=!1,"undefined"!=typeof wpParselyTracksTelemetry&&(this.isEnabled=!0,this.loadTrackingLibrary())}return e.getInstance=function(){return window.wpParselyTelemetryInstance||Object.defineProperty(window,"wpParselyTelemetryInstance",{value:new e,writable:!1,configurable:!1,enumerable:!1}),window.wpParselyTelemetryInstance},e.prototype.loadTrackingLibrary=function(){var e=this,t=document.createElement("script");t.async=!0,t.src="//stats.wp.com/w.js",t.onload=function(){e.isLoaded=!0,e._tkq=window._tkq||[]},document.head.appendChild(t)},e.trackEvent=function(t){return n=this,r=arguments,s=function(t,n){var r;return void 0===n&&(n={}),function(e,t){var n,r,i,s,o={label:0,sent:function(){if(1&i[0])throw i[1];return i[1]},trys:[],ops:[]};return s={next:a(0),throw:a(1),return:a(2)},"function"==typeof Symbol&&(s[Symbol.iterator]=function(){return this}),s;function a(a){return function(l){return function(a){if(n)throw new TypeError("Generator is already executing.");for(;s&&(s=0,a[0]&&(o=0)),o;)try{if(n=1,r&&(i=2&a[0]?r.return:a[0]?r.throw||((i=r.return)&&i.call(r),0):r.next)&&!(i=i.call(r,a[1])).done)return i;switch(r=0,i&&(a=[2&a[0],i.value]),a[0]){case 0:case 1:i=a;break;case 4:return o.label++,{value:a[1],done:!1};case 5:o.label++,r=a[1],a=[0];continue;case 7:a=o.ops.pop(),o.trys.pop();continue;default:if(!((i=(i=o.trys).length>0&&i[i.length-1])||6!==a[0]&&2!==a[0])){o=0;continue}if(3===a[0]&&(!i||a[1]>i[0]&&a[1]=1e4&&(clearInterval(s),n("Telemetry library not loaded"))}),100);else n("Telemetry not enabled")}))},e.prototype.trackEvent=function(t,n){var r;this.isLoaded?(0!==t.indexOf(e.TRACKS_PREFIX)&&(t=e.TRACKS_PREFIX+t),this.isEventNameValid(t)?(n=this.prepareProperties(n),null===(r=this._tkq)||void 0===r||r.push(["recordEvent",t,n])):console.error("Error tracking event: Invalid event name")):console.error("Error tracking event: Telemetry not loaded")},e.prototype.isTelemetryEnabled=function(){return this.isEnabled},e.prototype.isProprietyValid=function(t){return e.PROPERTY_REGEX.test(t)},e.prototype.isEventNameValid=function(t){return e.EVENT_NAME_REGEX.test(t)},e.prototype.prepareProperties=function(e){return(e=this.sanitizeProperties(e)).parsely_version=wpParselyTracksTelemetry.version,wpParselyTracksTelemetry.user&&(e._ut=wpParselyTracksTelemetry.user.type,e._ui=wpParselyTracksTelemetry.user.id),wpParselyTracksTelemetry.vipgo_env&&(e.vipgo_env=wpParselyTracksTelemetry.vipgo_env),this.sanitizeProperties(e)},e.prototype.sanitizeProperties=function(e){var t=this,n={};return Object.keys(e).forEach((function(r){t.isProprietyValid(r)&&(n[r]=e[r])})),n},e.TRACKS_PREFIX="wpparsely_",e.EVENT_NAME_REGEX=/^(([a-z0-9]+)_){2}([a-z0-9_]+)$/,e.PROPERTY_REGEX=/^[a-z_][a-z0-9_]*$/,e}(),j=(P.trackEvent,function(){return(0,p.jsx)(f.SVG,{"aria-hidden":"true",version:"1.1",viewBox:"0 0 15 15",width:"15",height:"15",xmlns:"http://www.w3.org/2000/svg",children:(0,p.jsx)(f.Path,{d:"M0 14.0025V11.0025L7.5 3.5025L10.5 6.5025L3 14.0025H0ZM12 5.0025L13.56 3.4425C14.15 2.8525 14.15 1.9025 13.56 1.3225L12.68 0.4425C12.09 -0.1475 11.14 -0.1475 10.56 0.4425L9 2.0025L12 5.0025Z"})})}),T=function(e){var t=e.size,n=void 0===t?24:t,r=e.className,i=void 0===r?"wp-parsely-icon":r;return(0,p.jsxs)(f.SVG,{className:i,height:n,viewBox:"0 0 60 65",width:n,xmlns:"http://www.w3.org/2000/svg",children:[(0,p.jsx)(f.Path,{fill:"#5ba745",d:"M23.72,51.53c0-.18,0-.34-.06-.52a13.11,13.11,0,0,0-2.1-5.53A14.74,14.74,0,0,0,19.12,43c-.27-.21-.5-.11-.51.22l-.24,3.42c0,.33-.38.35-.49,0l-1.5-4.8a1.4,1.4,0,0,0-.77-.78,23.91,23.91,0,0,0-3.1-.84c-1.38-.24-3.39-.39-3.39-.39-.34,0-.45.21-.25.49l2.06,3.76c.2.27,0,.54-.29.33l-4.51-3.6a3.68,3.68,0,0,0-2.86-.48c-1,.16-2.44.46-2.44.46a.68.68,0,0,0-.39.25.73.73,0,0,0-.14.45S.41,43,.54,44a3.63,3.63,0,0,0,1.25,2.62L6.48,50c.28.2.09.49-.23.37l-4.18-.94c-.32-.12-.5,0-.4.37,0,0,.69,1.89,1.31,3.16a24,24,0,0,0,1.66,2.74,1.34,1.34,0,0,0,1,.52l5,.13c.33,0,.41.38.1.48L7.51,58c-.31.1-.34.35-.07.55a14.29,14.29,0,0,0,3.05,1.66,13.09,13.09,0,0,0,5.9.5,25.13,25.13,0,0,0,4.34-1,9.55,9.55,0,0,1-.08-1.2,9.32,9.32,0,0,1,3.07-6.91"}),(0,p.jsx)(f.Path,{fill:"#5ba745",d:"M59.7,41.53a.73.73,0,0,0-.14-.45.68.68,0,0,0-.39-.25s-1.43-.3-2.44-.46a3.64,3.64,0,0,0-2.86.48l-4.51,3.6c-.26.21-.49-.06-.29-.33l2.06-3.76c.2-.28.09-.49-.25-.49,0,0-2,.15-3.39.39a23.91,23.91,0,0,0-3.1.84,1.4,1.4,0,0,0-.77.78l-1.5,4.8c-.11.32-.48.3-.49,0l-.24-3.42c0-.33-.24-.43-.51-.22a14.74,14.74,0,0,0-2.44,2.47A13.11,13.11,0,0,0,36.34,51c0,.18,0,.34-.06.52a9.26,9.26,0,0,1,3,8.1,24.1,24.1,0,0,0,4.34,1,13.09,13.09,0,0,0,5.9-.5,14.29,14.29,0,0,0,3.05-1.66c.27-.2.24-.45-.07-.55l-3.22-1.17c-.31-.1-.23-.47.1-.48l5-.13a1.38,1.38,0,0,0,1-.52A24.6,24.6,0,0,0,57,52.92c.61-1.27,1.31-3.16,1.31-3.16.1-.33-.08-.49-.4-.37l-4.18.94c-.32.12-.51-.17-.23-.37l4.69-3.34A3.63,3.63,0,0,0,59.46,44c.13-1,.24-2.47.24-2.47"}),(0,p.jsx)(f.Path,{fill:"#5ba745",d:"M46.5,25.61c0-.53-.35-.72-.8-.43l-4.86,2.66c-.45.28-.56-.27-.23-.69l4.66-6.23a2,2,0,0,0,.28-1.68,36.51,36.51,0,0,0-2.19-4.89,34,34,0,0,0-2.81-3.94c-.33-.41-.74-.35-.91.16l-2.28,5.68c-.16.5-.6.48-.59-.05l.28-8.93a2.54,2.54,0,0,0-.66-1.64S35,4.27,33.88,3.27,30.78.69,30.78.69a1.29,1.29,0,0,0-1.54,0s-1.88,1.49-3.12,2.59-2.48,2.35-2.48,2.35A2.5,2.5,0,0,0,23,7.27l.27,8.93c0,.53-.41.55-.58.05l-2.29-5.69c-.17-.5-.57-.56-.91-.14a35.77,35.77,0,0,0-3,4.2,35.55,35.55,0,0,0-2,4.62,2,2,0,0,0,.27,1.67l4.67,6.24c.33.42.23,1-.22.69l-4.87-2.66c-.45-.29-.82-.1-.82.43a18.6,18.6,0,0,0,.83,5.07,20.16,20.16,0,0,0,5.37,7.77c3.19,3,5.93,7.8,7.45,11.08A9.6,9.6,0,0,1,30,49.09a9.31,9.31,0,0,1,2.86.45c1.52-3.28,4.26-8.11,7.44-11.09a20.46,20.46,0,0,0,5.09-7,19,19,0,0,0,1.11-5.82"}),(0,p.jsx)(f.Path,{fill:"#5ba745",d:"M36.12,58.44A6.12,6.12,0,1,1,30,52.32a6.11,6.11,0,0,1,6.12,6.12"})]})},L=function(){return L=Object.assign||function(e){for(var t,n=1,r=arguments.length;nhere.',"wp-parsely"):s.code===$.ParselySuggestionsApiOpenAiError||s.code===$.ParselySuggestionsApiOpenAiUnavailable?s.message=(0,_.__)("The Parse.ly API returned an internal server error. Please retry with a different input, or try again later.","wp-parsely"):s.code===$.HttpRequestFailed&&s.message.includes("cURL error 28")?s.message=(0,_.__)("The Parse.ly API did not respond in a timely manner. Please try again later.","wp-parsely"):s.code===$.ParselySuggestionsApiSchemaError?s.message=(0,_.__)("The Parse.ly API returned a validation error. Please try again with different parameters.","wp-parsely"):s.code===$.ParselySuggestionsApiNoData?s.message=(0,_.__)("The Parse.ly API couldn't find any relevant data to fulfill the request. Please retry with a different input.","wp-parsely"):s.code===$.ParselySuggestionsApiOpenAiSchema?s.message=(0,_.__)("The Parse.ly API returned an incorrect response. Please try again later.","wp-parsely"):s.code===$.ParselySuggestionsApiAuthUnavailable&&(s.message=(0,_.__)("The Parse.ly API is currently unavailable. Please try again later.","wp-parsely")),s}return re(t,e),t.prototype.Message=function(e){return void 0===e&&(e=null),[$.PluginCredentialsNotSetMessageDetected,$.PluginSettingsSiteIdNotSet,$.PluginSettingsApiSecretNotSet].includes(this.code)?K(e):(this.code===$.FetchError&&(this.hint=this.Hint((0,_.__)("This error can sometimes be caused by ad-blockers or browser tracking protections. Please add this site to any applicable allow lists and try again.","wp-parsely"))),this.code!==$.ParselyApiForbidden&&this.code!==$.ParselySuggestionsApiNoAuthentication||(this.hint=this.Hint((0,_.__)("Please ensure that the Site ID and API Secret given in the plugin's settings are correct.","wp-parsely"))),this.code===$.HttpRequestFailed&&(this.hint=this.Hint((0,_.__)("The Parse.ly API cannot be reached. Please verify that you are online.","wp-parsely"))),(0,p.jsx)(W,{className:null==e?void 0:e.className,testId:"error",children:"

".concat(this.message,"

").concat(this.hint?this.hint:"")}))},t.prototype.Hint=function(e){return'

'.concat((0,_.__)("Hint:","wp-parsely")," ").concat(e,"

")},t.prototype.createErrorSnackbar=function(){//.test(this.message)||(0,h.dispatch)("core/notices").createNotice("error",this.message,{type:"snackbar"})},t}(Error),se=function(e){var t=e.isDetectingEnabled,n=e.onLinkChange,r=e.onLinkRemove,i=e.onLinkAdd,s=e.debounceValue,o=void 0===s?500:s,a=(0,h.useSelect)((function(e){return{blocks:(0,e("core/block-editor").getBlocks)()}}),[]).blocks,l=(0,b.useRef)(a),c=(0,b.useRef)(t);return(0,b.useEffect)((function(){var e=(0,z.debounce)((function(){for(var t=[],s=0;s0)return r(e.innerBlocks,t[s].innerBlocks);if(JSON.stringify(e)!==JSON.stringify(t[s])){var o=t[s],a=i.parseFromString(e.attributes.content||"","text/html"),l=i.parseFromString((null==o?void 0:o.attributes.content)||"","text/html"),c=Array.from(a.querySelectorAll("a[data-smartlink]")),u=Array.from(l.querySelectorAll("a[data-smartlink]")),d=c.filter((function(e){return!u.some((function(t){return t.dataset.smartlink===e.dataset.smartlink}))})),p=u.filter((function(e){return!c.some((function(t){return t.dataset.smartlink===e.dataset.smartlink}))})),f=c.filter((function(e){var t=u.find((function(t){return t.dataset.smartlink===e.dataset.smartlink}));return t&&t.outerHTML!==e.outerHTML}));(d.length>0||p.length>0||f.length>0)&&n.push({block:e,prevBlock:o,addedLinks:d,removedLinks:p,changedLinks:f})}}}))};return r(e,t),n}(a,l.current);o.length>0&&(o.forEach((function(e){e.changedLinks.length>0&&n&&n(e),e.addedLinks.length>0&&i&&i(e),e.removedLinks.length>0&&r&&r(e)})),l.current=a)}),o);return e(t),function(){e.cancel()}}),[a,o,t,i,n,r]),null},oe=function(e){var t=e.value,n=e.onChange,r=e.max,i=e.min,s=e.suffix,o=e.size,a=e.label,l=e.initialPosition,c=e.disabled,u=e.className;return(0,p.jsxs)("div",{className:"parsely-inputrange-control ".concat(u||""),children:[(0,p.jsx)(f.__experimentalHeading,{className:"parsely-inputrange-control__label",level:3,children:a}),(0,p.jsxs)("div",{className:"parsely-inputrange-control__controls",children:[(0,p.jsx)(f.__experimentalNumberControl,{disabled:c,value:t,suffix:(0,p.jsx)(f.__experimentalInputControlSuffixWrapper,{children:s}),size:null!=o?o:"__unstable-large",min:i,max:r,onChange:function(e){var t=parseInt(e,10);isNaN(t)||n(t)}}),(0,p.jsx)(f.RangeControl,{disabled:c,value:t,showTooltip:!1,initialPosition:l,onChange:function(e){n(e)},withInputField:!1,min:i,max:r})]})]})},ae=function(e,t,n,r){return new(n||(n=Promise))((function(i,s){function o(e){try{l(r.next(e))}catch(e){s(e)}}function a(e){try{l(r.throw(e))}catch(e){s(e)}}function l(e){var t;e.done?i(e.value):(t=e.value,t instanceof n?t:new n((function(e){e(t)}))).then(o,a)}l((r=r.apply(e,t||[])).next())}))},le=function(e,t){var n,r,i,s,o={label:0,sent:function(){if(1&i[0])throw i[1];return i[1]},trys:[],ops:[]};return s={next:a(0),throw:a(1),return:a(2)},"function"==typeof Symbol&&(s[Symbol.iterator]=function(){return this}),s;function a(a){return function(l){return function(a){if(n)throw new TypeError("Generator is already executing.");for(;s&&(s=0,a[0]&&(o=0)),o;)try{if(n=1,r&&(i=2&a[0]?r.return:a[0]?r.throw||((i=r.return)&&i.call(r),0):r.next)&&!(i=i.call(r,a[1])).done)return i;switch(r=0,i&&(a=[2&a[0],i.value]),a[0]){case 0:case 1:i=a;break;case 4:return o.label++,{value:a[1],done:!1};case 5:o.label++,r=a[1],a=[0];continue;case 7:a=o.ops.pop(),o.trys.pop();continue;default:if(!((i=(i=o.trys).length>0&&i[i.length-1])||6!==a[0]&&2!==a[0])){o=0;continue}if(3===a[0]&&(!i||a[1]>i[0]&&a[1]0&&i[i.length-1])||6!==a[0]&&2!==a[0])){o=0;continue}if(3===a[0]&&(!i||a[1]>i[0]&&a[1]0&&i[i.length-1])||6!==a[0]&&2!==a[0])){o=0;continue}if(3===a[0]&&(!i||a[1]>i[0]&&a[1]0&&i[i.length-1])||6!==a[0]&&2!==a[0])){o=0;continue}if(3===a[0]&&(!i||a[1]>i[0]&&a[1]0&&i[i.length-1])||6!==a[0]&&2!==a[0])){o=0;continue}if(3===a[0]&&(!i||a[1]>i[0]&&a[1]

","\n\x3c!-- /wp:paragraph --\x3e");t&&h((0,Q.parse)(n))}),[s]),(0,p.jsxs)("div",{className:"smart-linking-review-suggestion",children:[(0,p.jsx)(f.KeyboardShortcuts,{shortcuts:{left:o,right:a,up:o,down:a}}),(0,p.jsx)("div",{className:"review-suggestion-post-title",children:null===(t=s.post_data)||void 0===t?void 0:t.title}),(0,p.jsxs)("div",{className:"review-suggestion-preview",children:[!(null===(n=s.post_data)||void 0===n?void 0:n.is_first_paragraph)&&(0,p.jsx)($e,{topOrBottom:"top"}),(0,p.jsx)(Ze,{block:d[0],link:s,useOriginalBlock:!0}),!(null===(r=s.post_data)||void 0===r?void 0:r.is_last_paragraph)&&(0,p.jsx)($e,{topOrBottom:"bottom"})]}),(0,p.jsx)(f.__experimentalDivider,{}),(0,p.jsx)(We,{link:s}),(0,p.jsxs)("div",{className:"review-controls",children:[(0,p.jsx)(f.Tooltip,{shortcut:"←",text:(0,_.__)("Previous","wp-parsely"),children:(0,p.jsx)(f.Button,{disabled:!l,className:"wp-parsely-review-suggestion-previous",onClick:o,icon:He,children:(0,_.__)("Previous","wp-parsely")})}),(0,p.jsx)("div",{className:"reviews-controls-middle",children:(0,p.jsx)(f.Button,{target:"_blank",href:(null===(i=s.post_data)||void 0===i?void 0:i.edit_link)+"&smart-link="+s.uid,variant:"secondary",onClick:function(){P.trackEvent("smart_linking_open_in_editor_pressed",{type:"inbound",uid:s.uid})},children:(0,_.__)("Open in the Editor","wp-parsely")})}),(0,p.jsx)(f.Tooltip,{shortcut:"→",text:(0,_.__)("Next","wp-parsely"),children:(0,p.jsxs)(f.Button,{disabled:!c,onClick:a,className:"wp-parsely-review-suggestion-next",children:[(0,_.__)("Next","wp-parsely"),(0,p.jsx)(X,{icon:ze})]})})]})]})},Ye=function(e){var t=e.size,n=void 0===t?24:t,r=e.className,i=void 0===r?"wp-parsely-icon":r;return(0,p.jsxs)(f.SVG,{xmlns:"http://www.w3.org/2000/svg",className:i,width:n,height:n,viewBox:"0 0 24 24",fill:"none",children:[(0,p.jsx)(f.Path,{d:"M8.18983 5.90381L8.83642 7.54325L10.4758 8.18983L8.83642 8.8364L8.18983 10.4759L7.54324 8.8364L5.90381 8.18983L7.54324 7.54325L8.18983 5.90381Z"}),(0,p.jsx)(f.Path,{d:"M15.048 5.90381L15.9101 8.08972L18.0961 8.95186L15.9101 9.81397L15.048 11.9999L14.1859 9.81397L12 8.95186L14.1859 8.08972L15.048 5.90381Z"}),(0,p.jsx)(f.Path,{d:"M11.238 10.4761L12.3157 13.2085L15.048 14.2861L12.3157 15.3638L11.238 18.0962L10.1603 15.3638L7.42798 14.2861L10.1603 13.2085L11.238 10.4761Z"})]})},Je=function(e,t,n){if(n||2===arguments.length)for(var r,i=0,s=t.length;ii.bottom)&&(n.scrollTop=r.offsetTop-n.offsetTop)}}}}),[t,l]);var u=function(){var e=document.querySelector(".smart-linking-review-sidebar-tabs [data-active-item]"),t=null==e?void 0:e.nextElementSibling;t||(t=document.querySelector('.smart-linking-review-sidebar-tabs [role="tab"]')),t&&t.click()},d=(0,p.jsxs)("span",{className:"smart-linking-menu-label",children:[(0,_.__)("NEW","wp-parsely"),(0,p.jsx)(Ye,{})]}),h=[];n&&n.length>0&&h.push({name:"outbound",title:(0,_.__)("Outbound","wp-parsely")}),r&&r.length>0&&h.push({name:"inbound",title:(0,_.__)("Inbound","wp-parsely")});var v="outbound";return h=h.filter((function(e){return"outbound"===e.name&&r&&0===r.length&&(e.title=(0,_.__)("Outbound Smart Links","wp-parsely"),v="outbound"),"inbound"===e.name&&n&&0===n.length&&(e.title=(0,_.__)("Inbound Smart Links","wp-parsely"),v="inbound"),e})),(0,p.jsxs)("div",{className:"smart-linking-review-sidebar",ref:s,children:[(0,p.jsx)(f.KeyboardShortcuts,{shortcuts:{tab:function(){return u()},"shift+tab":function(){return u()}}}),(0,p.jsx)(f.TabPanel,{className:"smart-linking-review-sidebar-tabs",initialTabName:v,tabs:h,onSelect:function(e){var t,s;"outbound"===e&&n&&n.length>0&&i(n[0]),"inbound"===e&&r&&r.length>0&&i(r[0]),P.trackEvent("smart_linking_modal_tab_selected",{tab:e,total_inbound:null!==(t=null==r?void 0:r.length)&&void 0!==t?t:0,total_outbound:null!==(s=null==n?void 0:n.length)&&void 0!==s?s:0})},children:function(e){return(0,p.jsxs)(p.Fragment,{children:["outbound"===e.name&&(0,p.jsx)(p.Fragment,{children:n&&0!==n.length?n.map((function(e,n){return(0,p.jsxs)(f.MenuItem,{ref:function(e){o.current[n]=e},className:(null==t?void 0:t.uid)===e.uid?"is-selected":"",role:"menuitemradio",isSelected:(null==t?void 0:t.uid)===e.uid,onClick:function(){return i(e)},children:[(0,p.jsx)("span",{className:"smart-linking-menu-item",children:e.text}),!e.applied&&d]},e.uid)})):(0,p.jsxs)(p.Fragment,{children:[" ",(0,_.__)("No outbound links found.","wp-parsely")]})}),"inbound"===e.name&&(0,p.jsxs)(p.Fragment,{children:[(0,p.jsx)("div",{className:"review-sidebar-tip",children:(0,_.__)("This section shows external posts that link back to the current post.","wp-parsely")}),r&&0!==r.length?r.map((function(e,r){var s;return(0,p.jsx)(f.MenuItem,{ref:function(e){o.current[(n?n.length:0)+r]=e},className:(null==t?void 0:t.uid)===e.uid?"is-selected":"",role:"menuitemradio",isSelected:(null==t?void 0:t.uid)===e.uid,onClick:function(){return i(e)},children:(0,p.jsx)("span",{className:"smart-linking-menu-item",children:null===(s=e.post_data)||void 0===s?void 0:s.title})},e.uid)})):(0,p.jsxs)(p.Fragment,{children:[" ",(0,_.__)("No inbound links found.","wp-parsely")]})]})]})}})]})},Xe=(0,p.jsx)(x.SVG,{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 24 24",children:(0,p.jsx)(x.Path,{d:"M12 13.06l3.712 3.713 1.061-1.06L13.061 12l3.712-3.712-1.06-1.06L12 10.938 8.288 7.227l-1.061 1.06L10.939 12l-3.712 3.712 1.06 1.061L12 13.061z"})}),et=(0,p.jsx)(x.SVG,{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 24 24",children:(0,p.jsx)(x.Path,{d:"M16.7 7.1l-6.3 8.5-3.3-2.5-.9 1.2 4.5 3.4L17.9 8z"})}),tt=function(e){var t,n,r,i,s=null===(t=e.link.match)||void 0===t?void 0:t.blockId,o=(0,h.useSelect)((function(e){var t=e("core/block-editor"),n=t.getBlock,r=t.getBlockParents;return s?{block:n(s),parents:r(s).map((function(e){return n(e)})).filter((function(e){return void 0!==e}))}:{block:void 0,parents:[]}}),[s]),a=o.block,l=o.parents;return a?(0,p.jsxs)("div",{className:"review-suggestions-breadcrumbs",children:[l.map((function(e,t){var n;return(0,p.jsxs)("span",{children:[(0,p.jsx)("span",{className:"breadcrumbs-parent-block",children:null===(n=(0,Q.getBlockType)(e.name))||void 0===n?void 0:n.title}),(0,p.jsx)("span",{className:"breadcrumbs-parent-separator",children:" / "})]},t)})),(0,p.jsxs)("span",{className:"breadcrumbs-current-block",children:[(0,p.jsx)("span",{className:"breadcrumbs-current-block-type",children:null===(n=(0,Q.getBlockType)(a.name))||void 0===n?void 0:n.title}),(null===(i=null===(r=a.attributes)||void 0===r?void 0:r.metadata)||void 0===i?void 0:i.name)&&(0,p.jsx)("span",{className:"breadcrumbs-current-block-name",children:a.attributes.metadata.name})]})]}):(0,p.jsx)(p.Fragment,{})},nt=function(e){var t,n=e.link,r=(0,b.useState)(n.href),i=r[0],s=r[1],o=(0,b.useState)(null===(t=n.destination)||void 0===t?void 0:t.post_type),a=o[0],l=o[1],c=(0,b.useRef)(null),u=(0,h.useDispatch)(Te).updateSmartLink;return(0,b.useEffect)((function(){n.destination?l(n.destination.post_type):(l((0,_.__)("External","wp-parsely")),De.getInstance().getPostTypeByURL(n.href).then((function(e){e&&l(e.post_type),n.destination=e,u(n)})))}),[n,u]),(0,b.useEffect)((function(){var e=function(){if(c.current){var e=c.current.offsetWidth,t=Math.floor(e/8);s(function(e,t){var n=e.replace(/(^\w+:|^)\/\//,"").replace(/^www\./,"");if(!t||n.length<=t)return n;var r=n.split("/")[0],i=n.substring(r.length);t-=r.length;var s=Math.floor((t-3)/2),o=i.substring(0,s),a=i.substring(i.length-s);return"".concat(r).concat(o,"...").concat(a)}(n.href,t))}};return e(),window.addEventListener("resize",e),function(){window.removeEventListener("resize",e)}}),[n]),(0,p.jsx)(f.MenuItem,{ref:c,info:i,iconPosition:"left",icon:Ge,shortcut:a,className:"block-editor-link-control__search-item wp-parsely-link-suggestion-link-details",children:n.title})},rt=function(e){var t=e.link,n=e.onNext,r=e.onPrevious,i=e.onAccept,s=e.onReject,o=e.onRemove,a=e.onSelectInEditor,l=e.hasPrevious,c=e.hasNext;if(t&&void 0!==t.post_data)return(0,p.jsx)(Ke,{link:t,onNext:n,onPrevious:r,onAccept:i,onReject:s,onRemove:o,onSelectInEditor:a,hasPrevious:l,hasNext:c});if(!(null==t?void 0:t.match))return(0,p.jsx)(p.Fragment,{children:(0,_.__)("This Smart Link does not have any matches in the current content.","wp-parsely")});var u=t.match.blockId,d=(0,h.select)("core/block-editor").getBlock(u),v=t.applied;return d?(0,p.jsxs)("div",{className:"smart-linking-review-suggestion",children:[(0,p.jsx)(f.KeyboardShortcuts,{shortcuts:{left:r,right:n,up:r,down:n,a:function(){t&&!t.applied&&i()},r:function(){t&&(t.applied?o():s())}}}),(0,p.jsx)(tt,{link:t}),(0,p.jsx)("div",{className:"review-suggestion-preview",children:(0,p.jsx)(Ze,{block:d,link:t})}),(0,p.jsx)(f.__experimentalDivider,{}),(0,p.jsx)(nt,{link:t}),(0,p.jsxs)("div",{className:"review-controls",children:[(0,p.jsx)(f.Tooltip,{shortcut:"←",text:(0,_.__)("Previous","wp-parsely"),children:(0,p.jsx)(f.Button,{disabled:!l,className:"wp-parsely-review-suggestion-previous",onClick:r,icon:He,children:(0,_.__)("Previous","wp-parsely")})}),(0,p.jsxs)("div",{className:"reviews-controls-middle",children:[!v&&(0,p.jsxs)(p.Fragment,{children:[(0,p.jsx)(f.Tooltip,{shortcut:"R",text:(0,_.__)("Reject","wp-parsely"),children:(0,p.jsx)(f.Button,{className:"wp-parsely-review-suggestion-reject",icon:Xe,onClick:s,variant:"secondary",children:(0,_.__)("Reject","wp-parsely")})}),(0,p.jsx)(f.Tooltip,{shortcut:"A",text:(0,_.__)("Accept","wp-parsely"),children:(0,p.jsx)(f.Button,{className:"wp-parsely-review-suggestion-accept",icon:et,onClick:i,variant:"secondary",children:(0,_.__)("Accept","wp-parsely")})})]}),v&&(0,p.jsxs)(p.Fragment,{children:[(0,p.jsx)(f.Tooltip,{shortcut:"R",text:(0,_.__)("Remove","wp-parsely"),children:(0,p.jsx)(f.Button,{className:"wp-parsely-review-suggestion-reject",icon:Xe,onClick:o,variant:"secondary",children:(0,_.__)("Remove","wp-parsely")})}),(0,p.jsx)(f.Button,{className:"wp-parsely-review-suggestion-accept",onClick:a,variant:"secondary",children:(0,_.__)("Select in Editor","wp-parsely")})]})]}),(0,p.jsx)(f.Tooltip,{shortcut:"→",text:(0,_.__)("Next","wp-parsely"),children:(0,p.jsxs)(f.Button,{disabled:!c,onClick:n,className:"wp-parsely-review-suggestion-next",children:[(0,_.__)("Next","wp-parsely"),(0,p.jsx)(X,{icon:ze})]})})]})]}):(0,p.jsx)(p.Fragment,{children:(0,_.__)("No block is selected.","wp-parsely")})},it=function(e,t,n,r){return new(n||(n=Promise))((function(i,s){function o(e){try{l(r.next(e))}catch(e){s(e)}}function a(e){try{l(r.throw(e))}catch(e){s(e)}}function l(e){var t;e.done?i(e.value):(t=e.value,t instanceof n?t:new n((function(e){e(t)}))).then(o,a)}l((r=r.apply(e,t||[])).next())}))},st=function(e,t){var n,r,i,s,o={label:0,sent:function(){if(1&i[0])throw i[1];return i[1]},trys:[],ops:[]};return s={next:a(0),throw:a(1),return:a(2)},"function"==typeof Symbol&&(s[Symbol.iterator]=function(){return this}),s;function a(a){return function(l){return function(a){if(n)throw new TypeError("Generator is already executing.");for(;s&&(s=0,a[0]&&(o=0)),o;)try{if(n=1,r&&(i=2&a[0]?r.return:a[0]?r.throw||((i=r.return)&&i.call(r),0):r.next)&&!(i=i.call(r,a[1])).done)return i;switch(r=0,i&&(a=[2&a[0],i.value]),a[0]){case 0:case 1:i=a;break;case 4:return o.label++,{value:a[1],done:!1};case 5:o.label++,r=a[1],a=[0];continue;case 7:a=o.ops.pop(),o.trys.pop();continue;default:if(!((i=(i=o.trys).length>0&&i[i.length-1])||6!==a[0]&&2!==a[0])){o=0;continue}if(3===a[0]&&(!i||a[1]>i[0]&&a[1]0&&(a=o[0],(l=a.parentNode)&&(c=document.createTextNode(null!==(u=a.textContent)&&void 0!==u?u:""),l.replaceChild(c,a),te.updateBlockAttributes(n,{content:s.innerHTML}))),[4,E(t.uid)]):[2]):[2];case 1:return d.sent(),[2]}}))}))},C=(0,b.useCallback)((function(){c(!1),w().filter((function(e){return!e.applied})).length>0?o(!0):(ne.unlockPostAutosaving("smart-linking-review-modal"),t())}),[w,t]),A=function(e){o(!1),e?(c(!1),T().then((function(){C()}))):c(!0)},O=function(){if(ue(k)){var e=g.indexOf(k);if(!g[t=e+1])return;S(g[t])}else{var t;if(e=v.indexOf(k),!v[t=e+1])return;S(v[t])}},R=function(){if(ue(k)){var e=g.indexOf(k);if(!g[t=e-1])return;S(g[t])}else{var t;if(e=v.indexOf(k),!v[t=e-1])return;S(v[t])}};return(0,b.useEffect)((function(){l?ne.lockPostAutosaving("smart-linking-review-modal"):l&&0===d.length&&C()}),[l,t,d,C]),(0,b.useEffect)((function(){c(n)}),[n]),(0,p.jsxs)(p.Fragment,{children:[l&&(0,p.jsx)(f.Modal,{title:(0,_.__)("Review Smart Links","wp-parsely"),className:"wp-parsely-smart-linking-review-modal",onRequestClose:C,shouldCloseOnClickOutside:!1,shouldCloseOnEsc:!1,children:(0,p.jsxs)("div",{className:"smart-linking-modal-body",children:[(0,p.jsx)(Qe,{outboundLinks:v,inboundLinks:g,activeLink:k,setSelectedLink:S}),k&&(ue(k)?(0,p.jsx)(Ke,{link:k,onNext:O,onPrevious:R,hasNext:g.indexOf(k)0}):(0,p.jsx)(rt,{link:k,hasNext:m().indexOf(k)0,onNext:O,onPrevious:R,onAccept:function(){return it(void 0,void 0,void 0,(function(){var e,t;return st(this,(function(n){switch(n.label){case 0:return k.match?(r(k),[4,(i=k.match.blockId,s=k,it(void 0,void 0,void 0,(function(){var e,t;return st(this,(function(n){switch(n.label){case 0:return(e=document.createElement("a")).href=s.href,e.title=s.title,e.setAttribute("data-smartlink",s.uid),(t=(0,h.select)("core/block-editor").getBlock(i))?(fe(t,s,e),s.applied=!0,[4,L(s)]):[2];case 1:return n.sent(),[2]}}))})))]):[2];case 1:return n.sent(),P.trackEvent("smart_linking_link_accepted",{link:k.href,title:k.title,text:k.text,uid:k.uid}),0===y().length?(C(),[2]):(e=v.indexOf(k),v[t=e+1]?S(v[t]):S(v[0]),[2])}var i,s}))}))},onReject:function(){return it(void 0,void 0,void 0,(function(){var e,t;return st(this,(function(n){switch(n.label){case 0:return e=v.indexOf(k),v[t=e+1]?S(v[t]):v[0]?S(v[0]):C(),[4,E(k.uid)];case 1:return n.sent(),P.trackEvent("smart_linking_link_rejected",{link:k.href,title:k.title,text:k.text,uid:k.uid}),[2]}}))}))},onRemove:function(){return it(void 0,void 0,void 0,(function(){var e,t,n,r;return st(this,(function(i){switch(i.label){case 0:return k.match?(e=(0,h.select)("core/block-editor").getBlock(k.match.blockId))?(t=m(),n=t.indexOf(k),r=n-1,[4,N(e,k)]):[3,2]:[2];case 1:if(i.sent(),P.trackEvent("smart_linking_link_removed",{link:k.href,title:k.title,text:k.text,uid:k.uid}),0===(t=m()).length&&g.length>0)return S(g[0]),[2];if(0===t.length&&0===g.length)return C(),[2];if(t[r])return S(t[r]),[2];S(t[0]),i.label=2;case 2:return[2]}}))}))},onSelectInEditor:function(){if(k.match){var e=(0,h.select)("core/block-editor").getBlock(k.match.blockId);if(e){te.selectBlock(e.clientId);var t=document.querySelector('[data-block="'.concat(e.clientId,'"]'));t&&ke(t,k.uid),P.trackEvent("smart_linking_select_in_editor_pressed",{type:"outbound",uid:k.uid}),C()}}}}))]})}),s&&(0,p.jsxs)(f.Modal,{title:(0,_.__)("Review Smart Links","wp-parsely"),onRequestClose:function(){return A(!1)},className:"wp-parsely-smart-linking-close-dialog",children:[(0,_.__)("Are you sure you want to close? All un-accepted smart links will not be added.","wp-parsely"),(0,p.jsxs)("div",{className:"smart-linking-close-dialog-actions",children:[(0,p.jsx)(f.Button,{variant:"secondary",onClick:function(){return A(!1)},children:(0,_.__)("Go Back","wp-parsely")}),(0,p.jsx)(f.Button,{variant:"primary",onClick:function(){return A(!0)},children:(0,_.__)("Close","wp-parsely")})]})]})]})})),at=function(){return at=Object.assign||function(e){for(var t,n=1,r=arguments.length;n0&&i[i.length-1])||6!==a[0]&&2!==a[0])){o=0;continue}if(3===a[0]&&(!i||a[1]>i[0]&&a[1]0&&k("success",/* translators: %d: number of smart links applied */ /* translators: %d: number of smart links applied */ +/* translators: %s: block name */n((0,_.sprintf)((0,_.__)("%s blocks are not supported for Smart Links.","wp-parsely"),s))}T(_e.All===C)}}),[d,S,C,r,i,y,E,T,c]),(0,b.useEffect)((function(){if(!r&&o.current&&C&&!k&&y){var e=o.current.querySelector('button[data-value="'.concat(C,'"]'));e&&"true"!==e.getAttribute("aria-checked")&&(E(C),L(!0))}}),[y,x,r,S]),(0,b.useEffect)((function(){c(null)}),[y]),(0,p.jsx)("div",{className:"parsely-panel-settings",children:(0,p.jsxs)("div",{className:"parsely-panel-settings-body",children:[(0,p.jsxs)("div",{className:"smart-linking-block-select",children:[(0,p.jsx)(f.Disabled,{isDisabled:r,children:(0,p.jsxs)(f.__experimentalToggleGroupControl,{ref:o,__nextHasNoMarginBottom:!0,__next40pxDefaultSize:!0,isBlock:!0,value:C,label:(0,_.__)("Apply Smart Links to","wp-parsely"),onChange:function(e){return Ee(void 0,void 0,void 0,(function(){return Ne(this,(function(t){switch(t.label){case 0:return r?[2]:(v(!0),[4,T(_e.All===e)]);case 1:return t.sent(),[4,E(e)];case 2:return t.sent(),setTimeout((function(){v(!1)}),500),[2]}}))}))},children:[(0,p.jsx)(f.__experimentalToggleGroupControlOption,{label:(0,_.__)("Selected Block","wp-parsely"),value:"selected"}),(0,p.jsx)(f.__experimentalToggleGroupControlOption,{label:(0,_.__)("All Blocks","wp-parsely"),value:"all"})]})}),l&&(0,p.jsxs)("div",{className:"wp-parsely-smart-linking-hint",children:[(0,p.jsx)("strong",{children:(0,_.__)("Hint:","wp-parsely")})," ",l]})]}),(0,p.jsx)("div",{className:"smart-linking-settings",children:(0,p.jsx)(oe,{value:w,onChange:function(e){j(null!=e?e:1),s("MaxLinks",null!=e?e:mt)},label:(0,_.__)("Target Number of Links","wp-parsely"),suffix:(0,_.__)("Links","wp-parsely"),min:1,max:20,initialPosition:w,disabled:r})})]})})},Ae=window.wp.editor,Oe=window.wp.url,Re=function(){function e(){this.abortControllers=new Map}return e.prototype.cancelRequest=function(e){if(e)(t=this.abortControllers.get(e))&&(t.abort(),this.abortControllers.delete(e));else{var t,n=Array.from(this.abortControllers.keys()).pop();n&&(t=this.abortControllers.get(n))&&(t.abort(),this.abortControllers.delete(n))}},e.prototype.cancelAll=function(){this.abortControllers.forEach((function(e){return e.abort()})),this.abortControllers.clear()},e.prototype.getOrCreateController=function(e){if(e&&this.abortControllers.has(e))return{abortController:this.abortControllers.get(e),abortId:e};var t=null!=e?e:"auto-"+Date.now(),n=new AbortController;return this.abortControllers.set(t,n),{abortController:n,abortId:t}},e.prototype.fetch=function(e,t){return n=this,r=void 0,s=function(){var n,r,i,s,o,a;return function(e,t){var n,r,i,s,o={label:0,sent:function(){if(1&i[0])throw i[1];return i[1]},trys:[],ops:[]};return s={next:a(0),throw:a(1),return:a(2)},"function"==typeof Symbol&&(s[Symbol.iterator]=function(){return this}),s;function a(a){return function(l){return function(a){if(n)throw new TypeError("Generator is already executing.");for(;s&&(s=0,a[0]&&(o=0)),o;)try{if(n=1,r&&(i=2&a[0]?r.return:a[0]?r.throw||((i=r.return)&&i.call(r),0):r.next)&&!(i=i.call(r,a[1])).done)return i;switch(r=0,i&&(a=[2&a[0],i.value]),a[0]){case 0:case 1:i=a;break;case 4:return o.label++,{value:a[1],done:!1};case 5:o.label++,r=a[1],a=[0];continue;case 7:a=o.ops.pop(),o.trys.pop();continue;default:if(!((i=(i=o.trys).length>0&&i[i.length-1])||6!==a[0]&&2!==a[0])){o=0;continue}if(3===a[0]&&(!i||a[1]>i[0]&&a[1]0&&i[i.length-1])||6!==a[0]&&2!==a[0])){o=0;continue}if(3===a[0]&&(!i||a[1]>i[0]&&a[1]0&&i[i.length-1])||6!==a[0]&&2!==a[0])){o=0;continue}if(3===a[0]&&(!i||a[1]>i[0]&&a[1]

","\n\x3c!-- /wp:paragraph --\x3e");t&&h((0,Q.parse)(n))}),[s]),(0,p.jsxs)("div",{className:"smart-linking-review-suggestion",children:[(0,p.jsx)(f.KeyboardShortcuts,{shortcuts:{left:o,right:a,up:o,down:a}}),(0,p.jsx)("div",{className:"review-suggestion-post-title",children:null===(t=s.post_data)||void 0===t?void 0:t.title}),(0,p.jsxs)("div",{className:"review-suggestion-preview",children:[!(null===(n=s.post_data)||void 0===n?void 0:n.is_first_paragraph)&&(0,p.jsx)($e,{topOrBottom:"top"}),(0,p.jsx)(Ze,{block:d[0],link:s,useOriginalBlock:!0}),!(null===(r=s.post_data)||void 0===r?void 0:r.is_last_paragraph)&&(0,p.jsx)($e,{topOrBottom:"bottom"})]}),(0,p.jsx)(f.__experimentalDivider,{}),(0,p.jsx)(We,{link:s}),(0,p.jsxs)("div",{className:"review-controls",children:[(0,p.jsx)(f.Tooltip,{shortcut:"←",text:(0,_.__)("Previous","wp-parsely"),children:(0,p.jsx)(f.Button,{disabled:!l,className:"wp-parsely-review-suggestion-previous",onClick:o,icon:He,children:(0,_.__)("Previous","wp-parsely")})}),(0,p.jsx)("div",{className:"reviews-controls-middle",children:(0,p.jsx)(f.Button,{target:"_blank",href:(null===(i=s.post_data)||void 0===i?void 0:i.edit_link)+"&smart-link="+s.uid,variant:"secondary",onClick:function(){P.trackEvent("smart_linking_open_in_editor_pressed",{type:"inbound",uid:s.uid})},children:(0,_.__)("Open in the Editor","wp-parsely")})}),(0,p.jsx)(f.Tooltip,{shortcut:"→",text:(0,_.__)("Next","wp-parsely"),children:(0,p.jsxs)(f.Button,{disabled:!c,onClick:a,className:"wp-parsely-review-suggestion-next",children:[(0,_.__)("Next","wp-parsely"),(0,p.jsx)(X,{icon:ze})]})})]})]})},Ye=function(e){var t=e.size,n=void 0===t?24:t,r=e.className,i=void 0===r?"wp-parsely-icon":r;return(0,p.jsxs)(f.SVG,{xmlns:"http://www.w3.org/2000/svg",className:i,width:n,height:n,viewBox:"0 0 24 24",fill:"none",children:[(0,p.jsx)(f.Path,{d:"M8.18983 5.90381L8.83642 7.54325L10.4758 8.18983L8.83642 8.8364L8.18983 10.4759L7.54324 8.8364L5.90381 8.18983L7.54324 7.54325L8.18983 5.90381Z"}),(0,p.jsx)(f.Path,{d:"M15.048 5.90381L15.9101 8.08972L18.0961 8.95186L15.9101 9.81397L15.048 11.9999L14.1859 9.81397L12 8.95186L14.1859 8.08972L15.048 5.90381Z"}),(0,p.jsx)(f.Path,{d:"M11.238 10.4761L12.3157 13.2085L15.048 14.2861L12.3157 15.3638L11.238 18.0962L10.1603 15.3638L7.42798 14.2861L10.1603 13.2085L11.238 10.4761Z"})]})},Je=function(e,t,n){if(n||2===arguments.length)for(var r,i=0,s=t.length;ii.bottom)&&(n.scrollTop=r.offsetTop-n.offsetTop)}}}}),[t,l]);var u=function(){var e=document.querySelector(".smart-linking-review-sidebar-tabs [data-active-item]"),t=null==e?void 0:e.nextElementSibling;t||(t=document.querySelector('.smart-linking-review-sidebar-tabs [role="tab"]')),t&&t.click()},d=(0,p.jsxs)("span",{className:"smart-linking-menu-label",children:[(0,_.__)("NEW","wp-parsely"),(0,p.jsx)(Ye,{})]}),h=[];n&&n.length>0&&h.push({name:"outbound",title:(0,_.__)("Outbound","wp-parsely")}),r&&r.length>0&&h.push({name:"inbound",title:(0,_.__)("Inbound","wp-parsely")});var v="outbound";return h=h.filter((function(e){return"outbound"===e.name&&r&&0===r.length&&(e.title=(0,_.__)("Outbound Smart Links","wp-parsely"),v="outbound"),"inbound"===e.name&&n&&0===n.length&&(e.title=(0,_.__)("Inbound Smart Links","wp-parsely"),v="inbound"),e})),(0,p.jsxs)("div",{className:"smart-linking-review-sidebar",ref:s,children:[(0,p.jsx)(f.KeyboardShortcuts,{shortcuts:{tab:function(){return u()},"shift+tab":function(){return u()}}}),(0,p.jsx)(f.TabPanel,{className:"smart-linking-review-sidebar-tabs",initialTabName:v,tabs:h,onSelect:function(e){var t,s;"outbound"===e&&n&&n.length>0&&i(n[0]),"inbound"===e&&r&&r.length>0&&i(r[0]),P.trackEvent("smart_linking_modal_tab_selected",{tab:e,total_inbound:null!==(t=null==r?void 0:r.length)&&void 0!==t?t:0,total_outbound:null!==(s=null==n?void 0:n.length)&&void 0!==s?s:0})},children:function(e){return(0,p.jsxs)(p.Fragment,{children:["outbound"===e.name&&(0,p.jsx)(p.Fragment,{children:n&&0!==n.length?n.map((function(e,n){return(0,p.jsxs)(f.MenuItem,{ref:function(e){o.current[n]=e},className:(null==t?void 0:t.uid)===e.uid?"is-selected":"",role:"menuitemradio",isSelected:(null==t?void 0:t.uid)===e.uid,onClick:function(){return i(e)},children:[(0,p.jsx)("span",{className:"smart-linking-menu-item",children:e.text}),!e.applied&&d]},e.uid)})):(0,p.jsxs)(p.Fragment,{children:[" ",(0,_.__)("No outbound links found.","wp-parsely")]})}),"inbound"===e.name&&(0,p.jsxs)(p.Fragment,{children:[(0,p.jsx)("div",{className:"review-sidebar-tip",children:(0,_.__)("This section shows external posts that link back to the current post.","wp-parsely")}),r&&0!==r.length?r.map((function(e,r){var s;return(0,p.jsx)(f.MenuItem,{ref:function(e){o.current[(n?n.length:0)+r]=e},className:(null==t?void 0:t.uid)===e.uid?"is-selected":"",role:"menuitemradio",isSelected:(null==t?void 0:t.uid)===e.uid,onClick:function(){return i(e)},children:(0,p.jsx)("span",{className:"smart-linking-menu-item",children:null===(s=e.post_data)||void 0===s?void 0:s.title})},e.uid)})):(0,p.jsxs)(p.Fragment,{children:[" ",(0,_.__)("No inbound links found.","wp-parsely")]})]})]})}})]})},Xe=(0,p.jsx)(x.SVG,{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 24 24",children:(0,p.jsx)(x.Path,{d:"M12 13.06l3.712 3.713 1.061-1.06L13.061 12l3.712-3.712-1.06-1.06L12 10.938 8.288 7.227l-1.061 1.06L10.939 12l-3.712 3.712 1.06 1.061L12 13.061z"})}),et=(0,p.jsx)(x.SVG,{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 24 24",children:(0,p.jsx)(x.Path,{d:"M16.7 7.1l-6.3 8.5-3.3-2.5-.9 1.2 4.5 3.4L17.9 8z"})}),tt=function(e){var t,n,r,i,s=null===(t=e.link.match)||void 0===t?void 0:t.blockId,o=(0,h.useSelect)((function(e){var t=e("core/block-editor"),n=t.getBlock,r=t.getBlockParents;return s?{block:n(s),parents:r(s).map((function(e){return n(e)})).filter((function(e){return void 0!==e}))}:{block:void 0,parents:[]}}),[s]),a=o.block,l=o.parents;return a?(0,p.jsxs)("div",{className:"review-suggestions-breadcrumbs",children:[l.map((function(e,t){var n;return(0,p.jsxs)("span",{children:[(0,p.jsx)("span",{className:"breadcrumbs-parent-block",children:null===(n=(0,Q.getBlockType)(e.name))||void 0===n?void 0:n.title}),(0,p.jsx)("span",{className:"breadcrumbs-parent-separator",children:" / "})]},t)})),(0,p.jsxs)("span",{className:"breadcrumbs-current-block",children:[(0,p.jsx)("span",{className:"breadcrumbs-current-block-type",children:null===(n=(0,Q.getBlockType)(a.name))||void 0===n?void 0:n.title}),(null===(i=null===(r=a.attributes)||void 0===r?void 0:r.metadata)||void 0===i?void 0:i.name)&&(0,p.jsx)("span",{className:"breadcrumbs-current-block-name",children:a.attributes.metadata.name})]})]}):(0,p.jsx)(p.Fragment,{})},nt=function(e){var t,n=e.link,r=(0,b.useState)(n.href),i=r[0],s=r[1],o=(0,b.useState)(null===(t=n.destination)||void 0===t?void 0:t.post_type),a=o[0],l=o[1],c=(0,b.useRef)(null),u=(0,h.useDispatch)(Te).updateSmartLink;return(0,b.useEffect)((function(){n.destination?l(n.destination.post_type):(l((0,_.__)("External","wp-parsely")),De.getInstance().getPostTypeByURL(n.href).then((function(e){e&&l(e.post_type),n.destination=e,u(n)})))}),[n,u]),(0,b.useEffect)((function(){var e=function(){if(c.current){var e=c.current.offsetWidth,t=Math.floor(e/8);s(function(e,t){var n=e.replace(/(^\w+:|^)\/\//,"").replace(/^www\./,"");if(!t||n.length<=t)return n;var r=n.split("/")[0],i=n.substring(r.length);t-=r.length;var s=Math.floor((t-3)/2),o=i.substring(0,s),a=i.substring(i.length-s);return"".concat(r).concat(o,"...").concat(a)}(n.href,t))}};return e(),window.addEventListener("resize",e),function(){window.removeEventListener("resize",e)}}),[n]),(0,p.jsx)(f.MenuItem,{ref:c,info:i,iconPosition:"left",icon:Ge,shortcut:a,className:"block-editor-link-control__search-item wp-parsely-link-suggestion-link-details",children:n.title})},rt=function(e){var t=e.link,n=e.onNext,r=e.onPrevious,i=e.onAccept,s=e.onReject,o=e.onRemove,a=e.onSelectInEditor,l=e.hasPrevious,c=e.hasNext;if(t&&void 0!==t.post_data)return(0,p.jsx)(Ke,{link:t,onNext:n,onPrevious:r,onAccept:i,onReject:s,onRemove:o,onSelectInEditor:a,hasPrevious:l,hasNext:c});if(!(null==t?void 0:t.match))return(0,p.jsx)(p.Fragment,{children:(0,_.__)("This Smart Link does not have any matches in the current content.","wp-parsely")});var u=t.match.blockId,d=(0,h.select)("core/block-editor").getBlock(u),v=t.applied;return d?(0,p.jsxs)("div",{className:"smart-linking-review-suggestion",children:[(0,p.jsx)(f.KeyboardShortcuts,{shortcuts:{left:r,right:n,up:r,down:n,a:function(){t&&!t.applied&&i()},r:function(){t&&(t.applied?o():s())}}}),(0,p.jsx)(tt,{link:t}),(0,p.jsx)("div",{className:"review-suggestion-preview",children:(0,p.jsx)(Ze,{block:d,link:t})}),(0,p.jsx)(f.__experimentalDivider,{}),(0,p.jsx)(nt,{link:t}),(0,p.jsxs)("div",{className:"review-controls",children:[(0,p.jsx)(f.Tooltip,{shortcut:"←",text:(0,_.__)("Previous","wp-parsely"),children:(0,p.jsx)(f.Button,{disabled:!l,className:"wp-parsely-review-suggestion-previous",onClick:r,icon:He,children:(0,_.__)("Previous","wp-parsely")})}),(0,p.jsxs)("div",{className:"reviews-controls-middle",children:[!v&&(0,p.jsxs)(p.Fragment,{children:[(0,p.jsx)(f.Tooltip,{shortcut:"R",text:(0,_.__)("Reject","wp-parsely"),children:(0,p.jsx)(f.Button,{className:"wp-parsely-review-suggestion-reject",icon:Xe,onClick:s,variant:"secondary",children:(0,_.__)("Reject","wp-parsely")})}),(0,p.jsx)(f.Tooltip,{shortcut:"A",text:(0,_.__)("Accept","wp-parsely"),children:(0,p.jsx)(f.Button,{className:"wp-parsely-review-suggestion-accept",icon:et,onClick:i,variant:"secondary",children:(0,_.__)("Accept","wp-parsely")})})]}),v&&(0,p.jsxs)(p.Fragment,{children:[(0,p.jsx)(f.Tooltip,{shortcut:"R",text:(0,_.__)("Remove","wp-parsely"),children:(0,p.jsx)(f.Button,{className:"wp-parsely-review-suggestion-reject",icon:Xe,onClick:o,variant:"secondary",children:(0,_.__)("Remove","wp-parsely")})}),(0,p.jsx)(f.Button,{className:"wp-parsely-review-suggestion-accept",onClick:a,variant:"secondary",children:(0,_.__)("Select in Editor","wp-parsely")})]})]}),(0,p.jsx)(f.Tooltip,{shortcut:"→",text:(0,_.__)("Next","wp-parsely"),children:(0,p.jsxs)(f.Button,{disabled:!c,onClick:n,className:"wp-parsely-review-suggestion-next",children:[(0,_.__)("Next","wp-parsely"),(0,p.jsx)(X,{icon:ze})]})})]})]}):(0,p.jsx)(p.Fragment,{children:(0,_.__)("No block is selected.","wp-parsely")})},it=function(e,t,n,r){return new(n||(n=Promise))((function(i,s){function o(e){try{l(r.next(e))}catch(e){s(e)}}function a(e){try{l(r.throw(e))}catch(e){s(e)}}function l(e){var t;e.done?i(e.value):(t=e.value,t instanceof n?t:new n((function(e){e(t)}))).then(o,a)}l((r=r.apply(e,t||[])).next())}))},st=function(e,t){var n,r,i,s,o={label:0,sent:function(){if(1&i[0])throw i[1];return i[1]},trys:[],ops:[]};return s={next:a(0),throw:a(1),return:a(2)},"function"==typeof Symbol&&(s[Symbol.iterator]=function(){return this}),s;function a(a){return function(l){return function(a){if(n)throw new TypeError("Generator is already executing.");for(;s&&(s=0,a[0]&&(o=0)),o;)try{if(n=1,r&&(i=2&a[0]?r.return:a[0]?r.throw||((i=r.return)&&i.call(r),0):r.next)&&!(i=i.call(r,a[1])).done)return i;switch(r=0,i&&(a=[2&a[0],i.value]),a[0]){case 0:case 1:i=a;break;case 4:return o.label++,{value:a[1],done:!1};case 5:o.label++,r=a[1],a=[0];continue;case 7:a=o.ops.pop(),o.trys.pop();continue;default:if(!((i=(i=o.trys).length>0&&i[i.length-1])||6!==a[0]&&2!==a[0])){o=0;continue}if(3===a[0]&&(!i||a[1]>i[0]&&a[1]0&&(a=o[0],(l=a.parentNode)&&(c=document.createTextNode(null!==(u=a.textContent)&&void 0!==u?u:""),l.replaceChild(c,a),te.updateBlockAttributes(n,{content:s.innerHTML}))),[4,E(t.uid)]):[2]):[2];case 1:return d.sent(),[2]}}))}))},C=(0,b.useCallback)((function(){c(!1),w().filter((function(e){return!e.applied})).length>0?o(!0):(ne.unlockPostAutosaving("smart-linking-review-modal"),t())}),[w,t]),A=function(e){o(!1),e?(c(!1),T().then((function(){C()}))):c(!0)},O=function(){if(ue(k)){var e=g.indexOf(k);if(!g[t=e+1])return;S(g[t])}else{var t;if(e=v.indexOf(k),!v[t=e+1])return;S(v[t])}},R=function(){if(ue(k)){var e=g.indexOf(k);if(!g[t=e-1])return;S(g[t])}else{var t;if(e=v.indexOf(k),!v[t=e-1])return;S(v[t])}};return(0,b.useEffect)((function(){l?ne.lockPostAutosaving("smart-linking-review-modal"):l&&0===d.length&&C()}),[l,t,d,C]),(0,b.useEffect)((function(){c(n)}),[n]),(0,p.jsxs)(p.Fragment,{children:[l&&(0,p.jsx)(f.Modal,{title:(0,_.__)("Review Smart Links","wp-parsely"),className:"wp-parsely-smart-linking-review-modal",onRequestClose:C,shouldCloseOnClickOutside:!1,shouldCloseOnEsc:!1,children:(0,p.jsxs)("div",{className:"smart-linking-modal-body",children:[(0,p.jsx)(Qe,{outboundLinks:v,inboundLinks:g,activeLink:k,setSelectedLink:S}),k&&(ue(k)?(0,p.jsx)(Ke,{link:k,onNext:O,onPrevious:R,hasNext:g.indexOf(k)0}):(0,p.jsx)(rt,{link:k,hasNext:m().indexOf(k)0,onNext:O,onPrevious:R,onAccept:function(){return it(void 0,void 0,void 0,(function(){var e,t;return st(this,(function(n){switch(n.label){case 0:return k.match?(r(k),[4,(i=k.match.blockId,s=k,it(void 0,void 0,void 0,(function(){var e,t;return st(this,(function(n){switch(n.label){case 0:return(e=document.createElement("a")).href=s.href,e.title=s.title,e.setAttribute("data-smartlink",s.uid),(t=(0,h.select)("core/block-editor").getBlock(i))?(fe(t,s,e),s.applied=!0,[4,L(s)]):[2];case 1:return n.sent(),[2]}}))})))]):[2];case 1:return n.sent(),P.trackEvent("smart_linking_link_accepted",{link:k.href,title:k.title,text:k.text,uid:k.uid}),0===y().length?(C(),[2]):(e=v.indexOf(k),v[t=e+1]?S(v[t]):S(v[0]),[2])}var i,s}))}))},onReject:function(){return it(void 0,void 0,void 0,(function(){var e,t;return st(this,(function(n){switch(n.label){case 0:return e=v.indexOf(k),v[t=e+1]?S(v[t]):v[0]?S(v[0]):C(),[4,E(k.uid)];case 1:return n.sent(),P.trackEvent("smart_linking_link_rejected",{link:k.href,title:k.title,text:k.text,uid:k.uid}),[2]}}))}))},onRemove:function(){return it(void 0,void 0,void 0,(function(){var e,t,n,r;return st(this,(function(i){switch(i.label){case 0:return k.match?(e=(0,h.select)("core/block-editor").getBlock(k.match.blockId))?(t=m(),n=t.indexOf(k),r=n-1,[4,N(e,k)]):[3,2]:[2];case 1:if(i.sent(),P.trackEvent("smart_linking_link_removed",{link:k.href,title:k.title,text:k.text,uid:k.uid}),0===(t=m()).length&&g.length>0)return S(g[0]),[2];if(0===t.length&&0===g.length)return C(),[2];if(t[r])return S(t[r]),[2];S(t[0]),i.label=2;case 2:return[2]}}))}))},onSelectInEditor:function(){if(k.match){var e=(0,h.select)("core/block-editor").getBlock(k.match.blockId);if(e){te.selectBlock(e.clientId);var t=document.querySelector('[data-block="'.concat(e.clientId,'"]'));t&&ke(t,k.uid),P.trackEvent("smart_linking_select_in_editor_pressed",{type:"outbound",uid:k.uid}),C()}}}}))]})}),s&&(0,p.jsxs)(f.Modal,{title:(0,_.__)("Review Smart Links","wp-parsely"),onRequestClose:function(){return A(!1)},className:"wp-parsely-smart-linking-close-dialog",children:[(0,_.__)("Are you sure you want to close? All un-accepted smart links will not be added.","wp-parsely"),(0,p.jsxs)("div",{className:"smart-linking-close-dialog-actions",children:[(0,p.jsx)(f.Button,{variant:"secondary",onClick:function(){return A(!1)},children:(0,_.__)("Go Back","wp-parsely")}),(0,p.jsx)(f.Button,{variant:"primary",onClick:function(){return A(!0)},children:(0,_.__)("Close","wp-parsely")})]})]})]})})),at=function(){return at=Object.assign||function(e){for(var t,n=1,r=arguments.length;n0&&i[i.length-1])||6!==a[0]&&2!==a[0])){o=0;continue}if(3===a[0]&&(!i||a[1]>i[0]&&a[1]0&&k("success",/* translators: %d: number of smart links applied */ /* translators: %d: number of smart links applied */ (0,_.sprintf)((0,_.__)("%s smart links successfully applied.","wp-parsely"),g),{type:"snackbar"}):y(0)}),[w]),(0,b.useEffect)((function(){if(!(Object.keys(R).length>0)){var e={maxLinksPerPost:a.SmartLinking.MaxLinks};te(e)}}),[te,a]);var he=(0,h.useSelect)((function(e){var t=e("core/block-editor"),r=t.getSelectedBlock,i=t.getBlock,s=t.getBlocks,o=e("core/editor"),a=o.getEditedPostContent,l=o.getCurrentPostAttribute;return{allBlocks:s(),selectedBlock:n?i(n):r(),postContent:a(),postPermalink:l("link")}}),[n]),ve=he.allBlocks,me=he.selectedBlock,xe=he.postContent,ke=he.postPermalink,Se=function(e){return lt(void 0,void 0,void 0,(function(){var t,n,r,i,s;return ct(this,(function(o){switch(o.label){case 0:t=[],o.label=1;case 1:return o.trys.push([1,4,,9]),[4,re((n=E||!me)?_e.All:_e.Selected)];case 2:return o.sent(),a=ke.replace(/^https?:\/\//i,""),r=["http://"+a,"https://"+a],i=function(e){return e.map((function(e){return e.href}))}(F),r.push.apply(r,i),[4,De.getInstance().generateSmartLinks(me&&!n?(0,Q.getBlockContent)(me):xe,O,r)];case 3:return t=o.sent(),[3,9];case 4:if((s=o.sent()).code&&s.code===$.ParselyAborted)throw s.numRetries=3-e,s;return e>0&&s.retryFetch?(console.error(s),[4,ce(!0)]):[3,8];case 5:return o.sent(),[4,ue()];case 6:return o.sent(),[4,Se(e-1)];case 7:return[2,o.sent()];case 8:throw s;case 9:return[2,t]}var a}))}))},Pe=function(){for(var e=[],t=0;t[type="button"]').forEach((function(e){e.setAttribute("disabled","disabled")}))},Ne=function(){document.querySelectorAll('.edit-post-header__settings>[type="button"]').forEach((function(e){e.removeAttribute("disabled")})),ne.unlockPostSaving("wp-parsely-block-overlay")};return(0,p.jsxs)("div",{className:"wp-parsely-smart-linking",children:[(0,p.jsx)(se,{isDetectingEnabled:!L,onLinkRemove:function(e){!function(e){ae(this,void 0,void 0,(function(){var t,n,r;return le(this,(function(i){switch(i.label){case 0:return[4,we((0,Q.getBlockContent)(e),e.clientId)];case 1:return t=i.sent(),n=t.missingSmartLinks,r=t.didAnyFixes,n.forEach((function(e){(0,h.dispatch)(Te).removeSmartLink(e.uid)})),[2,r]}}))}))}(e.block)}}),(0,p.jsxs)(f.PanelRow,{className:t,children:[(0,p.jsxs)("div",{className:"smart-linking-text",children:[(0,_.__)("Automatically insert links to your most relevant, top performing content.","wp-parsely"),(0,p.jsxs)(f.Button,{href:"https://docs.parse.ly/plugin-content-helper/#h-smart-linking-beta",target:"_blank",variant:"link",children:[(0,_.__)("Learn more about Parse.ly AI","wp-parsely"),(0,p.jsx)(X,{icon:ee,size:18,className:"parsely-external-link-icon"})]})]}),C&&(0,p.jsx)(f.Notice,{status:"info",onRemove:function(){return Z(null)},className:"wp-parsely-content-helper-error",children:C.Message()}),w&&g>0&&(0,p.jsx)(f.Notice,{status:"success",onRemove:function(){return x(!1)},className:"wp-parsely-smart-linking-suggested-links",children:(0,_.sprintf)(/* translators: 1 - number of smart links generated */ /* translators: 1 - number of smart links generated */ (0,_.__)("Successfully added %s smart links.","wp-parsely"),g>0?g:A.length)}),(0,p.jsx)(Ce,{disabled:T,selectedBlock:me,onSettingChange:function(e,t){var n;d({SmartLinking:at(at({},a.SmartLinking),(n={},n[e]=t,n))}),"MaxLinks"===e&&oe(t)}}),(0,p.jsx)("div",{className:"smart-linking-generate",children:(0,p.jsx)(f.Button,{onClick:function(){return lt(void 0,void 0,void 0,(function(){var e,t,n,r,s,o,a,l;return ct(this,(function(c){switch(c.label){case 0:return[4,q(!0)];case 1:return c.sent(),[4,de()];case 2:return c.sent(),[4,Z(null)];case 3:return c.sent(),x(!1),P.trackEvent("smart_linking_generate_pressed",{is_full_content:E,selected_block:null!==(o=null==me?void 0:me.name)&&void 0!==o?o:"none",context:i}),[4,Pe(E?"all":null==me?void 0:me.clientId)];case 4:c.sent(),e=setTimeout((function(){var e;q(!1),P.trackEvent("smart_linking_generate_timeout",{is_full_content:E,selected_block:null!==(e=null==me?void 0:me.name)&&void 0!==e?e:"none",context:i}),je(E?"all":null==me?void 0:me.clientId)}),18e4),t=I,c.label=5;case 5:return c.trys.push([5,8,10,15]),[4,Se(3)];case 6:return n=c.sent(),[4,(u=n,lt(void 0,void 0,void 0,(function(){var e;return ct(this,(function(t){switch(t.label){case 0:return u=u.filter((function(e){return!F.some((function(t){return t.uid===e.uid&&t.applied}))})),e=ke.replace(/^https?:\/\//,"").replace(/\/+$/,""),u=(u=u.filter((function(t){return!t.href.includes(e)||(console.warn("PCH Smart Linking: Skipping self-reference link: ".concat(t.href)),!1)}))).filter((function(e){return!F.some((function(t){return t.href===e.href?(console.warn("PCH Smart Linking: Skipping duplicate link: ".concat(e.href)),!0):t.text===e.text&&t.offset!==e.offset&&(console.warn("PCH Smart Linking: Skipping duplicate link text: ".concat(e.text)),!0)}))})),u=(u=ge(E?ve:[me],u,{}).filter((function(e){return e.match}))).filter((function(e){if(!e.match)return!1;var t=e.match.blockLinkPosition,n=t+e.text.length;return!F.some((function(r){if(!r.match)return!1;if(e.match.blockId!==r.match.blockId)return!1;var i=r.match.blockLinkPosition,s=i+r.text.length;return t>=i&&n<=s}))})),[4,W(u)];case 1:return t.sent(),[2,u]}}))})))];case 7:if(0===c.sent().length)throw new ie((0,_.__)("No smart links were generated.","wp-parsely"),$.ParselySuggestionsApiNoData,"");return pe(!0),[3,15];case 8:return r=c.sent(),s=new ie(null!==(a=r.message)&&void 0!==a?a:"An unknown error has occurred.",null!==(l=r.code)&&void 0!==l?l:$.UnknownError),r.code&&r.code===$.ParselyAborted&&(s.message=(0,_.sprintf)(/* translators: %d: number of retry attempts, %s: attempt plural */ /* translators: %d: number of retry attempts, %s: attempt plural */ (0,_.__)("The Smart Linking process was cancelled after %1$d %2$s.","wp-parsely"),r.numRetries,(0,_._n)("attempt","attempts",r.numRetries,"wp-parsely"))),console.error(r),[4,Z(s)];case 9:return c.sent(),s.createErrorSnackbar(),[3,15];case 10:return[4,q(!1)];case 11:return c.sent(),[4,re(t)];case 12:return c.sent(),[4,ce(!1)];case 13:return c.sent(),[4,je(E?"all":null==me?void 0:me.clientId)];case 14:return c.sent(),clearTimeout(e),[7];case 15:return[2]}var u}))}))},variant:"primary",isBusy:T,disabled:T,children:M?(0,_.sprintf)(/* translators: %1$d: number of retry attempts, %2$d: maximum number of retries */ /* translators: %1$d: number of retry attempts, %2$d: maximum number of retries */ @@ -22,7 +22,7 @@ message:(0,_.sprintf)((0,_.__)('in section "%1$s"',"wp-parsely"),n.value)};if(w. message:(0,_.sprintf)((0,_.__)('by author "%1$s"',"wp-parsely"),n.value)};throw new ie((0,_.__)("No valid filter type has been specified.","wp-parsely"),$.CannotFormulateApiQuery)},t}(Re),xn=function(){return xn=Object.assign||function(e){for(var t,n=1,r=arguments.length;n0&&i[i.length-1])||6!==a[0]&&2!==a[0])){o=0;continue}if(3===a[0]&&(!i||a[1]>i[0]&&a[1]0&&f.every(Number.isInteger)?null!==(n=l("taxonomy","category",{include:f,context:"view"}))&&void 0!==n?n:void 0:null,tagRecords:o=Array.isArray(h)&&h.length>0&&h.every(Number.isInteger)?null!==(r=l("taxonomy","post_tag",{include:h,context:"view"}))&&void 0!==r?r:void 0:null,isLoading:u("getEntityRecords",["root","user",{include:[p],context:"view"}])||u("getEntityRecords",["taxonomy","category",{include:f,context:"view"}])||u("getEntityRecords",["taxonomy","post_tag",{include:h,context:"view"}]),hasResolved:(c("getEntityRecords",["root","user",{include:[p],context:"view"}])||null===i)&&(c("getEntityRecords",["taxonomy","category",{include:f,context:"view"}])||null===s)&&(c("getEntityRecords",["taxonomy","post_tag",{include:h,context:"view"}])||null===o)}}),[]);return(0,b.useEffect)((function(){var e=r.authorRecords,t=r.categoryRecords,i=r.tagRecords,s=r.isLoading;r.hasResolved&&!s&&n({authors:e,categories:t,tags:i,isReady:!0})}),[r]),t}(),c=l.authors,u=l.categories,d=l.tags,v=l.isReady;(0,b.useEffect)((function(){if(v){var e=function(e){return function(e){return!(!Array.isArray(e)||0===e.length)&&e.every((function(e){return"name"in e&&"id"in e&&"slug"in e&&"description"in e&&"link"in e}))}(e)?e.map((function(e){return e.name})):[]};a({authors:e(c),categories:e(u),tags:e(d)})}}),[c,u,d,v]);var g=(0,b.useState)(!0),x=g[0],k=g[1],S=(0,b.useState)(),j=S[0],T=S[1],L=(0,b.useState)(),E=L[0],N=L[1],C=(0,b.useState)([]),A=C[0],O=C[1],R=(0,b.useState)({type:t.RelatedPosts.FilterBy,value:t.RelatedPosts.FilterValue}),I=R[0],M=R[1],G=(0,b.useState)(void 0),H=G[0],U=G[1],q=(0,z.useDebounce)(U,1e3);(0,h.useSelect)((function(e){if("undefined"==typeof jest){var t=e("core/editor").getEditedPostContent;q(t())}else q("Jest test is running")}),[q]);var Z=function(e,r){n({RelatedPosts:xn(xn({},t.RelatedPosts),{FilterBy:e,FilterValue:r})})};return(0,b.useEffect)((function(){var e,t,n=function(e){return kn(void 0,void 0,void 0,(function(){return Sn(this,(function(t){return bn.getInstance().getRelatedPosts(r,i,I).then((function(e){O(e.posts),N(e.message),k(!1)})).catch((function(t){return kn(void 0,void 0,void 0,(function(){return Sn(this,(function(r){switch(r.label){case 0:return e>0&&t.retryFetch?[4,new Promise((function(e){return setTimeout(e,500)}))]:[3,3];case 1:return r.sent(),[4,n(e-1)];case 2:return r.sent(),[3,4];case 3:k(!1),T(t),r.label=4;case 4:return[2]}}))}))})),[2]}))}))},s=w.Author===I.type,a=w.Tag===I.type,l=w.Section===I.type,c=w.Unavailable===I.type,u=0===o.authors.length,d=0===o.tags.length,p=0===o.categories.length,f=s&&!o.authors.includes(I.value),h=a&&!o.tags.includes(I.value),v=l&&!o.categories.includes(I.value);return k(!0),c||a&&d||l&&p||s&&u?Object.values(o).every((function(e){return 0===e.length}))||M((e="",t=w.Unavailable,o.tags.length>=1?(t=w.Tag,e=o.tags[0]):o.categories.length>=1?(t=w.Section,e=o.categories[0]):o.authors.length>=1&&(t=w.Author,e=o.authors[0]),{type:t,value:e})):h?M({type:w.Tag,value:o.tags[0]}):v?M({type:w.Section,value:o.categories[0]}):f?M({type:w.Author,value:o.authors[0]}):n(1),function(){k(!1),O([]),N(""),T(void 0)}}),[r,i,I,o]),0===o.authors.length&&0===o.categories.length&&0===o.tags.length&&v?(0,p.jsx)("div",{className:"wp-parsely-related-posts",children:(0,p.jsx)("div",{className:"related-posts-body",children:(0,_.__)("Error: No author, section, or tags could be found for this post.","wp-parsely")})}):(0,p.jsxs)("div",{className:"wp-parsely-related-posts",children:[(0,p.jsx)("div",{className:"related-posts-description",children:(0,_.__)("Find top-performing related posts based on a key metric.","wp-parsely")}),(0,p.jsxs)("div",{className:"related-posts-body",children:[(0,p.jsxs)("div",{className:"related-posts-settings",children:[(0,p.jsx)(f.SelectControl,{size:"__unstable-large",onChange:function(e){var r;D(r=e,m)&&(n({RelatedPosts:xn(xn({},t.RelatedPosts),{Metric:r})}),P.trackEvent("related_posts_metric_changed",{metric:r}))},prefix:(0,p.jsx)(f.__experimentalInputControlPrefixWrapper,{children:(0,_.__)("Metric: ","wp-parsely")}),value:i,children:Object.values(m).map((function(e){return(0,p.jsx)("option",{value:e,children:V(e)},e)}))}),(0,p.jsx)(f.SelectControl,{size:"__unstable-large",value:r,prefix:(0,p.jsxs)(f.__experimentalInputControlPrefixWrapper,{children:[(0,_.__)("Period: ","wp-parsely")," "]}),onChange:function(e){return function(e){D(e,y)&&(n({RelatedPosts:xn(xn({},t.RelatedPosts),{Period:e})}),P.trackEvent("related_posts_period_changed",{period:e}))}(e)},children:Object.values(y).map((function(e){return(0,p.jsx)("option",{value:e,children:F(e)},e)}))})]}),(0,p.jsx)(ln,{label:(0,_.__)("Filter by","wp-parsely"),filter:I,onFilterTypeChange:function(e){if(D(e,w)){var t="",n=e;w.Tag===n&&(t=o.tags[0]),w.Section===n&&(t=o.categories[0]),w.Author===n&&(t=o.authors[0]),""!==t&&(Z(n,t),M({type:n,value:t}),P.trackEvent("related_posts_filter_type_changed",{filter_type:n}))}},onFilterValueChange:function(e){"string"==typeof e&&(Z(I.type,e),M(xn(xn({},I),{value:e})))},postData:o}),(0,p.jsxs)("div",{className:"related-posts-wrapper",children:[(0,p.jsx)("div",{children:(0,p.jsx)("p",{className:"related-posts-descr","data-testid":"parsely-related-posts-descr",children:w.Tag===I.type?(0,_.sprintf)(/* translators: 1: tag name, 2: period */ /* translators: 1: tag name, 2: period */ (0,_.__)("Top related posts with the “%1$s” tag in the %2$s.","wp-parsely"),I.value,F(r,!0)):w.Section===I.type?(0,_.sprintf)(/* translators: 1: section name, 2: period */ /* translators: 1: section name, 2: period */ (0,_.__)("Top related posts in the “%1$s” section in the %2$s.","wp-parsely"),I.value,F(r,!0)):w.Author===I.type?(0,_.sprintf)(/* translators: 1: author name, 2: period */ /* translators: 1: author name, 2: period */ -(0,_.__)("Top related posts by %1$s in the %2$s.","wp-parsely"),I.value,F(r,!0)):null!=E?E:""})}),j&&j.Message(),x&&(0,p.jsx)("div",{className:"related-posts-loading-message","data-testid":"parsely-related-posts-loading-message",children:(0,_.__)("Loading…","wp-parsely")}),!x&&!j&&0===A.length&&(0,p.jsx)("div",{className:"related-posts-empty","data-testid":"parsely-related-posts-empty",children:(0,_.__)("No related posts found.","wp-parsely")}),!x&&A.length>0&&(0,p.jsx)("div",{className:"related-posts-list",children:A.map((function(e){return(0,p.jsx)(hn,{metric:i,post:e,postContent:H},e.id)}))})]})]})]})},jn=(0,p.jsx)(x.SVG,{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 24 24",children:(0,p.jsx)(x.Path,{d:"m19 7-3-3-8.5 8.5-1 4 4-1L19 7Zm-7 11.5H5V20h7v-1.5Z"})}),Tn=function(){return(0,p.jsx)(f.SVG,{xmlns:"http://www.w3.org/2000/svg",width:"18",height:"18",viewBox:"0 0 18 18",fill:"none",children:(0,p.jsx)(f.Path,{fillRule:"evenodd",clipRule:"evenodd",d:"M13.5034 7.91642L9 12.0104L4.49662 7.91642L5.25337 7.08398L8.99999 10.49L12.7466 7.08398L13.5034 7.91642Z",fill:"#1E1E1E"})})},Ln={journalist:{label:(0,_.__)("Journalist","wp-parsely")},editorialWriter:{label:(0,_.__)("Editorial Writer","wp-parsely")},investigativeReporter:{label:(0,_.__)("Investigative Reporter","wp-parsely")},techAnalyst:{label:(0,_.__)("Tech Analyst","wp-parsely")},businessAnalyst:{label:(0,_.__)("Business Analyst","wp-parsely")},culturalCommentator:{label:(0,_.__)("Cultural Commentator","wp-parsely")},scienceCorrespondent:{label:(0,_.__)("Science Correspondent","wp-parsely")},politicalAnalyst:{label:(0,_.__)("Political Analyst","wp-parsely")},healthWellnessAdvocate:{label:(0,_.__)("Health and Wellness Advocate","wp-parsely")},environmentalJournalist:{label:(0,_.__)("Environmental Journalist","wp-parsely")},custom:{label:(0,_.__)("Custom Persona","wp-parsely"),icon:jn}},En=Object.keys(Ln),Nn=function(e){return"custom"===e||""===e?Ln.custom.label:Cn(e)?e:Ln[e].label},Cn=function(e){return!En.includes(e)||"custom"===e},An=function(e){var t=e.value,n=e.onChange,r=(0,b.useState)(""),i=r[0],s=r[1],o=(0,z.useDebounce)(n,500);return(0,p.jsx)("div",{className:"parsely-persona-selector-custom",children:(0,p.jsx)(f.TextControl,{value:i||t,placeholder:(0,_.__)("Enter a custom persona…","wp-parsely"),onChange:function(e){if(""===e)return n(""),void s("");e.length>32&&(e=e.slice(0,32)),o(e),s(e)}})})},On=function(e){var t=e.persona,n=e.value,r=void 0===n?(0,_.__)("Select a persona…","wp-parsely"):n,i=e.label,s=void 0===i?(0,_.__)("Persona","wp-parsely"):i,o=e.onChange,a=e.onDropdownChange,l=e.disabled,c=void 0!==l&&l,u=e.allowCustom,d=void 0!==u&&u;return(0,p.jsxs)(f.Disabled,{isDisabled:c,children:[s&&(0,p.jsx)("div",{className:"wp-parsely-dropdown-label",children:s}),(0,p.jsx)(f.DropdownMenu,{label:(0,_.__)("Persona","wp-parsely"),className:"parsely-persona-selector-dropdown"+(c?" is-disabled":""),popoverProps:{className:"wp-parsely-popover"},toggleProps:{children:(0,p.jsxs)(p.Fragment,{children:[(0,p.jsx)("div",{className:"parsely-persona-selector-label",children:Cn(t)?Ln.custom.label:r}),(0,p.jsx)(Tn,{})]})},children:function(e){var n=e.onClose;return(0,p.jsx)(f.MenuGroup,{label:(0,_.__)("Persona","wp-parsely"),children:(0,p.jsx)(p.Fragment,{children:En.map((function(e){if(!d&&"custom"===e)return null;var r=Ln[e],i=e===t||Cn(t)&&"custom"===e;return(0,p.jsxs)(f.MenuItem,{isSelected:i,className:i?"is-selected":"",role:"menuitemradio",onClick:function(){null==a||a(e),o(e),n(),"custom"===e&&setTimeout((function(){var e=document.querySelector(".parsely-persona-selector-custom input");e&&e.focus()}),0)},children:[r.icon&&(0,p.jsx)(X,{icon:r.icon}),r.label]},e)}))})})}}),d&&Cn(t)&&(0,p.jsx)(An,{onChange:function(e){o(""!==e?e:"custom")},value:"custom"===t?"":t})]})},Rn={neutral:{label:(0,_.__)("Neutral","wp-parsely")},formal:{label:(0,_.__)("Formal","wp-parsely")},humorous:{label:(0,_.__)("Humorous","wp-parsely")},confident:{label:(0,_.__)("Confident","wp-parsely")},provocative:{label:(0,_.__)("Provocative","wp-parsely")},serious:{label:(0,_.__)("Serious","wp-parsely")},inspirational:{label:(0,_.__)("Inspirational","wp-parsely")},skeptical:{label:(0,_.__)("Skeptical","wp-parsely")},conversational:{label:(0,_.__)("Conversational","wp-parsely")},analytical:{label:(0,_.__)("Analytical","wp-parsely")},custom:{label:(0,_.__)("Custom Tone","wp-parsely"),icon:jn}},In=Object.keys(Rn),Bn=function(e){return"custom"===e||""===e?Rn.custom.label:Mn(e)?e:Rn[e].label},Mn=function(e){return!In.includes(e)||"custom"===e},Dn=function(e){var t=e.value,n=e.onChange,r=(0,b.useState)(""),i=r[0],s=r[1],o=(0,z.useDebounce)(n,500);return(0,p.jsx)("div",{className:"parsely-tone-selector-custom",children:(0,p.jsx)(f.TextControl,{value:i||t,placeholder:(0,_.__)("Enter a custom tone","wp-parsely"),onChange:function(e){if(""===e)return n(""),void s("");e.length>32&&(e=e.slice(0,32)),o(e),s(e)}})})},Fn=function(e){var t=e.tone,n=e.value,r=void 0===n?(0,_.__)("Select a tone","wp-parsely"):n,i=e.label,s=void 0===i?(0,_.__)("Tone","wp-parsely"):i,o=e.onChange,a=e.onDropdownChange,l=e.disabled,c=void 0!==l&&l,u=e.allowCustom,d=void 0!==u&&u;return(0,p.jsxs)(f.Disabled,{isDisabled:c,children:[(0,p.jsx)("div",{className:"wp-parsely-dropdown-label",children:s}),(0,p.jsx)(f.DropdownMenu,{label:(0,_.__)("Tone","wp-parsely"),className:"parsely-tone-selector-dropdown"+(c?" is-disabled":""),popoverProps:{className:"wp-parsely-popover"},toggleProps:{children:(0,p.jsxs)(p.Fragment,{children:[(0,p.jsx)("div",{className:"parsely-tone-selector-label",children:Mn(t)?Rn.custom.label:r}),(0,p.jsx)(Tn,{})]})},children:function(e){var n=e.onClose;return(0,p.jsx)(f.MenuGroup,{label:(0,_.__)("Select a tone","wp-parsely"),children:(0,p.jsx)(p.Fragment,{children:In.map((function(e){if(!d&&"custom"===e)return null;var r=Rn[e],i=e===t||Mn(t)&&"custom"===e;return(0,p.jsxs)(f.MenuItem,{isSelected:i,className:i?"is-selected":"",role:"menuitemradio",onClick:function(){null==a||a(e),o(e),n(),"custom"===e&&setTimeout((function(){var e=document.querySelector(".parsely-tone-selector-custom input");e&&e.focus()}),0)},children:[r.icon&&(0,p.jsx)(X,{icon:r.icon}),r.label]},e)}))})})}}),d&&Mn(t)&&(0,p.jsx)(Dn,{onChange:function(e){o(""!==e?e:"custom")},value:"custom"===t?"":t})]})},Vn=(0,p.jsx)(x.SVG,{width:"24",height:"24",viewBox:"0 0 24 24",xmlns:"http://www.w3.org/2000/svg",children:(0,p.jsx)(x.Path,{d:"M10.97 10.159a3.382 3.382 0 0 0-2.857.955l1.724 1.723-2.836 2.913L7 17h1.25l2.913-2.837 1.723 1.723a3.38 3.38 0 0 0 .606-.825c.33-.63.446-1.343.35-2.032L17 10.695 13.305 7l-2.334 3.159Z"})}),Gn=(0,p.jsx)(x.SVG,{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 24 24",children:(0,p.jsx)(x.Path,{d:"M18.3 11.7c-.6-.6-1.4-.9-2.3-.9H6.7l2.9-3.3-1.1-1-4.5 5L8.5 16l1-1-2.7-2.7H16c.5 0 .9.2 1.3.5 1 1 1 3.4 1 4.5v.3h1.5v-.2c0-1.5 0-4.3-1.5-5.7z"})}),Hn=(0,p.jsx)(x.SVG,{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 24 24",children:(0,p.jsx)(x.Path,{fillRule:"evenodd",clipRule:"evenodd",d:"M12 5.5A2.25 2.25 0 0 0 9.878 7h4.244A2.251 2.251 0 0 0 12 5.5ZM12 4a3.751 3.751 0 0 0-3.675 3H5v1.5h1.27l.818 8.997a2.75 2.75 0 0 0 2.739 2.501h4.347a2.75 2.75 0 0 0 2.738-2.5L17.73 8.5H19V7h-3.325A3.751 3.751 0 0 0 12 4Zm4.224 4.5H7.776l.806 8.861a1.25 1.25 0 0 0 1.245 1.137h4.347a1.25 1.25 0 0 0 1.245-1.137l.805-8.861Z"})}),zn=(0,p.jsx)(x.SVG,{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 24 24",children:(0,p.jsx)(x.Path,{d:"m21.5 9.1-6.6-6.6-4.2 5.6c-1.2-.1-2.4.1-3.6.7-.1 0-.1.1-.2.1-.5.3-.9.6-1.2.9l3.7 3.7-5.7 5.7v1.1h1.1l5.7-5.7 3.7 3.7c.4-.4.7-.8.9-1.2.1-.1.1-.2.2-.3.6-1.1.8-2.4.6-3.6l5.6-4.1zm-7.3 3.5.1.9c.1.9 0 1.8-.4 2.6l-6-6c.8-.4 1.7-.5 2.6-.4l.9.1L15 4.9 19.1 9l-4.9 3.6z"})}),Un=function(){return Un=Object.assign||function(e){for(var t,n=1,r=arguments.length;n0&&i[i.length-1])||6!==a[0]&&2!==a[0])){o=0;continue}if(3===a[0]&&(!i||a[1]>i[0]&&a[1]0&&i[i.length-1])||6!==a[0]&&2!==a[0])){o=0;continue}if(3===a[0]&&(!i||a[1]>i[0]&&a[1]0&&i[i.length-1])||6!==a[0]&&2!==a[0])){o=0;continue}if(3===a[0]&&(!i||a[1]>i[0]&&a[1]0?(0,p.jsx)("span",{className:"parsely-write-titles-text",children:(0,b.createInterpolateElement)( +(0,_.__)("Top related posts by %1$s in the %2$s.","wp-parsely"),I.value,F(r,!0)):null!=E?E:""})}),j&&j.Message(),x&&(0,p.jsx)("div",{className:"related-posts-loading-message","data-testid":"parsely-related-posts-loading-message",children:(0,_.__)("Loading…","wp-parsely")}),!x&&!j&&0===A.length&&(0,p.jsx)("div",{className:"related-posts-empty","data-testid":"parsely-related-posts-empty",children:(0,_.__)("No related posts found.","wp-parsely")}),!x&&A.length>0&&(0,p.jsx)("div",{className:"related-posts-list",children:A.map((function(e){return(0,p.jsx)(hn,{metric:i,post:e,postContent:H},e.id)}))})]})]})]})},jn=(0,p.jsx)(x.SVG,{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 24 24",children:(0,p.jsx)(x.Path,{d:"m19 7-3-3-8.5 8.5-1 4 4-1L19 7Zm-7 11.5H5V20h7v-1.5Z"})}),Tn=function(){return(0,p.jsx)(f.SVG,{xmlns:"http://www.w3.org/2000/svg",width:"18",height:"18",viewBox:"0 0 18 18",fill:"none",children:(0,p.jsx)(f.Path,{fillRule:"evenodd",clipRule:"evenodd",d:"M13.5034 7.91642L9 12.0104L4.49662 7.91642L5.25337 7.08398L8.99999 10.49L12.7466 7.08398L13.5034 7.91642Z",fill:"#1E1E1E"})})},Ln={journalist:{label:(0,_.__)("Journalist","wp-parsely")},editorialWriter:{label:(0,_.__)("Editorial Writer","wp-parsely")},investigativeReporter:{label:(0,_.__)("Investigative Reporter","wp-parsely")},techAnalyst:{label:(0,_.__)("Tech Analyst","wp-parsely")},businessAnalyst:{label:(0,_.__)("Business Analyst","wp-parsely")},culturalCommentator:{label:(0,_.__)("Cultural Commentator","wp-parsely")},scienceCorrespondent:{label:(0,_.__)("Science Correspondent","wp-parsely")},politicalAnalyst:{label:(0,_.__)("Political Analyst","wp-parsely")},healthWellnessAdvocate:{label:(0,_.__)("Health and Wellness Advocate","wp-parsely")},environmentalJournalist:{label:(0,_.__)("Environmental Journalist","wp-parsely")},custom:{label:(0,_.__)("Custom Persona","wp-parsely"),icon:jn}},En=Object.keys(Ln),Nn=function(e){return"custom"===e||""===e?Ln.custom.label:Cn(e)?e:Ln[e].label},Cn=function(e){return!En.includes(e)||"custom"===e},An=function(e){var t=e.value,n=e.onChange,r=(0,b.useState)(""),i=r[0],s=r[1],o=(0,z.useDebounce)(n,500);return(0,p.jsx)("div",{className:"parsely-persona-selector-custom",children:(0,p.jsx)(f.TextControl,{value:i||t,placeholder:(0,_.__)("Enter a custom persona…","wp-parsely"),onChange:function(e){if(""===e)return n(""),void s("");e.length>32&&(e=e.slice(0,32)),o(e),s(e)}})})},On=function(e){var t=e.persona,n=e.value,r=void 0===n?(0,_.__)("Select a persona…","wp-parsely"):n,i=e.label,s=void 0===i?(0,_.__)("Persona","wp-parsely"):i,o=e.onChange,a=e.onDropdownChange,l=e.disabled,c=void 0!==l&&l,u=e.allowCustom,d=void 0!==u&&u;return(0,p.jsxs)(f.Disabled,{isDisabled:c,children:[s&&(0,p.jsx)("div",{className:"wp-parsely-dropdown-label",children:s}),(0,p.jsx)(f.DropdownMenu,{label:(0,_.__)("Persona","wp-parsely"),className:"parsely-persona-selector-dropdown"+(c?" is-disabled":""),popoverProps:{className:"wp-parsely-popover"},toggleProps:{children:(0,p.jsxs)(p.Fragment,{children:[(0,p.jsx)("div",{className:"parsely-persona-selector-label",children:Cn(t)?Ln.custom.label:r}),(0,p.jsx)(Tn,{})]})},children:function(e){var n=e.onClose;return(0,p.jsx)(f.MenuGroup,{label:(0,_.__)("Persona","wp-parsely"),children:(0,p.jsx)(p.Fragment,{children:En.map((function(e){if(!d&&"custom"===e)return null;var r=Ln[e],i=e===t||Cn(t)&&"custom"===e;return(0,p.jsxs)(f.MenuItem,{isSelected:i,className:i?"is-selected":"",role:"menuitemradio",onClick:function(){null==a||a(e),o(e),n(),"custom"===e&&setTimeout((function(){var e=document.querySelector(".parsely-persona-selector-custom input");e&&e.focus()}),0)},children:[r.icon&&(0,p.jsx)(X,{icon:r.icon}),r.label]},e)}))})})}}),d&&Cn(t)&&(0,p.jsx)(An,{onChange:function(e){o(""!==e?e:"custom")},value:"custom"===t?"":t})]})},Rn={neutral:{label:(0,_.__)("Neutral","wp-parsely")},formal:{label:(0,_.__)("Formal","wp-parsely")},humorous:{label:(0,_.__)("Humorous","wp-parsely")},confident:{label:(0,_.__)("Confident","wp-parsely")},provocative:{label:(0,_.__)("Provocative","wp-parsely")},serious:{label:(0,_.__)("Serious","wp-parsely")},inspirational:{label:(0,_.__)("Inspirational","wp-parsely")},skeptical:{label:(0,_.__)("Skeptical","wp-parsely")},conversational:{label:(0,_.__)("Conversational","wp-parsely")},analytical:{label:(0,_.__)("Analytical","wp-parsely")},custom:{label:(0,_.__)("Custom Tone","wp-parsely"),icon:jn}},In=Object.keys(Rn),Bn=function(e){return"custom"===e||""===e?Rn.custom.label:Mn(e)?e:Rn[e].label},Mn=function(e){return!In.includes(e)||"custom"===e},Dn=function(e){var t=e.value,n=e.onChange,r=(0,b.useState)(""),i=r[0],s=r[1],o=(0,z.useDebounce)(n,500);return(0,p.jsx)("div",{className:"parsely-tone-selector-custom",children:(0,p.jsx)(f.TextControl,{value:i||t,placeholder:(0,_.__)("Enter a custom tone","wp-parsely"),onChange:function(e){if(""===e)return n(""),void s("");e.length>32&&(e=e.slice(0,32)),o(e),s(e)}})})},Fn=function(e){var t=e.tone,n=e.value,r=void 0===n?(0,_.__)("Select a tone","wp-parsely"):n,i=e.label,s=void 0===i?(0,_.__)("Tone","wp-parsely"):i,o=e.onChange,a=e.onDropdownChange,l=e.disabled,c=void 0!==l&&l,u=e.allowCustom,d=void 0!==u&&u;return(0,p.jsxs)(f.Disabled,{isDisabled:c,children:[(0,p.jsx)("div",{className:"wp-parsely-dropdown-label",children:s}),(0,p.jsx)(f.DropdownMenu,{label:(0,_.__)("Tone","wp-parsely"),className:"parsely-tone-selector-dropdown"+(c?" is-disabled":""),popoverProps:{className:"wp-parsely-popover"},toggleProps:{children:(0,p.jsxs)(p.Fragment,{children:[(0,p.jsx)("div",{className:"parsely-tone-selector-label",children:Mn(t)?Rn.custom.label:r}),(0,p.jsx)(Tn,{})]})},children:function(e){var n=e.onClose;return(0,p.jsx)(f.MenuGroup,{label:(0,_.__)("Select a tone","wp-parsely"),children:(0,p.jsx)(p.Fragment,{children:In.map((function(e){if(!d&&"custom"===e)return null;var r=Rn[e],i=e===t||Mn(t)&&"custom"===e;return(0,p.jsxs)(f.MenuItem,{isSelected:i,className:i?"is-selected":"",role:"menuitemradio",onClick:function(){null==a||a(e),o(e),n(),"custom"===e&&setTimeout((function(){var e=document.querySelector(".parsely-tone-selector-custom input");e&&e.focus()}),0)},children:[r.icon&&(0,p.jsx)(X,{icon:r.icon}),r.label]},e)}))})})}}),d&&Mn(t)&&(0,p.jsx)(Dn,{onChange:function(e){o(""!==e?e:"custom")},value:"custom"===t?"":t})]})},Vn=(0,p.jsx)(x.SVG,{width:"24",height:"24",viewBox:"0 0 24 24",xmlns:"http://www.w3.org/2000/svg",children:(0,p.jsx)(x.Path,{d:"M10.97 10.159a3.382 3.382 0 0 0-2.857.955l1.724 1.723-2.836 2.913L7 17h1.25l2.913-2.837 1.723 1.723a3.38 3.38 0 0 0 .606-.825c.33-.63.446-1.343.35-2.032L17 10.695 13.305 7l-2.334 3.159Z"})}),Gn=(0,p.jsx)(x.SVG,{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 24 24",children:(0,p.jsx)(x.Path,{d:"M18.3 11.7c-.6-.6-1.4-.9-2.3-.9H6.7l2.9-3.3-1.1-1-4.5 5L8.5 16l1-1-2.7-2.7H16c.5 0 .9.2 1.3.5 1 1 1 3.4 1 4.5v.3h1.5v-.2c0-1.5 0-4.3-1.5-5.7z"})}),Hn=(0,p.jsx)(x.SVG,{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 24 24",children:(0,p.jsx)(x.Path,{fillRule:"evenodd",clipRule:"evenodd",d:"M12 5.5A2.25 2.25 0 0 0 9.878 7h4.244A2.251 2.251 0 0 0 12 5.5ZM12 4a3.751 3.751 0 0 0-3.675 3H5v1.5h1.27l.818 8.997a2.75 2.75 0 0 0 2.739 2.501h4.347a2.75 2.75 0 0 0 2.738-2.5L17.73 8.5H19V7h-3.325A3.751 3.751 0 0 0 12 4Zm4.224 4.5H7.776l.806 8.861a1.25 1.25 0 0 0 1.245 1.137h4.347a1.25 1.25 0 0 0 1.245-1.137l.805-8.861Z"})}),zn=(0,p.jsx)(x.SVG,{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 24 24",children:(0,p.jsx)(x.Path,{d:"m21.5 9.1-6.6-6.6-4.2 5.6c-1.2-.1-2.4.1-3.6.7-.1 0-.1.1-.2.1-.5.3-.9.6-1.2.9l3.7 3.7-5.7 5.7v1.1h1.1l5.7-5.7 3.7 3.7c.4-.4.7-.8.9-1.2.1-.1.1-.2.2-.3.6-1.1.8-2.4.6-3.6l5.6-4.1zm-7.3 3.5.1.9c.1.9 0 1.8-.4 2.6l-6-6c.8-.4 1.7-.5 2.6-.4l.9.1L15 4.9 19.1 9l-4.9 3.6z"})}),Un=function(){return Un=Object.assign||function(e){for(var t,n=1,r=arguments.length;n0&&i[i.length-1])||6!==a[0]&&2!==a[0])){o=0;continue}if(3===a[0]&&(!i||a[1]>i[0]&&a[1]0&&i[i.length-1])||6!==a[0]&&2!==a[0])){o=0;continue}if(3===a[0]&&(!i||a[1]>i[0]&&a[1]0&&i[i.length-1])||6!==a[0]&&2!==a[0])){o=0;continue}if(3===a[0]&&(!i||a[1]>i[0]&&a[1]0?(0,p.jsx)("span",{className:"parsely-write-titles-text",children:(0,b.createInterpolateElement)( // translators: %1$s is the tone, %2$s is the persona. // translators: %1$s is the tone, %2$s is the persona. (0,_.__)("We've generated a few titles based on the content of your post, written as a .","wp-parsely"),{tone:(0,p.jsx)("strong",{children:Bn(a)}),persona:(0,p.jsx)("strong",{children:Nn(u)})})}):(0,_.__)("Use Parse.ly AI to generate a title for your post.","wp-parsely"),(0,p.jsxs)(f.Button,{href:"https://docs.parse.ly/plugin-content-helper/#h-title-suggestions-beta",target:"_blank",variant:"link",children:[(0,_.__)("Learn more about Parse.ly AI","wp-parsely"),(0,p.jsx)(X,{icon:ee,size:18,className:"parsely-external-link-icon"})]})]}),i&&(0,p.jsx)(f.Notice,{className:"wp-parsely-content-helper-error",onRemove:function(){return s(void 0)},status:"info",children:i.Message()}),void 0!==k&&(0,p.jsx)(Jn,{title:k,type:fn.PostTitle,isOriginal:!0}),00&&(0,p.jsx)(Qn,{pinnedTitles:m,isOpen:!0}),y.length>0&&(0,p.jsx)(er,{suggestions:y,isOpen:!0,isLoading:g})]}),(0,p.jsx)(Xn,{isLoading:g,onPersonaChange:function(e){C("Persona",e),d(e)},onSettingChange:C,onToneChange:function(e){C("Tone",e),l(e)},persona:t.TitleSuggestions.Persona,tone:t.TitleSuggestions.Tone}),(0,p.jsx)("div",{className:"title-suggestions-generate",children:(0,p.jsxs)(f.Button,{variant:"primary",isBusy:g,disabled:g||"custom"===a||"custom"===u,onClick:function(){return ir(void 0,void 0,void 0,(function(){return sr(this,(function(e){switch(e.label){case 0:return s(void 0),!1!==g?[3,2]:(P.trackEvent("title_suggestions_generate_pressed",{request_more:y.length>0,total_titles:y.length,total_pinned:y.filter((function(e){return e.isPinned})).length,tone:a,persona:u}),[4,(t=fn.PostTitle,n=A,r=a,i=u,ir(void 0,void 0,void 0,(function(){var e,o,a;return sr(this,(function(l){switch(l.label){case 0:return[4,T(!0)];case 1:l.sent(),e=nr.getInstance(),l.label=2;case 2:return l.trys.push([2,5,,6]),[4,e.generateTitles(n,3,r,i)];case 3:return o=l.sent(),[4,j(t,o)];case 4:return l.sent(),[3,6];case 5:return a=l.sent(),s(a),j(t,[]),[3,6];case 6:return[4,T(!1)];case 7:return l.sent(),[2]}}))})))]);case 1:e.sent(),e.label=2;case 2:return[2]}var t,n,r,i}))}))},children:[g&&(0,_.__)("Generating Titles…","wp-parsely"),!g&&w.length>0&&(0,_.__)("Generate More","wp-parsely"),!g&&0===w.length&&(0,_.__)("Generate Titles","wp-parsely")]})})]})})},ar=function(){return ar=Object.assign||function(e){for(var t,n=1,r=arguments.length;n array('react', 'wp-api-fetch', 'wp-components', 'wp-data', 'wp-editor', 'wp-element', 'wp-hooks', 'wp-i18n', 'wp-plugins', 'wp-primitives', 'wp-url', 'wp-wordcount'), 'version' => '654bab3f782c2b075d8f'); + array('react', 'wp-api-fetch', 'wp-components', 'wp-data', 'wp-editor', 'wp-element', 'wp-hooks', 'wp-i18n', 'wp-plugins', 'wp-primitives', 'wp-url', 'wp-wordcount'), 'version' => '577d3025e0a7c2398780'); diff --git a/build/content-helper/excerpt-generator.js b/build/content-helper/excerpt-generator.js index dfc1398c0e..82cb2f791a 100644 --- a/build/content-helper/excerpt-generator.js +++ b/build/content-helper/excerpt-generator.js @@ -1,4 +1,4 @@ -!function(){"use strict";var e={20:function(e,t,r){var n=r(609),o=Symbol.for("react.element"),a=Symbol.for("react.fragment"),i=Object.prototype.hasOwnProperty,s=n.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED.ReactCurrentOwner,l={key:!0,ref:!0,__self:!0,__source:!0};function c(e,t,r){var n,a={},c=null,u=null;for(n in void 0!==r&&(c=""+r),void 0!==t.key&&(c=""+t.key),void 0!==t.ref&&(u=t.ref),t)i.call(t,n)&&!l.hasOwnProperty(n)&&(a[n]=t[n]);if(e&&e.defaultProps)for(n in t=e.defaultProps)void 0===a[n]&&(a[n]=t[n]);return{$$typeof:o,type:e,key:c,ref:u,props:a,_owner:s.current}}t.Fragment=a,t.jsx=c,t.jsxs=c},848:function(e,t,r){e.exports=r(20)},609:function(e){e.exports=window.React}},t={};function r(n){var o=t[n];if(void 0!==o)return o.exports;var a=t[n]={exports:{}};return e[n](a,a.exports,r),a.exports}r.n=function(e){var t=e&&e.__esModule?function(){return e.default}:function(){return e};return r.d(t,{a:t}),t},r.d=function(e,t){for(var n in t)r.o(t,n)&&!r.o(e,n)&&Object.defineProperty(e,n,{enumerable:!0,get:t[n]})},r.o=function(e,t){return Object.prototype.hasOwnProperty.call(e,t)},function(){var e,t,n,o,a,i,s,l,c,u,p,d=window.wp.data,y=window.wp.hooks,h=window.wp.plugins,f=((0,d.dispatch)("core/block-editor"),(0,d.dispatch)("core/editor"),(0,d.dispatch)("core/edit-post")),w=r(848),v=window.wp.components,g=window.wp.editor;void 0!==window.wp&&(p=null!==(t=null===(e=window.wp.editor)||void 0===e?void 0:e.PluginDocumentSettingPanel)&&void 0!==t?t:null!==(o=null===(n=window.wp.editPost)||void 0===n?void 0:n.PluginDocumentSettingPanel)&&void 0!==o?o:null===(a=window.wp.editSite)||void 0===a?void 0:a.PluginDocumentSettingPanel,null!==(s=null===(i=window.wp.editor)||void 0===i?void 0:i.PluginSidebar)&&void 0!==s||null!==(c=null===(l=window.wp.editPost)||void 0===l?void 0:l.PluginSidebar)&&void 0!==c||null===(u=window.wp.editSite)||void 0===u||u.PluginSidebar);var _,b,P=window.wp.element,m=window.wp.i18n,x=window.wp.wordcount,E=window.wp.primitives,A=(0,w.jsx)(E.SVG,{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 24 24",children:(0,w.jsx)(E.Path,{d:"M19.5 4.5h-7V6h4.44l-5.97 5.97 1.06 1.06L18 7.06v4.44h1.5v-7Zm-13 1a2 2 0 0 0-2 2v10a2 2 0 0 0 2 2h10a2 2 0 0 0 2-2v-3H17v3a.5.5 0 0 1-.5.5h-10a.5.5 0 0 1-.5-.5v-10a.5.5 0 0 1 .5-.5h3V5.5h-3Z"})}),S=function(){function e(){this._tkq=[],this.isLoaded=!1,this.isEnabled=!1,"undefined"!=typeof wpParselyTracksTelemetry&&(this.isEnabled=!0,this.loadTrackingLibrary())}return e.getInstance=function(){return window.wpParselyTelemetryInstance||Object.defineProperty(window,"wpParselyTelemetryInstance",{value:new e,writable:!1,configurable:!1,enumerable:!1}),window.wpParselyTelemetryInstance},e.prototype.loadTrackingLibrary=function(){var e=this,t=document.createElement("script");t.async=!0,t.src="//stats.wp.com/w.js",t.onload=function(){e.isLoaded=!0,e._tkq=window._tkq||[]},document.head.appendChild(t)},e.trackEvent=function(t){return r=this,n=arguments,a=function(t,r){var n;return void 0===r&&(r={}),function(e,t){var r,n,o,a,i={label:0,sent:function(){if(1&o[0])throw o[1];return o[1]},trys:[],ops:[]};return a={next:s(0),throw:s(1),return:s(2)},"function"==typeof Symbol&&(a[Symbol.iterator]=function(){return this}),a;function s(s){return function(l){return function(s){if(r)throw new TypeError("Generator is already executing.");for(;a&&(a=0,s[0]&&(i=0)),i;)try{if(r=1,n&&(o=2&s[0]?n.return:s[0]?n.throw||((o=n.return)&&o.call(n),0):n.next)&&!(o=o.call(n,s[1])).done)return o;switch(n=0,o&&(s=[2&s[0],o.value]),s[0]){case 0:case 1:o=s;break;case 4:return i.label++,{value:s[1],done:!1};case 5:i.label++,n=s[1],s=[0];continue;case 7:s=i.ops.pop(),i.trys.pop();continue;default:if(!((o=(o=i.trys).length>0&&o[o.length-1])||6!==s[0]&&2!==s[0])){i=0;continue}if(3===s[0]&&(!o||s[1]>o[0]&&s[1]=1e4&&(clearInterval(a),r("Telemetry library not loaded"))}),100);else r("Telemetry not enabled")}))},e.prototype.trackEvent=function(t,r){var n;this.isLoaded?(0!==t.indexOf(e.TRACKS_PREFIX)&&(t=e.TRACKS_PREFIX+t),this.isEventNameValid(t)?(r=this.prepareProperties(r),null===(n=this._tkq)||void 0===n||n.push(["recordEvent",t,r])):console.error("Error tracking event: Invalid event name")):console.error("Error tracking event: Telemetry not loaded")},e.prototype.isTelemetryEnabled=function(){return this.isEnabled},e.prototype.isProprietyValid=function(t){return e.PROPERTY_REGEX.test(t)},e.prototype.isEventNameValid=function(t){return e.EVENT_NAME_REGEX.test(t)},e.prototype.prepareProperties=function(e){return(e=this.sanitizeProperties(e)).parsely_version=wpParselyTracksTelemetry.version,wpParselyTracksTelemetry.user&&(e._ut=wpParselyTracksTelemetry.user.type,e._ui=wpParselyTracksTelemetry.user.id),wpParselyTracksTelemetry.vipgo_env&&(e.vipgo_env=wpParselyTracksTelemetry.vipgo_env),this.sanitizeProperties(e)},e.prototype.sanitizeProperties=function(e){var t=this,r={};return Object.keys(e).forEach((function(n){t.isProprietyValid(n)&&(r[n]=e[n])})),r},e.TRACKS_PREFIX="wpparsely_",e.EVENT_NAME_REGEX=/^(([a-z0-9]+)_){2}([a-z0-9_]+)$/,e.PROPERTY_REGEX=/^[a-z_][a-z0-9_]*$/,e}(),k=(S.trackEvent,function(e){void 0===e&&(e=null);var t="";(null==e?void 0:e.children)&&(t=e.children);var r="content-helper-error-message";return(null==e?void 0:e.className)&&(r+=" "+e.className),(0,w.jsx)("div",{className:r,"data-testid":null==e?void 0:e.testId,dangerouslySetInnerHTML:{__html:t}})}),T=(_=function(e,t){return _=Object.setPrototypeOf||{__proto__:[]}instanceof Array&&function(e,t){e.__proto__=t}||function(e,t){for(var r in t)Object.prototype.hasOwnProperty.call(t,r)&&(e[r]=t[r])},_(e,t)},function(e,t){if("function"!=typeof t&&null!==t)throw new TypeError("Class extends value "+String(t)+" is not a constructor or null");function __(){this.constructor=e}_(e,t),e.prototype=null===t?Object.create(t):(__.prototype=t.prototype,new __)});!function(e){e.AccessToFeatureDisabled="ch_access_to_feature_disabled",e.CannotFormulateApiQuery="ch_cannot_formulate_api_query",e.FetchError="fetch_error",e.HttpRequestFailed="http_request_failed",e.ParselyAborted="ch_parsely_aborted",e[e.ParselyApiForbidden=403]="ParselyApiForbidden",e.ParselyApiResponseContainsError="ch_response_contains_error",e.ParselyApiReturnedNoData="ch_parsely_api_returned_no_data",e.ParselyApiReturnedTooManyResults="ch_parsely_api_returned_too_many_results",e.PluginCredentialsNotSetMessageDetected="parsely_credentials_not_set_message_detected",e.PluginSettingsApiSecretNotSet="parsely_api_secret_not_set",e.PluginSettingsSiteIdNotSet="parsely_site_id_not_set",e.PostIsNotPublished="ch_post_not_published",e.UnknownError="ch_unknown_error",e.ParselySuggestionsApiAuthUnavailable="AUTH_UNAVAILABLE",e.ParselySuggestionsApiNoAuthentication="NO_AUTHENTICATION",e.ParselySuggestionsApiNoAuthorization="NO_AUTHORIZATION",e.ParselySuggestionsApiNoData="NO_DATA",e.ParselySuggestionsApiOpenAiError="OPENAI_ERROR",e.ParselySuggestionsApiOpenAiSchema="OPENAI_SCHEMA",e.ParselySuggestionsApiOpenAiUnavailable="OPENAI_UNAVAILABLE",e.ParselySuggestionsApiSchemaError="SCHEMA_ERROR"}(b||(b={}));var N=function(e){function t(r,n,o){void 0===o&&(o=(0,m.__)("Error: ","wp-parsely"));var a=this;r.startsWith(o)&&(o=""),(a=e.call(this,o+r)||this).hint=null,a.name=a.constructor.name,a.code=n;var i=[b.AccessToFeatureDisabled,b.ParselyApiForbidden,b.ParselyApiResponseContainsError,b.ParselyApiReturnedNoData,b.ParselyApiReturnedTooManyResults,b.PluginCredentialsNotSetMessageDetected,b.PluginSettingsApiSecretNotSet,b.PluginSettingsSiteIdNotSet,b.PostIsNotPublished,b.UnknownError,b.ParselySuggestionsApiAuthUnavailable,b.ParselySuggestionsApiNoAuthentication,b.ParselySuggestionsApiNoAuthorization,b.ParselySuggestionsApiNoData,b.ParselySuggestionsApiSchemaError];return a.retryFetch=!i.includes(a.code),Object.setPrototypeOf(a,t.prototype),a.code===b.AccessToFeatureDisabled?a.message=(0,m.__)("Access to this feature is disabled by the site's administration.","wp-parsely"):a.code===b.ParselySuggestionsApiNoAuthorization?a.message=(0,m.__)('This AI-powered feature is opt-in. To gain access, please submit a request here.',"wp-parsely"):a.code===b.ParselySuggestionsApiOpenAiError||a.code===b.ParselySuggestionsApiOpenAiUnavailable?a.message=(0,m.__)("The Parse.ly API returned an internal server error. Please retry with a different input, or try again later.","wp-parsely"):a.code===b.HttpRequestFailed&&a.message.includes("cURL error 28")?a.message=(0,m.__)("The Parse.ly API did not respond in a timely manner. Please try again later.","wp-parsely"):a.code===b.ParselySuggestionsApiSchemaError?a.message=(0,m.__)("The Parse.ly API returned a validation error. Please try again with different parameters.","wp-parsely"):a.code===b.ParselySuggestionsApiNoData?a.message=(0,m.__)("The Parse.ly API couldn't find any relevant data to fulfill the request. Please retry with a different input.","wp-parsely"):a.code===b.ParselySuggestionsApiOpenAiSchema?a.message=(0,m.__)("The Parse.ly API returned an incorrect response. Please try again later.","wp-parsely"):a.code===b.ParselySuggestionsApiAuthUnavailable&&(a.message=(0,m.__)("The Parse.ly API is currently unavailable. Please try again later.","wp-parsely")),a}return T(t,e),t.prototype.Message=function(e){return void 0===e&&(e=null),[b.PluginCredentialsNotSetMessageDetected,b.PluginSettingsSiteIdNotSet,b.PluginSettingsApiSecretNotSet].includes(this.code)?function(e){var t;return void 0===e&&(e=null),(0,w.jsx)(k,{className:null==e?void 0:e.className,testId:"empty-credentials-message",children:null!==(t=window.wpParselyEmptyCredentialsMessage)&&void 0!==t?t:(0,m.__)("Please ensure that the Site ID and API Secret given in the plugin's settings are correct.","wp-parsely")})}(e):(this.code===b.FetchError&&(this.hint=this.Hint((0,m.__)("This error can sometimes be caused by ad-blockers or browser tracking protections. Please add this site to any applicable allow lists and try again.","wp-parsely"))),this.code!==b.ParselyApiForbidden&&this.code!==b.ParselySuggestionsApiNoAuthentication||(this.hint=this.Hint((0,m.__)("Please ensure that the Site ID and API Secret given in the plugin's settings are correct.","wp-parsely"))),this.code===b.HttpRequestFailed&&(this.hint=this.Hint((0,m.__)("The Parse.ly API cannot be reached. Please verify that you are online.","wp-parsely"))),(0,w.jsx)(k,{className:null==e?void 0:e.className,testId:"error",children:"

".concat(this.message,"

").concat(this.hint?this.hint:"")}))},t.prototype.Hint=function(e){return'

'.concat((0,m.__)("Hint:","wp-parsely")," ").concat(e,"

")},t.prototype.createErrorSnackbar=function(){//.test(this.message)||(0,d.dispatch)("core/notices").createNotice("error",this.message,{type:"snackbar"})},t}(Error),j=function(e){var t=e.size,r=void 0===t?24:t,n=e.className,o=void 0===n?"wp-parsely-icon":n;return(0,w.jsxs)(v.SVG,{className:o,height:r,viewBox:"0 0 60 65",width:r,xmlns:"http://www.w3.org/2000/svg",children:[(0,w.jsx)(v.Path,{fill:"#5ba745",d:"M23.72,51.53c0-.18,0-.34-.06-.52a13.11,13.11,0,0,0-2.1-5.53A14.74,14.74,0,0,0,19.12,43c-.27-.21-.5-.11-.51.22l-.24,3.42c0,.33-.38.35-.49,0l-1.5-4.8a1.4,1.4,0,0,0-.77-.78,23.91,23.91,0,0,0-3.1-.84c-1.38-.24-3.39-.39-3.39-.39-.34,0-.45.21-.25.49l2.06,3.76c.2.27,0,.54-.29.33l-4.51-3.6a3.68,3.68,0,0,0-2.86-.48c-1,.16-2.44.46-2.44.46a.68.68,0,0,0-.39.25.73.73,0,0,0-.14.45S.41,43,.54,44a3.63,3.63,0,0,0,1.25,2.62L6.48,50c.28.2.09.49-.23.37l-4.18-.94c-.32-.12-.5,0-.4.37,0,0,.69,1.89,1.31,3.16a24,24,0,0,0,1.66,2.74,1.34,1.34,0,0,0,1,.52l5,.13c.33,0,.41.38.1.48L7.51,58c-.31.1-.34.35-.07.55a14.29,14.29,0,0,0,3.05,1.66,13.09,13.09,0,0,0,5.9.5,25.13,25.13,0,0,0,4.34-1,9.55,9.55,0,0,1-.08-1.2,9.32,9.32,0,0,1,3.07-6.91"}),(0,w.jsx)(v.Path,{fill:"#5ba745",d:"M59.7,41.53a.73.73,0,0,0-.14-.45.68.68,0,0,0-.39-.25s-1.43-.3-2.44-.46a3.64,3.64,0,0,0-2.86.48l-4.51,3.6c-.26.21-.49-.06-.29-.33l2.06-3.76c.2-.28.09-.49-.25-.49,0,0-2,.15-3.39.39a23.91,23.91,0,0,0-3.1.84,1.4,1.4,0,0,0-.77.78l-1.5,4.8c-.11.32-.48.3-.49,0l-.24-3.42c0-.33-.24-.43-.51-.22a14.74,14.74,0,0,0-2.44,2.47A13.11,13.11,0,0,0,36.34,51c0,.18,0,.34-.06.52a9.26,9.26,0,0,1,3,8.1,24.1,24.1,0,0,0,4.34,1,13.09,13.09,0,0,0,5.9-.5,14.29,14.29,0,0,0,3.05-1.66c.27-.2.24-.45-.07-.55l-3.22-1.17c-.31-.1-.23-.47.1-.48l5-.13a1.38,1.38,0,0,0,1-.52A24.6,24.6,0,0,0,57,52.92c.61-1.27,1.31-3.16,1.31-3.16.1-.33-.08-.49-.4-.37l-4.18.94c-.32.12-.51-.17-.23-.37l4.69-3.34A3.63,3.63,0,0,0,59.46,44c.13-1,.24-2.47.24-2.47"}),(0,w.jsx)(v.Path,{fill:"#5ba745",d:"M46.5,25.61c0-.53-.35-.72-.8-.43l-4.86,2.66c-.45.28-.56-.27-.23-.69l4.66-6.23a2,2,0,0,0,.28-1.68,36.51,36.51,0,0,0-2.19-4.89,34,34,0,0,0-2.81-3.94c-.33-.41-.74-.35-.91.16l-2.28,5.68c-.16.5-.6.48-.59-.05l.28-8.93a2.54,2.54,0,0,0-.66-1.64S35,4.27,33.88,3.27,30.78.69,30.78.69a1.29,1.29,0,0,0-1.54,0s-1.88,1.49-3.12,2.59-2.48,2.35-2.48,2.35A2.5,2.5,0,0,0,23,7.27l.27,8.93c0,.53-.41.55-.58.05l-2.29-5.69c-.17-.5-.57-.56-.91-.14a35.77,35.77,0,0,0-3,4.2,35.55,35.55,0,0,0-2,4.62,2,2,0,0,0,.27,1.67l4.67,6.24c.33.42.23,1-.22.69l-4.87-2.66c-.45-.29-.82-.1-.82.43a18.6,18.6,0,0,0,.83,5.07,20.16,20.16,0,0,0,5.37,7.77c3.19,3,5.93,7.8,7.45,11.08A9.6,9.6,0,0,1,30,49.09a9.31,9.31,0,0,1,2.86.45c1.52-3.28,4.26-8.11,7.44-11.09a20.46,20.46,0,0,0,5.09-7,19,19,0,0,0,1.11-5.82"}),(0,w.jsx)(v.Path,{fill:"#5ba745",d:"M36.12,58.44A6.12,6.12,0,1,1,30,52.32a6.11,6.11,0,0,1,6.12,6.12"})]})},C=window.wp.url,I=window.wp.apiFetch,O=r.n(I),R=function(){function e(){this.abortControllers=new Map}return e.prototype.cancelRequest=function(e){if(e)(t=this.abortControllers.get(e))&&(t.abort(),this.abortControllers.delete(e));else{var t,r=Array.from(this.abortControllers.keys()).pop();r&&(t=this.abortControllers.get(r))&&(t.abort(),this.abortControllers.delete(r))}},e.prototype.cancelAll=function(){this.abortControllers.forEach((function(e){return e.abort()})),this.abortControllers.clear()},e.prototype.getOrCreateController=function(e){if(e&&this.abortControllers.has(e))return{abortController:this.abortControllers.get(e),abortId:e};var t=null!=e?e:"auto-"+Date.now(),r=new AbortController;return this.abortControllers.set(t,r),{abortController:r,abortId:t}},e.prototype.fetch=function(e,t){return r=this,n=void 0,a=function(){var r,n,o,a,i,s;return function(e,t){var r,n,o,a,i={label:0,sent:function(){if(1&o[0])throw o[1];return o[1]},trys:[],ops:[]};return a={next:s(0),throw:s(1),return:s(2)},"function"==typeof Symbol&&(a[Symbol.iterator]=function(){return this}),a;function s(s){return function(l){return function(s){if(r)throw new TypeError("Generator is already executing.");for(;a&&(a=0,s[0]&&(i=0)),i;)try{if(r=1,n&&(o=2&s[0]?n.return:s[0]?n.throw||((o=n.return)&&o.call(n),0):n.next)&&!(o=o.call(n,s[1])).done)return o;switch(n=0,o&&(s=[2&s[0],o.value]),s[0]){case 0:case 1:o=s;break;case 4:return i.label++,{value:s[1],done:!1};case 5:i.label++,n=s[1],s=[0];continue;case 7:s=i.ops.pop(),i.trys.pop();continue;default:if(!((o=(o=i.trys).length>0&&o[o.length-1])||6!==s[0]&&2!==s[0])){i=0;continue}if(3===s[0]&&(!o||s[1]>o[0]&&s[1]0&&o[o.length-1])||6!==s[0]&&2!==s[0])){i=0;continue}if(3===s[0]&&(!o||s[1]>o[0]&&s[1]0&&o[o.length-1])||6!==s[0]&&2!==s[0])){i=0;continue}if(3===s[0]&&(!o||s[1]>o[0]&&s[1]0?(0,m.sprintf)( +!function(){"use strict";var e={20:function(e,t,r){var n=r(609),o=Symbol.for("react.element"),a=Symbol.for("react.fragment"),i=Object.prototype.hasOwnProperty,s=n.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED.ReactCurrentOwner,l={key:!0,ref:!0,__self:!0,__source:!0};function c(e,t,r){var n,a={},c=null,u=null;for(n in void 0!==r&&(c=""+r),void 0!==t.key&&(c=""+t.key),void 0!==t.ref&&(u=t.ref),t)i.call(t,n)&&!l.hasOwnProperty(n)&&(a[n]=t[n]);if(e&&e.defaultProps)for(n in t=e.defaultProps)void 0===a[n]&&(a[n]=t[n]);return{$$typeof:o,type:e,key:c,ref:u,props:a,_owner:s.current}}t.Fragment=a,t.jsx=c,t.jsxs=c},848:function(e,t,r){e.exports=r(20)},609:function(e){e.exports=window.React}},t={};function r(n){var o=t[n];if(void 0!==o)return o.exports;var a=t[n]={exports:{}};return e[n](a,a.exports,r),a.exports}r.n=function(e){var t=e&&e.__esModule?function(){return e.default}:function(){return e};return r.d(t,{a:t}),t},r.d=function(e,t){for(var n in t)r.o(t,n)&&!r.o(e,n)&&Object.defineProperty(e,n,{enumerable:!0,get:t[n]})},r.o=function(e,t){return Object.prototype.hasOwnProperty.call(e,t)},function(){var e,t,n,o,a,i,s,l,c,u,p,d=window.wp.data,y=window.wp.hooks,h=window.wp.plugins,f=((0,d.dispatch)("core/block-editor"),(0,d.dispatch)("core/editor"),(0,d.dispatch)("core/edit-post")),w=r(848),v=window.wp.components,g=window.wp.editor;void 0!==window.wp&&(p=null!==(t=null===(e=window.wp.editor)||void 0===e?void 0:e.PluginDocumentSettingPanel)&&void 0!==t?t:null!==(o=null===(n=window.wp.editPost)||void 0===n?void 0:n.PluginDocumentSettingPanel)&&void 0!==o?o:null===(a=window.wp.editSite)||void 0===a?void 0:a.PluginDocumentSettingPanel,null!==(s=null===(i=window.wp.editor)||void 0===i?void 0:i.PluginSidebar)&&void 0!==s||null!==(c=null===(l=window.wp.editPost)||void 0===l?void 0:l.PluginSidebar)&&void 0!==c||null===(u=window.wp.editSite)||void 0===u||u.PluginSidebar);var _,b,P=window.wp.element,m=window.wp.i18n,x=window.wp.wordcount,E=window.wp.primitives,A=(0,w.jsx)(E.SVG,{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 24 24",children:(0,w.jsx)(E.Path,{d:"M19.5 4.5h-7V6h4.44l-5.97 5.97 1.06 1.06L18 7.06v4.44h1.5v-7Zm-13 1a2 2 0 0 0-2 2v10a2 2 0 0 0 2 2h10a2 2 0 0 0 2-2v-3H17v3a.5.5 0 0 1-.5.5h-10a.5.5 0 0 1-.5-.5v-10a.5.5 0 0 1 .5-.5h3V5.5h-3Z"})}),S=function(){function e(){this._tkq=[],this.isLoaded=!1,this.isEnabled=!1,"undefined"!=typeof wpParselyTracksTelemetry&&(this.isEnabled=!0,this.loadTrackingLibrary())}return e.getInstance=function(){return window.wpParselyTelemetryInstance||Object.defineProperty(window,"wpParselyTelemetryInstance",{value:new e,writable:!1,configurable:!1,enumerable:!1}),window.wpParselyTelemetryInstance},e.prototype.loadTrackingLibrary=function(){var e=this,t=document.createElement("script");t.async=!0,t.src="//stats.wp.com/w.js",t.onload=function(){e.isLoaded=!0,e._tkq=window._tkq||[]},document.head.appendChild(t)},e.trackEvent=function(t){return r=this,n=arguments,a=function(t,r){var n;return void 0===r&&(r={}),function(e,t){var r,n,o,a,i={label:0,sent:function(){if(1&o[0])throw o[1];return o[1]},trys:[],ops:[]};return a={next:s(0),throw:s(1),return:s(2)},"function"==typeof Symbol&&(a[Symbol.iterator]=function(){return this}),a;function s(s){return function(l){return function(s){if(r)throw new TypeError("Generator is already executing.");for(;a&&(a=0,s[0]&&(i=0)),i;)try{if(r=1,n&&(o=2&s[0]?n.return:s[0]?n.throw||((o=n.return)&&o.call(n),0):n.next)&&!(o=o.call(n,s[1])).done)return o;switch(n=0,o&&(s=[2&s[0],o.value]),s[0]){case 0:case 1:o=s;break;case 4:return i.label++,{value:s[1],done:!1};case 5:i.label++,n=s[1],s=[0];continue;case 7:s=i.ops.pop(),i.trys.pop();continue;default:if(!((o=(o=i.trys).length>0&&o[o.length-1])||6!==s[0]&&2!==s[0])){i=0;continue}if(3===s[0]&&(!o||s[1]>o[0]&&s[1]=1e4&&(clearInterval(a),r("Telemetry library not loaded"))}),100);else r("Telemetry not enabled")}))},e.prototype.trackEvent=function(t,r){var n;this.isLoaded?(0!==t.indexOf(e.TRACKS_PREFIX)&&(t=e.TRACKS_PREFIX+t),this.isEventNameValid(t)?(r=this.prepareProperties(r),null===(n=this._tkq)||void 0===n||n.push(["recordEvent",t,r])):console.error("Error tracking event: Invalid event name")):console.error("Error tracking event: Telemetry not loaded")},e.prototype.isTelemetryEnabled=function(){return this.isEnabled},e.prototype.isProprietyValid=function(t){return e.PROPERTY_REGEX.test(t)},e.prototype.isEventNameValid=function(t){return e.EVENT_NAME_REGEX.test(t)},e.prototype.prepareProperties=function(e){return(e=this.sanitizeProperties(e)).parsely_version=wpParselyTracksTelemetry.version,wpParselyTracksTelemetry.user&&(e._ut=wpParselyTracksTelemetry.user.type,e._ui=wpParselyTracksTelemetry.user.id),wpParselyTracksTelemetry.vipgo_env&&(e.vipgo_env=wpParselyTracksTelemetry.vipgo_env),this.sanitizeProperties(e)},e.prototype.sanitizeProperties=function(e){var t=this,r={};return Object.keys(e).forEach((function(n){t.isProprietyValid(n)&&(r[n]=e[n])})),r},e.TRACKS_PREFIX="wpparsely_",e.EVENT_NAME_REGEX=/^(([a-z0-9]+)_){2}([a-z0-9_]+)$/,e.PROPERTY_REGEX=/^[a-z_][a-z0-9_]*$/,e}(),k=(S.trackEvent,function(e){void 0===e&&(e=null);var t="";(null==e?void 0:e.children)&&(t=e.children);var r="content-helper-error-message";return(null==e?void 0:e.className)&&(r+=" "+e.className),(0,w.jsx)("div",{className:r,"data-testid":null==e?void 0:e.testId,dangerouslySetInnerHTML:{__html:t}})}),T=(_=function(e,t){return _=Object.setPrototypeOf||{__proto__:[]}instanceof Array&&function(e,t){e.__proto__=t}||function(e,t){for(var r in t)Object.prototype.hasOwnProperty.call(t,r)&&(e[r]=t[r])},_(e,t)},function(e,t){if("function"!=typeof t&&null!==t)throw new TypeError("Class extends value "+String(t)+" is not a constructor or null");function __(){this.constructor=e}_(e,t),e.prototype=null===t?Object.create(t):(__.prototype=t.prototype,new __)});!function(e){e.AccessToFeatureDisabled="ch_access_to_feature_disabled",e.CannotFormulateApiQuery="ch_cannot_formulate_api_query",e.FetchError="fetch_error",e.HttpRequestFailed="http_request_failed",e.ParselyAborted="ch_parsely_aborted",e[e.ParselyApiForbidden=403]="ParselyApiForbidden",e.ParselyApiResponseContainsError="ch_response_contains_error",e.ParselyApiReturnedNoData="ch_parsely_api_returned_no_data",e.ParselyApiReturnedTooManyResults="ch_parsely_api_returned_too_many_results",e.PluginCredentialsNotSetMessageDetected="parsely_credentials_not_set_message_detected",e.PluginSettingsApiSecretNotSet="parsely_api_secret_not_set",e.PluginSettingsSiteIdNotSet="parsely_site_id_not_set",e.PostIsNotPublished="ch_post_not_published",e.UnknownError="ch_unknown_error",e.ParselySuggestionsApiAuthUnavailable="AUTH_UNAVAILABLE",e.ParselySuggestionsApiNoAuthentication="NO_AUTHENTICATION",e.ParselySuggestionsApiNoAuthorization="NO_AUTHORIZATION",e.ParselySuggestionsApiNoData="NO_DATA",e.ParselySuggestionsApiOpenAiError="OPENAI_ERROR",e.ParselySuggestionsApiOpenAiSchema="OPENAI_SCHEMA",e.ParselySuggestionsApiOpenAiUnavailable="OPENAI_UNAVAILABLE",e.ParselySuggestionsApiSchemaError="SCHEMA_ERROR"}(b||(b={}));var N=function(e){function t(r,n,o){void 0===o&&(o=(0,m.__)("Error: ","wp-parsely"));var a=this;r.startsWith(o)&&(o=""),(a=e.call(this,o+r)||this).hint=null,a.name=a.constructor.name,a.code=n;var i=[b.AccessToFeatureDisabled,b.ParselyApiForbidden,b.ParselyApiResponseContainsError,b.ParselyApiReturnedNoData,b.ParselyApiReturnedTooManyResults,b.PluginCredentialsNotSetMessageDetected,b.PluginSettingsApiSecretNotSet,b.PluginSettingsSiteIdNotSet,b.PostIsNotPublished,b.UnknownError,b.ParselySuggestionsApiAuthUnavailable,b.ParselySuggestionsApiNoAuthentication,b.ParselySuggestionsApiNoAuthorization,b.ParselySuggestionsApiNoData,b.ParselySuggestionsApiSchemaError];return a.retryFetch=!i.includes(a.code),Object.setPrototypeOf(a,t.prototype),a.code===b.AccessToFeatureDisabled?a.message=(0,m.__)("Access to this feature is disabled by the site's administration.","wp-parsely"):a.code===b.ParselySuggestionsApiNoAuthorization?a.message=(0,m.__)('This AI-powered feature is opt-in. To gain access, please submit a request here.',"wp-parsely"):a.code===b.ParselySuggestionsApiOpenAiError||a.code===b.ParselySuggestionsApiOpenAiUnavailable?a.message=(0,m.__)("The Parse.ly API returned an internal server error. Please retry with a different input, or try again later.","wp-parsely"):a.code===b.HttpRequestFailed&&a.message.includes("cURL error 28")?a.message=(0,m.__)("The Parse.ly API did not respond in a timely manner. Please try again later.","wp-parsely"):a.code===b.ParselySuggestionsApiSchemaError?a.message=(0,m.__)("The Parse.ly API returned a validation error. Please try again with different parameters.","wp-parsely"):a.code===b.ParselySuggestionsApiNoData?a.message=(0,m.__)("The Parse.ly API couldn't find any relevant data to fulfill the request. Please retry with a different input.","wp-parsely"):a.code===b.ParselySuggestionsApiOpenAiSchema?a.message=(0,m.__)("The Parse.ly API returned an incorrect response. Please try again later.","wp-parsely"):a.code===b.ParselySuggestionsApiAuthUnavailable&&(a.message=(0,m.__)("The Parse.ly API is currently unavailable. Please try again later.","wp-parsely")),a}return T(t,e),t.prototype.Message=function(e){return void 0===e&&(e=null),[b.PluginCredentialsNotSetMessageDetected,b.PluginSettingsSiteIdNotSet,b.PluginSettingsApiSecretNotSet].includes(this.code)?function(e){var t;return void 0===e&&(e=null),(0,w.jsx)(k,{className:null==e?void 0:e.className,testId:"empty-credentials-message",children:null!==(t=window.wpParselyEmptyCredentialsMessage)&&void 0!==t?t:(0,m.__)("Please ensure that the Site ID and API Secret given in the plugin's settings are correct.","wp-parsely")})}(e):(this.code===b.FetchError&&(this.hint=this.Hint((0,m.__)("This error can sometimes be caused by ad-blockers or browser tracking protections. Please add this site to any applicable allow lists and try again.","wp-parsely"))),this.code!==b.ParselyApiForbidden&&this.code!==b.ParselySuggestionsApiNoAuthentication||(this.hint=this.Hint((0,m.__)("Please ensure that the Site ID and API Secret given in the plugin's settings are correct.","wp-parsely"))),this.code===b.HttpRequestFailed&&(this.hint=this.Hint((0,m.__)("The Parse.ly API cannot be reached. Please verify that you are online.","wp-parsely"))),(0,w.jsx)(k,{className:null==e?void 0:e.className,testId:"error",children:"

".concat(this.message,"

").concat(this.hint?this.hint:"")}))},t.prototype.Hint=function(e){return'

'.concat((0,m.__)("Hint:","wp-parsely")," ").concat(e,"

")},t.prototype.createErrorSnackbar=function(){//.test(this.message)||(0,d.dispatch)("core/notices").createNotice("error",this.message,{type:"snackbar"})},t}(Error),j=function(e){var t=e.size,r=void 0===t?24:t,n=e.className,o=void 0===n?"wp-parsely-icon":n;return(0,w.jsxs)(v.SVG,{className:o,height:r,viewBox:"0 0 60 65",width:r,xmlns:"http://www.w3.org/2000/svg",children:[(0,w.jsx)(v.Path,{fill:"#5ba745",d:"M23.72,51.53c0-.18,0-.34-.06-.52a13.11,13.11,0,0,0-2.1-5.53A14.74,14.74,0,0,0,19.12,43c-.27-.21-.5-.11-.51.22l-.24,3.42c0,.33-.38.35-.49,0l-1.5-4.8a1.4,1.4,0,0,0-.77-.78,23.91,23.91,0,0,0-3.1-.84c-1.38-.24-3.39-.39-3.39-.39-.34,0-.45.21-.25.49l2.06,3.76c.2.27,0,.54-.29.33l-4.51-3.6a3.68,3.68,0,0,0-2.86-.48c-1,.16-2.44.46-2.44.46a.68.68,0,0,0-.39.25.73.73,0,0,0-.14.45S.41,43,.54,44a3.63,3.63,0,0,0,1.25,2.62L6.48,50c.28.2.09.49-.23.37l-4.18-.94c-.32-.12-.5,0-.4.37,0,0,.69,1.89,1.31,3.16a24,24,0,0,0,1.66,2.74,1.34,1.34,0,0,0,1,.52l5,.13c.33,0,.41.38.1.48L7.51,58c-.31.1-.34.35-.07.55a14.29,14.29,0,0,0,3.05,1.66,13.09,13.09,0,0,0,5.9.5,25.13,25.13,0,0,0,4.34-1,9.55,9.55,0,0,1-.08-1.2,9.32,9.32,0,0,1,3.07-6.91"}),(0,w.jsx)(v.Path,{fill:"#5ba745",d:"M59.7,41.53a.73.73,0,0,0-.14-.45.68.68,0,0,0-.39-.25s-1.43-.3-2.44-.46a3.64,3.64,0,0,0-2.86.48l-4.51,3.6c-.26.21-.49-.06-.29-.33l2.06-3.76c.2-.28.09-.49-.25-.49,0,0-2,.15-3.39.39a23.91,23.91,0,0,0-3.1.84,1.4,1.4,0,0,0-.77.78l-1.5,4.8c-.11.32-.48.3-.49,0l-.24-3.42c0-.33-.24-.43-.51-.22a14.74,14.74,0,0,0-2.44,2.47A13.11,13.11,0,0,0,36.34,51c0,.18,0,.34-.06.52a9.26,9.26,0,0,1,3,8.1,24.1,24.1,0,0,0,4.34,1,13.09,13.09,0,0,0,5.9-.5,14.29,14.29,0,0,0,3.05-1.66c.27-.2.24-.45-.07-.55l-3.22-1.17c-.31-.1-.23-.47.1-.48l5-.13a1.38,1.38,0,0,0,1-.52A24.6,24.6,0,0,0,57,52.92c.61-1.27,1.31-3.16,1.31-3.16.1-.33-.08-.49-.4-.37l-4.18.94c-.32.12-.51-.17-.23-.37l4.69-3.34A3.63,3.63,0,0,0,59.46,44c.13-1,.24-2.47.24-2.47"}),(0,w.jsx)(v.Path,{fill:"#5ba745",d:"M46.5,25.61c0-.53-.35-.72-.8-.43l-4.86,2.66c-.45.28-.56-.27-.23-.69l4.66-6.23a2,2,0,0,0,.28-1.68,36.51,36.51,0,0,0-2.19-4.89,34,34,0,0,0-2.81-3.94c-.33-.41-.74-.35-.91.16l-2.28,5.68c-.16.5-.6.48-.59-.05l.28-8.93a2.54,2.54,0,0,0-.66-1.64S35,4.27,33.88,3.27,30.78.69,30.78.69a1.29,1.29,0,0,0-1.54,0s-1.88,1.49-3.12,2.59-2.48,2.35-2.48,2.35A2.5,2.5,0,0,0,23,7.27l.27,8.93c0,.53-.41.55-.58.05l-2.29-5.69c-.17-.5-.57-.56-.91-.14a35.77,35.77,0,0,0-3,4.2,35.55,35.55,0,0,0-2,4.62,2,2,0,0,0,.27,1.67l4.67,6.24c.33.42.23,1-.22.69l-4.87-2.66c-.45-.29-.82-.1-.82.43a18.6,18.6,0,0,0,.83,5.07,20.16,20.16,0,0,0,5.37,7.77c3.19,3,5.93,7.8,7.45,11.08A9.6,9.6,0,0,1,30,49.09a9.31,9.31,0,0,1,2.86.45c1.52-3.28,4.26-8.11,7.44-11.09a20.46,20.46,0,0,0,5.09-7,19,19,0,0,0,1.11-5.82"}),(0,w.jsx)(v.Path,{fill:"#5ba745",d:"M36.12,58.44A6.12,6.12,0,1,1,30,52.32a6.11,6.11,0,0,1,6.12,6.12"})]})},C=window.wp.url,I=window.wp.apiFetch,O=r.n(I),R=function(){function e(){this.abortControllers=new Map}return e.prototype.cancelRequest=function(e){if(e)(t=this.abortControllers.get(e))&&(t.abort(),this.abortControllers.delete(e));else{var t,r=Array.from(this.abortControllers.keys()).pop();r&&(t=this.abortControllers.get(r))&&(t.abort(),this.abortControllers.delete(r))}},e.prototype.cancelAll=function(){this.abortControllers.forEach((function(e){return e.abort()})),this.abortControllers.clear()},e.prototype.getOrCreateController=function(e){if(e&&this.abortControllers.has(e))return{abortController:this.abortControllers.get(e),abortId:e};var t=null!=e?e:"auto-"+Date.now(),r=new AbortController;return this.abortControllers.set(t,r),{abortController:r,abortId:t}},e.prototype.fetch=function(e,t){return r=this,n=void 0,a=function(){var r,n,o,a,i,s;return function(e,t){var r,n,o,a,i={label:0,sent:function(){if(1&o[0])throw o[1];return o[1]},trys:[],ops:[]};return a={next:s(0),throw:s(1),return:s(2)},"function"==typeof Symbol&&(a[Symbol.iterator]=function(){return this}),a;function s(s){return function(l){return function(s){if(r)throw new TypeError("Generator is already executing.");for(;a&&(a=0,s[0]&&(i=0)),i;)try{if(r=1,n&&(o=2&s[0]?n.return:s[0]?n.throw||((o=n.return)&&o.call(n),0):n.next)&&!(o=o.call(n,s[1])).done)return o;switch(n=0,o&&(s=[2&s[0],o.value]),s[0]){case 0:case 1:o=s;break;case 4:return i.label++,{value:s[1],done:!1};case 5:i.label++,n=s[1],s=[0];continue;case 7:s=i.ops.pop(),i.trys.pop();continue;default:if(!((o=(o=i.trys).length>0&&o[o.length-1])||6!==s[0]&&2!==s[0])){i=0;continue}if(3===s[0]&&(!o||s[1]>o[0]&&s[1]0&&o[o.length-1])||6!==s[0]&&2!==s[0])){i=0;continue}if(3===s[0]&&(!o||s[1]>o[0]&&s[1]0&&o[o.length-1])||6!==s[0]&&2!==s[0])){i=0;continue}if(3===s[0]&&(!o||s[1]>o[0]&&s[1]0?(0,m.sprintf)( // Translators: %1$s the number of words in the excerpt. // Translators: %1$s the number of words in the excerpt. (0,m._n)("%1$s word","%1$s words",e,"wp-parsely"),e):"")}),[h.currentExcerpt,k]),(0,P.useEffect)((function(){var e=document.querySelector(".editor-post-excerpt textarea");e&&(e.scrollTop=0)}),[h.newExcerptGeneratedCount]),(0,w.jsxs)("div",{className:"editor-post-excerpt",children:[(0,w.jsxs)("div",{style:{position:"relative"},children:[t&&(0,w.jsx)("div",{className:"editor-post-excerpt__loading_animation",children:(0,w.jsx)(H,{})}),(0,w.jsx)(v.TextareaControl,{__nextHasNoMarginBottom:!0,label:(0,m.__)("Write an excerpt (optional)","wp-parsely"),className:"editor-post-excerpt__textarea",onChange:function(e){h.isUnderReview||_({excerpt:e}),f(F(F({},h),{currentExcerpt:e})),l(!0)},onKeyUp:function(){var e;if(s)l(!1);else{var t=document.querySelector(".editor-post-excerpt textarea"),r=null!==(e=null==t?void 0:t.textContent)&&void 0!==e?e:"";f(F(F({},h),{currentExcerpt:r}))}},value:t?"":h.isUnderReview?h.currentExcerpt:k,help:u||null})]}),(0,w.jsxs)(v.Button,{href:(0,m.__)("https://wordpress.org/documentation/article/page-post-settings-sidebar/#excerpt","wp-parsely"),target:"_blank",variant:"link",children:[(0,m.__)("Learn more about manual excerpts","wp-parsely"),(0,w.jsx)(v.Icon,{icon:A,size:18,className:"parsely-external-link-icon"})]}),(0,w.jsxs)("div",{className:"wp-parsely-excerpt-generator",children:[(0,w.jsxs)("div",{className:"wp-parsely-excerpt-generator-header",children:[(0,w.jsx)(j,{size:16}),(0,w.jsxs)("div",{className:"wp-parsely-excerpt-generator-header-label",children:[(0,m.__)("Generate With Parse.ly","wp-parsely"),(0,w.jsx)("span",{className:"beta-label",children:(0,m.__)("Beta","wp-parsely")})]})]}),o&&(0,w.jsx)(v.Notice,{className:"wp-parsely-excerpt-generator-error",onRemove:function(){return a(void 0)},status:"info",children:o.Message()}),(0,w.jsx)("div",{className:"wp-parsely-excerpt-generator-controls",children:h.isUnderReview?(0,w.jsxs)(w.Fragment,{children:[(0,w.jsx)(v.Button,{variant:"secondary",onClick:function(){return L(void 0,void 0,void 0,(function(){return M(this,(function(e){switch(e.label){case 0:return[4,_({excerpt:h.currentExcerpt})];case 1:return e.sent(),f(F(F({},h),{isUnderReview:!1})),S.trackEvent("excerpt_generator_accepted"),[2]}}))}))},children:(0,m.__)("Accept","wp-parsely")}),(0,w.jsx)(v.Button,{isDestructive:!0,variant:"secondary",onClick:function(){return L(void 0,void 0,void 0,(function(){return M(this,(function(e){return _({excerpt:h.oldExcerpt}),f(F(F({},h),{currentExcerpt:h.oldExcerpt,isUnderReview:!1})),S.trackEvent("excerpt_generator_discarded"),[2]}))}))},children:(0,m.__)("Discard","wp-parsely")})]}):(0,w.jsxs)(v.Button,{onClick:function(){return L(void 0,void 0,void 0,(function(){var e,t;return M(this,(function(n){switch(n.label){case 0:r(!0),a(void 0),n.label=1;case 1:return n.trys.push([1,3,4,5]),S.trackEvent("excerpt_generator_pressed"),[4,D.getInstance().generateExcerpt(C,T)];case 2:return e=n.sent(),f({currentExcerpt:e,isUnderReview:!0,newExcerptGeneratedCount:h.newExcerptGeneratedCount+1,oldExcerpt:k}),[3,5];case 3:return(t=n.sent())instanceof N?a(t):(a(new N((0,m.__)("An unknown error occurred.","wp-parsely"),b.UnknownError)),console.error(t)),[3,5];case 4:return r(!1),[7];case 5:return[2]}}))}))},variant:"primary",isBusy:t,disabled:t||!T,children:[t&&(0,m.__)("Generating Excerpt…","wp-parsely"),!t&&h.newExcerptGeneratedCount>0&&(0,m.__)("Regenerate Excerpt","wp-parsely"),!t&&0===h.newExcerptGeneratedCount&&(0,m.__)("Generate Excerpt","wp-parsely")]})}),(0,w.jsxs)(v.Button,{href:"https://docs.parse.ly/plugin-content-helper/#h-excerpt-generator-beta",target:"_blank",variant:"link",children:[(0,m.__)("Learn more about Parse.ly AI","wp-parsely"),(0,w.jsx)(v.Icon,{icon:A,size:18,className:"parsely-external-link-icon"})]})]})]})},H=function(){return(0,w.jsx)(v.Animate,{type:"loading",children:function(e){var t=e.className;return(0,w.jsx)("span",{className:t,children:(0,m.__)("Generating…","wp-parsely")})}})},q=function(){return(0,w.jsx)(g.PostTypeSupportCheck,{supportKeys:"excerpt",children:(0,w.jsx)(p,{name:"parsely-post-excerpt",title:(0,m.__)("Excerpt","wp-parsely"),children:(0,w.jsx)(G,{})})})};(0,y.addFilter)("plugins.registerPlugin","wp-parsely-excerpt-generator",(function(e,t){var r,n,o;return"wp-parsely-block-editor-sidebar"!==t||((null===(r=null===window||void 0===window?void 0:window.Jetpack_Editor_Initial_State)||void 0===r?void 0:r.available_blocks["ai-content-lens"])&&(console.log("Parse.ly: Jetpack AI is enabled and will be disabled."),(0,y.removeFilter)("blocks.registerBlockType","jetpack/ai-content-lens-features")),(0,h.registerPlugin)("wp-parsely-excerpt-generator",{render:q}),(null===(n=(0,d.dispatch)("core/editor"))||void 0===n?void 0:n.removeEditorPanel)?null===(o=(0,d.dispatch)("core/editor"))||void 0===o||o.removeEditorPanel("post-excerpt"):null==f||f.removeEditorPanel("post-excerpt")),e}),1e3)}()}(); \ No newline at end of file diff --git a/src/content-helper/editor-sidebar/smart-linking/provider.ts b/src/content-helper/editor-sidebar/smart-linking/provider.ts index ce30569f5d..66b3215d47 100644 --- a/src/content-helper/editor-sidebar/smart-linking/provider.ts +++ b/src/content-helper/editor-sidebar/smart-linking/provider.ts @@ -140,12 +140,12 @@ export class SmartLinkingProvider extends BaseProvider { ): Promise { const response = await this.fetch( { method: 'POST', - path: addQueryArgs( '/wp-parsely/v1/content-suggestions/suggest-linked-reference', { + path: addQueryArgs( '/wp-parsely/v2/content-helper/smart-linking/generate', { max_links: maxLinksPerPost, } ), data: { url_exclusion_list: urlExclusionList, - text: content, + content, }, } ); @@ -165,7 +165,7 @@ export class SmartLinkingProvider extends BaseProvider { public async addSmartLink( postID: number, linkSuggestion: SmartLink ): Promise { return await this.fetch( { method: 'POST', - path: `/wp-parsely/v1/smart-linking/${ postID }/add`, + path: `/wp-parsely/v2/content-helper/smart-linking/${ postID }/add`, data: { link: linkSuggestion, }, @@ -195,7 +195,7 @@ export class SmartLinkingProvider extends BaseProvider { return await this.fetch( { method: 'POST', - path: `/wp-parsely/v1/smart-linking/${ postID }/add-multiple`, + path: `/wp-parsely/v2/content-helper/smart-linking/${ postID }/add-multiple`, data: { links: linkSuggestions, }, @@ -230,7 +230,7 @@ export class SmartLinkingProvider extends BaseProvider { return await this.fetch( { method: 'POST', - path: `/wp-parsely/v1/smart-linking/${ postID }/set`, + path: `/wp-parsely/v2/content-helper/smart-linking/${ postID }/set`, data: { links: appliedSmartLinks, }, @@ -249,7 +249,7 @@ export class SmartLinkingProvider extends BaseProvider { public async getSmartLinks( postID: number ): Promise { return await this.fetch( { method: 'GET', - path: `/wp-parsely/v1/smart-linking/${ postID }/get`, + path: `/wp-parsely/v2/content-helper/smart-linking/${ postID }/get`, } ); } @@ -265,7 +265,7 @@ export class SmartLinkingProvider extends BaseProvider { public async getPostTypeByURL( url: string ): Promise { return await this.fetch( { method: 'POST', - path: '/wp-parsely/v1/smart-linking/url-to-post-type', + path: '/wp-parsely/v2/content-helper/smart-linking/url-to-post-type', data: { url, }, diff --git a/src/content-helper/editor-sidebar/title-suggestions/provider.ts b/src/content-helper/editor-sidebar/title-suggestions/provider.ts index c6095bf5e1..a5fb0d4a14 100644 --- a/src/content-helper/editor-sidebar/title-suggestions/provider.ts +++ b/src/content-helper/editor-sidebar/title-suggestions/provider.ts @@ -52,7 +52,7 @@ export class TitleSuggestionsProvider extends BaseProvider { public async generateTitles( content: string, limit: number = 3, tone: ToneProp, persona: PersonaProp ): Promise { const response = this.fetch( { method: 'POST', - path: addQueryArgs( '/wp-parsely/v1/content-suggestions/suggest-headline', { + path: addQueryArgs( '/wp-parsely/v2/content-helper/title-suggestions/generate', { limit, tone: getToneLabel( tone ), persona: getPersonaLabel( persona ), diff --git a/src/content-helper/excerpt-generator/provider.ts b/src/content-helper/excerpt-generator/provider.ts index 6fb4dcaf0b..9716dcd878 100644 --- a/src/content-helper/excerpt-generator/provider.ts +++ b/src/content-helper/excerpt-generator/provider.ts @@ -51,7 +51,7 @@ export class ExcerptGeneratorProvider extends BaseProvider { return await this.fetch( { method: 'POST', - path: addQueryArgs( '/wp-parsely/v1/content-suggestions/suggest-brief', { + path: addQueryArgs( '/wp-parsely/v2/content-helper/excerpt-generator/generate', { title, } ), data: { From 04de75080752ee4818138baaa1cbb4a75f1fe72f Mon Sep 17 00:00:00 2001 From: Henrique Mouta Date: Thu, 22 Aug 2024 14:37:54 +0100 Subject: [PATCH 05/49] Remove unused code and initializations --- .../class-smart-linking-endpoint.php | 602 ------------------ .../class-suggest-brief-api-proxy.php | 152 ----- .../class-suggest-headline-api-proxy.php | 123 ---- ...ass-suggest-linked-reference-api-proxy.php | 162 ----- wp-parsely.php | 31 +- 5 files changed, 1 insertion(+), 1069 deletions(-) delete mode 100644 src/Endpoints/content-helper/class-smart-linking-endpoint.php delete mode 100644 src/Endpoints/content-suggestions/class-suggest-brief-api-proxy.php delete mode 100644 src/Endpoints/content-suggestions/class-suggest-headline-api-proxy.php delete mode 100644 src/Endpoints/content-suggestions/class-suggest-linked-reference-api-proxy.php diff --git a/src/Endpoints/content-helper/class-smart-linking-endpoint.php b/src/Endpoints/content-helper/class-smart-linking-endpoint.php deleted file mode 100644 index d52e7e2c08..0000000000 --- a/src/Endpoints/content-helper/class-smart-linking-endpoint.php +++ /dev/null @@ -1,602 +0,0 @@ -get_param( 'post_id' ); - if ( is_numeric( $temp_post_id ) ) { - $post_id = intval( $temp_post_id ); - } - } - - $can_access_pch = Permissions::current_user_can_use_pch_feature( - 'smart_linking', - $this->parsely->get_options()['content_helper'], - $post_id - ); - - // Check if the current user has the smart linking capability. - $has_capability = current_user_can( - // phpcs:ignore WordPress.WP.Capabilities.Undetermined - $this->apply_capability_filters( - Base_Endpoint::DEFAULT_ACCESS_CAPABILITY - ) - ); - - return $can_access_pch && $has_capability; - } - - /** - * Registers the endpoints. - * - * @since 3.16.0 - */ - public function run(): void { - /** - * POST /smart-linking/url-to-post-type - * Converts a URL to a post type. - */ - $this->register_endpoint( - static::ENDPOINT . '/url-to-post-type', - 'url_to_post_type', - array( 'POST' ) - ); - - /** - * GET /smart-linking/{post_id}/get - * Gets the smart links for a post. - */ - $this->register_endpoint_with_args( - static::ENDPOINT . '/(?P\d+)/get', - 'get_smart_links', - array( 'GET' ), - array( - 'post_id' => array( - 'required' => true, - 'description' => __( 'The post ID.', 'wp-parsely' ), - 'validate_callback' => array( $this, 'private_api_request_validate_post_id' ), - ), - ) - ); - - /** - * POST /smart-linking/{post_id}/add - * Adds a smart link to a post. - */ - $this->register_endpoint_with_args( - static::ENDPOINT . '/(?P\d+)/add', - 'add_smart_link', - array( 'POST' ), - array( - 'post_id' => array( - 'required' => true, - 'description' => __( 'The post ID.', 'wp-parsely' ), - 'validate_callback' => array( $this, 'private_api_request_validate_post_id' ), - ), - 'link' => array( - 'required' => true, - 'type' => 'array', - 'description' => __( 'The smart link data to add.', 'wp-parsely' ), - 'validate_callback' => array( $this, 'private_api_request_validate_smart_link_params' ), - ), - 'update' => array( - 'type' => 'boolean', - 'description' => __( 'Whether to update the existing smart link.', 'wp-parsely' ), - 'default' => false, - ), - ) - ); - - /** - * POST /smart-linking/{post_id}/add-multiple - * Adds multiple smart links to a post. - */ - $this->register_endpoint_with_args( - static::ENDPOINT . '/(?P\d+)/add-multiple', - 'add_multiple_smart_links', - array( 'POST' ), - array( - 'post_id' => array( - 'required' => true, - 'description' => __( 'The post ID.', 'wp-parsely' ), - 'validate_callback' => array( $this, 'private_api_request_validate_post_id' ), - ), - 'links' => array( - 'required' => true, - 'type' => 'array', - 'description' => __( 'The multiple smart links data to add.', 'wp-parsely' ), - 'validate_callback' => array( $this, 'private_api_request_validate_multiple_smart_links' ), - ), - 'update' => array( - 'type' => 'boolean', - 'description' => __( 'Whether to update the existing smart links.', 'wp-parsely' ), - 'default' => false, - ), - ) - ); - - /** - * POST /smart-linking/{post_id}/set - * Updates the smart links of a given post and removes the ones that are not in the request. - */ - $this->register_endpoint_with_args( - static::ENDPOINT . '/(?P\d+)/set', - 'set_smart_links', - array( 'POST' ), - array( - 'post_id' => array( - 'required' => true, - 'description' => __( 'The post ID.', 'wp-parsely' ), - 'validate_callback' => array( $this, 'private_api_request_validate_post_id' ), - ), - 'links' => array( - 'required' => true, - 'type' => 'array', - 'description' => __( 'The smart links data to set.', 'wp-parsely' ), - 'validate_callback' => array( $this, 'private_api_request_validate_multiple_smart_links' ), - ), - ) - ); - } - - /** - * API Endpoint: POST /smart-linking/url-to-post-type. - * - * Converts a URL to a post type. - * - * @since 3.16.0 - * - * @param WP_REST_Request $request The request object. - * @return WP_REST_Response The response object. - */ - public function url_to_post_type( WP_REST_Request $request ): WP_REST_Response { - $url = $request->get_param( 'url' ); - - if ( ! is_string( $url ) ) { - return new WP_REST_Response( - array( - 'error' => array( - 'name' => 'invalid_request', - 'message' => __( 'Invalid request body.', 'wp-parsely' ), - ), - ), - 400 - ); - } - - $post_id = 0; - $cache = wp_cache_get( $url, 'wp_parsely_smart_link_url_to_postid' ); - - if ( is_integer( $cache ) ) { - $post_id = $cache; - } elseif ( function_exists( 'wpcom_vip_url_to_postid' ) ) { - $post_id = wpcom_vip_url_to_postid( $url ); - } else { - // phpcs:ignore WordPressVIPMinimum.Functions.RestrictedFunctions.url_to_postid_url_to_postid - $post_id = url_to_postid( $url ); - wp_cache_set( $url, $post_id, 'wp_parsely_smart_link_url_to_postid' ); - } - - $response = array( - 'data' => array( - 'post_id' => false, - 'post_type' => false, - ), - ); - - if ( 0 !== $post_id ) { - $response['data']['post_id'] = $post_id; - $response['data']['post_type'] = get_post_type( $post_id ); - } - - return new WP_REST_Response( $response, 200 ); - } - - - /** - * API Endpoint: GET /smart-linking/{post_id}/get. - * - * Gets the smart links for a post. - * - * @since 3.16.0 - * - * @param WP_REST_Request $request The request object. - * @return WP_REST_Response The response object. - */ - public function get_smart_links( WP_REST_Request $request ): WP_REST_Response { - /** - * The post object. - * - * @var WP_Post $post - */ - $post = $request->get_param( 'post' ); - - $outbound_links = Smart_Link::get_outbound_smart_links( $post->ID ); - $inbound_links = Smart_Link::get_inbound_smart_links( $post->ID ); - - $response = array( - 'outbound' => $this->serialize_smart_links( $outbound_links ), - 'inbound' => $this->serialize_smart_links( $inbound_links ), - ); - - return new WP_REST_Response( array( 'data' => $response ), 200 ); - } - - - /** - * API Endpoint: POST /smart-linking/{post_id}/add. - * - * Adds a smart link to a post. - * If the update parameter is set to true, the existing smart link will be updated. - * - * @since 3.16.0 - * - * @param WP_REST_Request $request The request object. - * @return WP_REST_Response The response object. - */ - public function add_smart_link( WP_REST_Request $request ): WP_REST_Response { - /** - * The Smart Link model. - * - * @var Smart_Link $smart_link - */ - $smart_link = $request->get_param( 'smart_link' ); - $should_update = $request->get_param( 'update' ) === true; - - if ( $smart_link->exists() && ! $should_update ) { - return new WP_REST_Response( - array( - 'error' => array( - 'name' => 'smart_link_exists', - 'message' => __( 'Smart link already exists.', 'wp-parsely' ), - ), - ), - 409 // HTTP Conflict. - ); - } - - // The smart link properties are set in the validate callback. - $saved = $smart_link->save(); - if ( ! $saved ) { - return new WP_REST_Response( - array( - 'error' => array( - 'name' => 'add_smart_link_failed', - 'message' => __( 'Failed to add the smart link.', 'wp-parsely' ), - ), - ), - 500 - ); - } - - return new WP_REST_Response( - array( - 'data' => json_decode( $smart_link->serialize() ), - ), - 200 - ); - } - - /** - * API Endpoint: POST /smart-linking/{post_id}/add_multiple. - * - * Adds multiple smart links to a post. - * - * @since 3.16.0 - * - * @param WP_REST_Request $request The request object. - * @return WP_REST_Response The response object. - */ - public function add_multiple_smart_links( WP_REST_Request $request ): WP_REST_Response { - /** - * Array of Smart Link models. - * - * @var Smart_Link[] $smart_links - */ - $smart_links = $request->get_param( 'smart_links' ); - $should_update = $request->get_param( 'update' ) === true; - - $added_links = array(); - $updated_links = array(); - $failed_links = array(); - - foreach ( $smart_links as $smart_link ) { - if ( $smart_link->exists() && ! $should_update ) { - $failed_links[] = $smart_link; - continue; - } - - $updated_link = $smart_link->exists() && $should_update; - - // The smart link properties are set in the validate callback. - $saved = $smart_link->save(); - - if ( ! $saved ) { - $failed_links[] = $smart_link; - continue; - } - - if ( $updated_link ) { - $updated_links[] = $smart_link; - } else { - $added_links[] = $smart_link; - } - } - - // If no link was added, return an error response. - if ( count( $added_links ) === 0 && count( $updated_links ) === 0 ) { - return new WP_REST_Response( - array( - 'error' => array( - 'name' => 'add_smart_link_failed', - 'message' => __( 'Failed to add all the smart links.', 'wp-parsely' ), - ), - ), - 500 - ); - } - - $response = array(); - if ( count( $added_links ) > 0 ) { - $response['added'] = $this->serialize_smart_links( $added_links ); - } - if ( count( $failed_links ) > 0 ) { - $response['failed'] = $this->serialize_smart_links( $failed_links ); - } - if ( count( $updated_links ) > 0 ) { - $response['updated'] = $this->serialize_smart_links( $updated_links ); - } - - return new WP_REST_Response( array( 'data' => $response ), 200 ); - } - - /** - * API Endpoint: POST /smart-linking/{post_id}/set. - * - * Updates the smart links of a given post and removes the ones that are not in the request. - * - * @since 3.16.0 - * - * @param WP_REST_Request $request The request object. - * @return WP_REST_Response The response object. - */ - public function set_smart_links( WP_REST_Request $request ): WP_REST_Response { - /** - * The post object. - * - * @var WP_Post $post - */ - $post = $request->get_param( 'post' ); - - /** - * Array of Smart Link models provided in the request. - * - * @var Smart_Link[] $smart_links - */ - $smart_links = $request->get_param( 'smart_links' ); - - // Get the current stored smart links. - $existing_links = Smart_Link::get_outbound_smart_links( $post->ID ); - $removed_links = array(); - - foreach ( $existing_links as $existing_link ) { - $found = false; - foreach ( $smart_links as $smart_link ) { - if ( $smart_link->get_uid() === $existing_link->get_uid() ) { - $found = true; - break; - } - } - - if ( ! $found ) { - $removed_links[] = $existing_link; - $existing_link->delete(); - } - } - - $saved_links = array(); - $failed_links = array(); - - foreach ( $smart_links as $smart_link ) { - // The smart link properties are set in the validate callback. - $saved = $smart_link->save(); - - if ( ! $saved ) { - $failed_links[] = $smart_link; - continue; - } - - $saved_links[] = $smart_link; - } - - $response = array( - 'saved' => $this->serialize_smart_links( $saved_links ), - 'removed' => $this->serialize_smart_links( $removed_links ), - ); - - if ( count( $failed_links ) > 0 ) { - $response['failed'] = $this->serialize_smart_links( $failed_links ); - } - - return new WP_REST_Response( array( 'data' => $response ), 200 ); - } - - /** - * Validates the post ID parameter. - * - * The callback sets the post object in the request object if the parameter is valid. - * - * @since 3.16.0 - * @access private - * - * @param string $param The parameter value. - * @param WP_REST_Request $request The request object. - * @return bool Whether the parameter is valid. - */ - public function private_api_request_validate_post_id( string $param, WP_REST_Request $request ): bool { - if ( ! is_numeric( $param ) ) { - return false; - } - - $param = filter_var( $param, FILTER_VALIDATE_INT ); - - if ( false === $param ) { - return false; - } - - // Validate if the post ID exists. - $post = get_post( $param ); - // Set the post attribute in the request. - $request->set_param( 'post', $post ); - - return null !== $post; - } - - /** - * Validates the smart link parameters. - * - * The callback sets the smart link object in the request object if the parameters are valid. - * - * @since 3.16.0 - * @access private - * - * @param array $params The parameters. - * @param WP_REST_Request $request The request object. - * @return bool Whether the parameters are valid. - */ - public function private_api_request_validate_smart_link_params( array $params, WP_REST_Request $request ): bool { - $required_params = array( 'uid', 'href', 'title', 'text', 'offset' ); - - foreach ( $required_params as $param ) { - if ( ! isset( $params[ $param ] ) ) { - return false; - } - } - - $encoded_data = wp_json_encode( $params ); - if ( false === $encoded_data ) { - return false; - } - - $post_id = $request->get_param( 'post_id' ); - if ( ! is_numeric( $post_id ) ) { - return false; - } - - if ( ! is_string( $params['uid'] ) ) { - return false; - } - - // Try to get the smart link from the UID. - $smart_link = Smart_Link::get_smart_link( $params['uid'], intval( $post_id ) ); - if ( $smart_link->exists() ) { - // Update the smart link with the new data. - $smart_link->set_href( $params['href'] ); - $smart_link->title = $params['title']; - $smart_link->text = $params['text']; - $smart_link->offset = $params['offset']; - } else { - /** - * The Smart Link model. - * - * @var Smart_Link $smart_link - */ - $smart_link = Smart_Link::deserialize( $encoded_data ); - $smart_link->set_source_post_id( intval( $post_id ) ); - } - - // Set the smart link attribute in the request. - $request->set_param( 'smart_link', $smart_link ); - - return true; - } - - /** - * Validates the multiple smart link parameters. - * - * The callback sets the smart links object in the request object if the parameters are valid. - * - * @since 3.16.0 - * @access private - * - * @param array> $param The parameter value. - * @param WP_REST_Request $request The request object. - * @return bool Whether the parameter is valid. - */ - public function private_api_request_validate_multiple_smart_links( array $param, WP_REST_Request $request ): bool { - $smart_links = array(); - - foreach ( $param as $link ) { - if ( $this->private_api_request_validate_smart_link_params( $link, $request ) ) { - $smart_link = $request->get_param( 'smart_link' ); - $smart_links[] = $smart_link; - } else { - return false; - } - } - $request->set_param( 'smart_link', null ); - $request->set_param( 'smart_links', $smart_links ); - - return true; - } - - /** - * Serializes an array of Smart Links. - * - * @since 3.16.0 - * - * @param Smart_Link[] $links The Smart Links to serialize. - * @return array The serialized Smart Links. - */ - private function serialize_smart_links( array $links ): array { - return array_map( - function ( Smart_Link $link ) { - return json_decode( $link->serialize(), true ); - }, - $links - ); - } -} diff --git a/src/Endpoints/content-suggestions/class-suggest-brief-api-proxy.php b/src/Endpoints/content-suggestions/class-suggest-brief-api-proxy.php deleted file mode 100644 index 78bb9c3270..0000000000 --- a/src/Endpoints/content-suggestions/class-suggest-brief-api-proxy.php +++ /dev/null @@ -1,152 +0,0 @@ -suggest_brief_api = new Suggest_Brief_API( $parsely ); - parent::__construct( $parsely, $this->suggest_brief_api ); - } - - /** - * Registers the endpoint's WP REST route. - * - * @since 3.13.0 - */ - public function run(): void { - $this->register_endpoint( '/content-suggestions/suggest-brief', array( 'POST' ) ); - } - - /** - * Generates the final data from the passed response. - * - * @since 3.13.0 - * - * @param array $response The response received by the proxy. - * @return array The generated data. - */ - protected function generate_data( $response ): array { - // Unused function. - return $response; - } - - /** - * Cached "proxy" to the Parse.ly API endpoint. - * - * @since 3.13.0 - * - * @param WP_REST_Request $request The request object. - * @return stdClass|WP_Error stdClass containing the data or a WP_Error object on failure. - */ - public function get_items( WP_REST_Request $request ) { - $validation = $this->validate_apikey_and_secret(); - if ( is_wp_error( $validation ) ) { - return $validation; - } - - $pch_options = $this->parsely->get_options()['content_helper']; - if ( ! Permissions::current_user_can_use_pch_feature( 'excerpt_suggestions', $pch_options ) ) { - return new WP_Error( 'ch_access_to_feature_disabled', '', array( 'status' => 403 ) ); - } - - /** - * The post content to be sent to the API. - * - * @var string|null $post_content - */ - $post_content = $request->get_param( 'content' ); - - /** - * The post title to be sent to the API. - * - * @var string|null $post_title - */ - $post_title = $request->get_param( 'title' ); - - /** - * The persona to be sent to the API. - * - * @var string|null $persona - */ - $persona = $request->get_param( 'persona' ); - - /** - * The style to be sent to the API. - * - * @var string|null $style - */ - $style = $request->get_param( 'style' ); - - if ( null === $post_content ) { - return new WP_Error( - 'parsely_content_not_set', - __( 'A post content must be set to use this endpoint', 'wp-parsely' ), - array( 'status' => 403 ) - ); - } - - if ( null === $post_title ) { - return new WP_Error( - 'parsely_title_not_set', - __( 'A post title must be set to use this endpoint', 'wp-parsely' ), - array( 'status' => 403 ) - ); - } - - if ( null === $persona ) { - $persona = 'journalist'; - } - - if ( null === $style ) { - $style = 'neutral'; - } - - $response = $this->suggest_brief_api->get_suggestion( $post_title, $post_content, $persona, $style ); - - if ( is_wp_error( $response ) ) { - return $response; - } - - return (object) array( - 'data' => $response, - ); - } -} diff --git a/src/Endpoints/content-suggestions/class-suggest-headline-api-proxy.php b/src/Endpoints/content-suggestions/class-suggest-headline-api-proxy.php deleted file mode 100644 index 49e02a77ab..0000000000 --- a/src/Endpoints/content-suggestions/class-suggest-headline-api-proxy.php +++ /dev/null @@ -1,123 +0,0 @@ -suggest_headline_api = new Suggest_Headline_API( $parsely ); - parent::__construct( $parsely, $this->suggest_headline_api ); - } - - /** - * Registers the endpoint's WP REST route. - * - * @since 3.12.0 - */ - public function run(): void { - $this->register_endpoint( '/content-suggestions/suggest-headline', array( 'POST' ) ); - } - - /** - * Generates the final data from the passed response. - * - * @since 3.12.0 - * - * @param array $response The response received by the proxy. - * @return array The generated data. - */ - protected function generate_data( $response ): array { - // Unused function. - return $response; - } - - /** - * Cached "proxy" to the Parse.ly API endpoint. - * - * @since 3.12.0 - * - * @param WP_REST_Request $request The request object. - * @return stdClass|WP_Error stdClass containing the data or a WP_Error object on failure. - */ - public function get_items( WP_REST_Request $request ) { - $validation = $this->validate_apikey_and_secret(); - if ( is_wp_error( $validation ) ) { - return $validation; - } - - $pch_options = $this->parsely->get_options()['content_helper']; - if ( ! Permissions::current_user_can_use_pch_feature( 'title_suggestions', $pch_options ) ) { - return new WP_Error( 'ch_access_to_feature_disabled', '', array( 'status' => 403 ) ); - } - - /** - * The post content to be sent to the API. - * - * @var string|null $post_content - */ - $post_content = $request->get_param( 'content' ); - - if ( null === $post_content ) { - return new WP_Error( - 'parsely_content_not_set', - __( 'A post content must be set to use this endpoint', 'wp-parsely' ), - array( 'status' => 403 ) - ); - } - - $limit = is_numeric( $request->get_param( 'limit' ) ) ? intval( $request->get_param( 'limit' ) ) : 3; - $tone = is_string( $request->get_param( 'tone' ) ) ? $request->get_param( 'tone' ) : 'neutral'; - $persona = is_string( $request->get_param( 'persona' ) ) ? $request->get_param( 'persona' ) : 'journalist'; - - if ( 0 === $limit ) { - $limit = 3; - } - - $response = $this->suggest_headline_api->get_titles( $post_content, $limit, $persona, $tone ); - - if ( is_wp_error( $response ) ) { - return $response; - } - - return (object) array( - 'data' => $response, - ); - } -} diff --git a/src/Endpoints/content-suggestions/class-suggest-linked-reference-api-proxy.php b/src/Endpoints/content-suggestions/class-suggest-linked-reference-api-proxy.php deleted file mode 100644 index c224155edb..0000000000 --- a/src/Endpoints/content-suggestions/class-suggest-linked-reference-api-proxy.php +++ /dev/null @@ -1,162 +0,0 @@ -suggest_linked_reference_api = new Suggest_Linked_Reference_API( $parsely ); - parent::__construct( $parsely, $this->suggest_linked_reference_api ); - } - - /** - * Registers the endpoint's WP REST route. - * - * @since 3.14.0 - */ - public function run(): void { - $this->register_endpoint( '/content-suggestions/suggest-linked-reference', array( 'POST' ) ); - } - - /** - * Generates the final data from the passed response. - * - * @since 3.14.0 - * - * @param array $response The response received by the proxy. - * @return array The generated data. - */ - protected function generate_data( $response ): array { - // Unused function. - return $response; - } - - /** - * Cached "proxy" to the Parse.ly API endpoint. - * - * @since 3.14.0 - * - * @param WP_REST_Request $request The request object. - * @return stdClass|WP_Error stdClass containing the data or a WP_Error - * object on failure. - */ - public function get_items( WP_REST_Request $request ) { - $validation = $this->validate_apikey_and_secret(); - if ( is_wp_error( $validation ) ) { - return $validation; - } - - $pch_options = $this->parsely->get_options()['content_helper']; - if ( ! Permissions::current_user_can_use_pch_feature( 'smart_linking', $pch_options ) ) { - return new WP_Error( 'ch_access_to_feature_disabled', '', array( 'status' => 403 ) ); - } - - /** - * The post content to be sent to the API. - * - * @var string|null $post_content - */ - $post_content = $request->get_param( 'text' ); - - /** - * The maximum amount of words of the link text. - * - * @var string|null $max_link_words - */ - $max_link_words = $request->get_param( 'max_link_words' ); - - /** - * The maximum number of links to return. - * - * @var string|null $max_links - */ - $max_links = $request->get_param( 'max_links' ); - - /** - * The URL exclusion list. - * - * @var mixed $url_exclusion_list - */ - $url_exclusion_list = $request->get_param( 'url_exclusion_list' ) ?? array(); - - if ( null === $post_content ) { - return new WP_Error( - 'parsely_content_not_set', - __( 'A post content must be set to use this endpoint', 'wp-parsely' ), - array( 'status' => 403 ) - ); - } - - if ( is_numeric( $max_link_words ) ) { - $max_link_words = (int) $max_link_words; - } else { - $max_link_words = 4; - } - - if ( is_numeric( $max_links ) ) { - $max_links = (int) $max_links; - } else { - $max_links = 10; - } - - if ( ! is_array( $url_exclusion_list ) ) { - $url_exclusion_list = array(); - } - - $response = $this->suggest_linked_reference_api->get_links( - $post_content, - $max_link_words, - $max_links, - $url_exclusion_list - ); - - if ( is_wp_error( $response ) ) { - return $response; - } - - // Convert the smart links to an array of objects. - $smart_links = array(); - foreach ( $response as $link ) { - $smart_links[] = $link->to_array(); - } - - return (object) array( - 'data' => $smart_links, - ); - } -} diff --git a/wp-parsely.php b/wp-parsely.php index bff6aad664..ebe9c85c0c 100644 --- a/wp-parsely.php +++ b/wp-parsely.php @@ -32,10 +32,6 @@ use Parsely\Content_Helper\Post_List_Stats; use Parsely\Endpoints\Analytics_Post_Detail_API_Proxy; use Parsely\Endpoints\Analytics_Posts_API_Proxy; -use Parsely\Endpoints\Content_Helper\Smart_Linking_Endpoint; -use Parsely\Endpoints\ContentSuggestions\Suggest_Brief_API_Proxy; -use Parsely\Endpoints\ContentSuggestions\Suggest_Headline_API_Proxy; -use Parsely\Endpoints\ContentSuggestions\Suggest_Linked_Reference_API_Proxy; use Parsely\Endpoints\GraphQL_Metadata; use Parsely\Endpoints\Referrers_Post_Detail_API_Proxy; use Parsely\Endpoints\Related_API_Proxy; @@ -47,9 +43,6 @@ use Parsely\Integrations\Integrations; use Parsely\RemoteAPI\Analytics_Post_Detail_API; use Parsely\RemoteAPI\Analytics_Posts_API; -use Parsely\RemoteAPI\ContentSuggestions\Suggest_Brief_API; -use Parsely\RemoteAPI\ContentSuggestions\Suggest_Headline_API; -use Parsely\RemoteAPI\ContentSuggestions\Suggest_Linked_Reference_API; use Parsely\RemoteAPI\Referrers_Post_Detail_API; use Parsely\RemoteAPI\Related_API; use Parsely\RemoteAPI\Remote_API_Cache; @@ -126,10 +119,9 @@ function parsely_wp_admin_early_register(): void { $network_admin_sites_list = new Network_Admin_Sites_List( $GLOBALS['parsely'] ); $network_admin_sites_list->run(); - // START OF PARSE.LY REST API REFACTOR (TODO: make the rest of the plugin use this). + // Initialize the REST API Controller. $rest_api_controller = new REST_API_Controller( $GLOBALS['parsely'] ); $rest_api_controller->init(); - // END OF PARSE.LY REST API REFACTOR. } add_action( 'rest_api_init', __NAMESPACE__ . '\\parsely_rest_api_init' ); @@ -148,9 +140,6 @@ function parsely_rest_api_init(): void { ( new Dashboard_Widget_Settings_Endpoint( $GLOBALS['parsely'] ) )->run(); ( new Editor_Sidebar_Settings_Endpoint( $GLOBALS['parsely'] ) )->run(); - // Internal Content Helper endpoints. - ( new Smart_Linking_Endpoint( $GLOBALS['parsely'] ) )->run(); - parsely_run_rest_api_endpoint( Related_API::class, Related_API_Proxy::class, @@ -174,24 +163,6 @@ function parsely_rest_api_init(): void { Referrers_Post_Detail_API_Proxy::class, $wp_cache ); - - parsely_run_rest_api_endpoint( - Suggest_Headline_API::class, - Suggest_Headline_API_Proxy::class, - $wp_cache - ); - - parsely_run_rest_api_endpoint( - Suggest_Brief_API::class, - Suggest_Brief_API_Proxy::class, - $wp_cache - ); - - parsely_run_rest_api_endpoint( - Suggest_Linked_Reference_API::class, - Suggest_Linked_Reference_API_Proxy::class, - $wp_cache - ); } add_action( 'init', __NAMESPACE__ . '\\init_recommendations_block' ); From 1b45964b9d60352a722b15b556e097b6b1e51352 Mon Sep 17 00:00:00 2001 From: Henrique Mouta Date: Thu, 22 Aug 2024 14:57:03 +0100 Subject: [PATCH 06/49] Fix sonarcloud issues --- tests/Integration/RestAPI/BaseAPIControllerTest.php | 1 - tests/Integration/RestAPI/BaseEndpointTest.php | 3 +-- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/tests/Integration/RestAPI/BaseAPIControllerTest.php b/tests/Integration/RestAPI/BaseAPIControllerTest.php index cc9835bcef..a584d1c516 100644 --- a/tests/Integration/RestAPI/BaseAPIControllerTest.php +++ b/tests/Integration/RestAPI/BaseAPIControllerTest.php @@ -146,7 +146,6 @@ protected function init(): void {} * @uses \Parsely\REST_API\Base_API_Controller::register_endpoint */ public function test_register_endpoint(): void { - $parsely = self::createMock( Parsely::class ); $endpoint = self::createMock( Base_Endpoint::class ); $endpoint->expects( self::once() )->method( 'init' ); diff --git a/tests/Integration/RestAPI/BaseEndpointTest.php b/tests/Integration/RestAPI/BaseEndpointTest.php index e9b1e6f85a..a8b15e3775 100644 --- a/tests/Integration/RestAPI/BaseEndpointTest.php +++ b/tests/Integration/RestAPI/BaseEndpointTest.php @@ -129,10 +129,9 @@ public function register_routes(): void { * * @since 3.17.0 * - * @param WP_REST_Request $request The request object. * @return array */ - public function get_test_data( WP_REST_Request $request ): array { + public function get_test_data(): array { return array( 'data' => 'test' ); } }; From fe193159586bf8a34ad406e40f2f3f08b7b477c7 Mon Sep 17 00:00:00 2001 From: Henrique Mouta Date: Fri, 23 Aug 2024 11:24:09 +0100 Subject: [PATCH 07/49] Refactor to avoid consts and use getters instead --- src/rest-api/class-base-api-controller.php | 144 +++++++++++------- src/rest-api/class-base-endpoint.php | 98 +++++------- src/rest-api/class-rest-api-controller.php | 24 +-- .../class-content-helper-controller.php | 9 +- .../class-endpoint-excerpt-generator.php | 20 +-- .../class-endpoint-smart-linking.php | 20 +-- .../class-endpoint-title-suggestions.php | 11 ++ .../RestAPI/BaseAPIControllerTest.php | 90 ++++++----- .../Integration/RestAPI/BaseEndpointTest.php | 58 ++----- .../ContentHelperControllerTest.php | 15 +- .../EndpointExcerptGeneratorTest.php | 4 +- .../EndpointSmartLinkingTest.php | 12 +- .../EndpointTitleSuggestionsTest.php | 4 +- .../RestAPI/RestAPIControllerTest.php | 4 +- 14 files changed, 272 insertions(+), 241 deletions(-) diff --git a/src/rest-api/class-base-api-controller.php b/src/rest-api/class-base-api-controller.php index 8e6693ea5f..a9f29f2ef2 100644 --- a/src/rest-api/class-base-api-controller.php +++ b/src/rest-api/class-base-api-controller.php @@ -23,116 +23,150 @@ abstract class Base_API_Controller { /** - * The API namespace. - * - * @var string|false - */ - public const NAMESPACE = false; - - /** - * The API version. - * - * @var string - */ - public const VERSION = ''; - - /** - * The route prefix, which acts as a namespace for the endpoints. + * The endpoints. * * @since 3.17.0 * - * @var string - */ - public const ROUTE_PREFIX = ''; - - /** - * The endpoints. - * * @var Base_Endpoint[] */ - public $endpoints; + private $endpoints; /** * The Parsely instance. * + * @since 3.17.0 + * * @var Parsely */ - public $parsely; + private $parsely; /** * Constructor. * + * @since 3.17.0 + * * @param Parsely $parsely The Parsely instance. - * @throws \UnexpectedValueException If the namespace is not defined. */ public function __construct( Parsely $parsely ) { $this->parsely = $parsely; $this->endpoints = array(); - - if ( static::NAMESPACE === false ) { - throw new \UnexpectedValueException( 'The API controller must define a namespace.' ); - } } /** - * Initialize the API controller. + * Initializes the API controller. * * This method should be overridden by child classes and used to register * endpoints. * + * @since 3.17.0 + * * @return void */ abstract protected function init(): void; /** - * Register a single endpoint. + * Gets the namespace for the API. + * + * This method should be overridden by child classes to define the namespace. * * @since 3.17.0 * - * @param Base_Endpoint $endpoint The endpoint to register. + * @return string The namespace. */ - protected function register_endpoint( Base_Endpoint $endpoint ): void { - $this->endpoints[] = $endpoint; - $endpoint->init(); - } + abstract protected function get_namespace(): string; /** - * Register multiple endpoints. + * Gets the version for the API. + * + * This method can be overridden by child classes to define the version. * * @since 3.17.0 * - * @param Base_Endpoint[] $endpoints The endpoints to register. + * @return string The version. */ - protected function register_endpoints( array $endpoints ): void { - foreach ( $endpoints as $endpoint ) { - $this->register_endpoint( $endpoint ); - } + protected function get_version(): string { + return ''; } /** - * Returns the namespace for the API. + * Gets the route prefix, which acts as a namespace for the endpoints. * - * If a version is defined, it will be appended to the namespace. + * This method can be overridden by child classes to define the route prefix. * * @since 3.17.0 - * @return string + * + * @return string The route prefix. */ - public function get_namespace(): string { - $namespace = static::NAMESPACE; + public function get_route_prefix(): string { + return ''; + } - if ( false === $namespace ) { - return ''; - } + /** + * Returns the full namespace for the API, including the version if defined. + * + * @since 3.17.0 + * + * @return string + */ + public function get_full_namespace(): string { + $namespace = $this->get_namespace(); - if ( '' !== static::VERSION ) { - $namespace .= '/' . static::VERSION; + if ( '' !== $this->get_version() ) { + $namespace .= '/' . $this->get_version(); } return $namespace; } /** - * Prefix a route with the route prefix. + * Gets the Parsely instance. + * + * @since 3.17.0 + * + * @return Parsely The Parsely instance. + */ + public function get_parsely(): Parsely { + return $this->parsely; + } + + /** + * Gets the registered endpoints. + * + * @since 3.17.0 + * + * @return Base_Endpoint[] The registered endpoints. + */ + public function get_endpoints(): array { + return $this->endpoints; + } + + /** + * Registers a single endpoint. + * + * @since 3.17.0 + * + * @param Base_Endpoint $endpoint The endpoint to register. + */ + protected function register_endpoint( Base_Endpoint $endpoint ): void { + $this->endpoints[] = $endpoint; + $endpoint->init(); + } + + /** + * Registers multiple endpoints. + * + * @since 3.17.0 + * + * @param Base_Endpoint[] $endpoints The endpoints to register. + */ + protected function register_endpoints( array $endpoints ): void { + foreach ( $endpoints as $endpoint ) { + $this->register_endpoint( $endpoint ); + } + } + + /** + * Prefixes a route with the route prefix. * * @since 3.17.0 * @@ -140,10 +174,10 @@ public function get_namespace(): string { * @return string The prefixed route. */ public function prefix_route( string $route ): string { - if ( '' === static::ROUTE_PREFIX ) { + if ( '' === $this->get_route_prefix() ) { return $route; } - return static::ROUTE_PREFIX . '/' . $route; + return $this->get_route_prefix() . '/' . $route; } } diff --git a/src/rest-api/class-base-endpoint.php b/src/rest-api/class-base-endpoint.php index e5cacc96fc..6c85681343 100644 --- a/src/rest-api/class-base-endpoint.php +++ b/src/rest-api/class-base-endpoint.php @@ -12,7 +12,6 @@ use Parsely\Parsely; use Parsely\Utils\Utils; -use UnexpectedValueException; use WP_Error; use WP_REST_Request; @@ -25,24 +24,6 @@ * @since 3.17.0 */ abstract class Base_Endpoint { - /** - * The endpoint. - * - * @since 3.17.0 - * - * @var string|false - */ - protected const ENDPOINT = false; - - /** - * The default user capability needed to access endpoints. - * - * @since 3.17.0 - * - * @var string - */ - public const DEFAULT_ACCESS_CAPABILITY = 'publish_posts'; - /** * The Parsely instance. * @@ -59,7 +40,7 @@ abstract class Base_Endpoint { * * @var Base_API_Controller $api_controller */ - public $api_controller; + protected $api_controller; /** * The registered routes. @@ -79,31 +60,25 @@ abstract class Base_Endpoint { */ public function __construct( Base_API_Controller $controller ) { $this->api_controller = $controller; - $this->parsely = $controller->parsely; + $this->parsely = $controller->get_parsely(); } /** - * Initialize the API endpoint, by registering the routes. + * Initializes the API endpoint, by registering the routes. * * Allows for the endpoint to be disabled via the * `wp_parsely_api_{endpoint}_endpoint_enabled` filter. * * @since 3.17.0 - * - * @throws UnexpectedValueException If the ENDPOINT constant is not defined. */ public function init(): void { - if ( false === static::ENDPOINT ) { - throw new UnexpectedValueException( 'ENDPOINT constant must be defined in child class.' ); - } - /** * Filter to enable/disable the endpoint. * * @return bool */ $filter_name = 'wp_parsely_api_' . - Utils::convert_endpoint_to_filter_key( static::ENDPOINT ) . + Utils::convert_endpoint_to_filter_key( $this->get_endpoint_name() ) . '_endpoint_enabled'; if ( ! apply_filters( $filter_name, true ) ) { // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.DynamicHooknameFound return; @@ -113,6 +88,33 @@ public function init(): void { add_action( 'rest_api_init', array( $this, 'register_routes' ) ); } + + /** + * Returns the endpoint name. + * + * This method should be overridden by child classes and used to return the + * endpoint name. + * + * @since 3.17.0 + * + * @return string + */ + abstract public function get_endpoint_name(): string; + + /** + * Returns the default access capability for the endpoint. + * + * This method can be overridden by child classes to return a different + * default access capability. + * + * @since 3.17.0 + * + * @return string + */ + protected function get_default_access_capability(): string { + return 'publish_posts'; + } + /** * Registers the routes for the endpoint. * @@ -135,18 +137,17 @@ abstract public function register_routes(): void; * @since 3.17.0 */ public function register_rest_route( string $route, array $methods, callable $callback, array $args = array() ): void { - // Trim any possible slashes from the route - // . + // Trim any possible slashes from the route. $route = trim( $route, '/' ); // Store the route for later reference. $this->registered_routes[] = $route; // Create the full route for the endpoint. - $route = $this->get_endpoint() . '/' . $route; + $route = $this->get_endpoint_name() . '/' . $route; // Register the route. register_rest_route( - $this->api_controller->get_namespace(), + $this->api_controller->get_full_namespace(), $this->api_controller->prefix_route( $route ), array( array( @@ -160,20 +161,6 @@ public function register_rest_route( string $route, array $methods, callable $ca ); } - /** - * Returns the endpoint name. - * - * @since 3.17.0 - * - * @return string - */ - public function get_endpoint(): string { - if ( false === static::ENDPOINT ) { - return ''; - } - return static::ENDPOINT; - } - /** * Returns the full endpoint path for a given route. @@ -184,10 +171,10 @@ public function get_endpoint(): string { * @return string */ public function get_full_endpoint( string $route = '' ): string { - $route = $this->get_endpoint() . '/' . $route; + $route = $this->get_endpoint_name() . '/' . $route; return '/' . - $this->api_controller->get_namespace() . + $this->api_controller->get_full_namespace() . '/' . $this->api_controller->prefix_route( $route ); } @@ -223,7 +210,7 @@ public function is_available_to_current_user( ?WP_REST_Request $request = null ) } // Validate the user capability. - $capability = static::DEFAULT_ACCESS_CAPABILITY; + $capability = $this->get_default_access_capability(); return current_user_can( // phpcs:ignore WordPress.WP.Capabilities.Undetermined $this->apply_capability_filters( $capability ) @@ -234,7 +221,7 @@ public function is_available_to_current_user( ?WP_REST_Request $request = null ) * Returns the user capability allowing access to the endpoint, after having * applied capability filters. * - * `DEFAULT_ACCESS_CAPABILITY` is not passed here by default, to allow for + * The default access capability is not passed here by default, to allow for * a more explicit declaration in child classes. * * @since 3.14.0 @@ -242,7 +229,6 @@ public function is_available_to_current_user( ?WP_REST_Request $request = null ) * * @param string $capability The original capability allowing access. * @return string The capability allowing access after applying the filters. - * @throws UnexpectedValueException If the ENDPOINT constant is not defined. */ public function apply_capability_filters( string $capability ): string { /** @@ -255,10 +241,6 @@ public function apply_capability_filters( string $capability ): string { $capability ); - if ( false === static::ENDPOINT ) { - throw new UnexpectedValueException( 'ENDPOINT constant must be defined in child class.' ); - } - /** * Filter to change the user capability for the specific endpoint. * @@ -266,8 +248,8 @@ public function apply_capability_filters( string $capability ): string { */ $endpoint_specific_user_capability = apply_filters( 'wp_parsely_user_capability_for_' . - Utils::convert_endpoint_to_filter_key( static::ENDPOINT ) . - '_api', + Utils::convert_endpoint_to_filter_key( $this->get_endpoint_name() ) . + '_api', $default_user_capability ); diff --git a/src/rest-api/class-rest-api-controller.php b/src/rest-api/class-rest-api-controller.php index b47850f421..e864e59fad 100644 --- a/src/rest-api/class-rest-api-controller.php +++ b/src/rest-api/class-rest-api-controller.php @@ -21,31 +21,35 @@ */ class REST_API_Controller extends Base_API_Controller { /** - * The API namespace. + * The controllers for each namespace. * * @since 3.17.0 * - * @var string + * @var Base_API_Controller[] */ - public const NAMESPACE = 'wp-parsely'; + public $controllers = array(); /** - * The API version. + * Gets the namespace for the API. * * @since 3.17.0 * - * @var string + * @return string The namespace. */ - public const VERSION = 'v2'; + protected function get_namespace(): string { + return 'wp-parsely'; + } /** - * The controllers for each namespace. + * Gets the version for the API. * * @since 3.17.0 * - * @var Base_API_Controller[] + * @return string The version. */ - public $controllers = array(); + protected function get_version(): string { + return 'v2'; + } /** * Initializes the REST API controller. @@ -55,7 +59,7 @@ class REST_API_Controller extends Base_API_Controller { public function init(): void { // Register the controllers for each namespace. $controllers = array( - new Content_Helper_Controller( $this->parsely ), + new Content_Helper_Controller( $this->get_parsely() ), ); // Initialize the controllers. diff --git a/src/rest-api/content-helper/class-content-helper-controller.php b/src/rest-api/content-helper/class-content-helper-controller.php index 5e58f9a0eb..0093a5576a 100644 --- a/src/rest-api/content-helper/class-content-helper-controller.php +++ b/src/rest-api/content-helper/class-content-helper-controller.php @@ -22,13 +22,15 @@ class Content_Helper_Controller extends REST_API_Controller { /** - * The route prefix, which acts as a namespace for the endpoints. + * Gets the prefix for this API route. * * @since 3.17.0 * - * @var string + * @return string The namespace. */ - public const ROUTE_PREFIX = 'content-helper'; + public function get_route_prefix(): string { + return 'content-helper'; + } /** * Initializes the Content Helper API endpoints. @@ -36,6 +38,7 @@ class Content_Helper_Controller extends REST_API_Controller { * @since 3.17.0 */ public function init(): void { + $endpoints = array( new Endpoint_Smart_Linking( $this ), new Endpoint_Excerpt_Generator( $this ), diff --git a/src/rest-api/content-helper/class-endpoint-excerpt-generator.php b/src/rest-api/content-helper/class-endpoint-excerpt-generator.php index f2fbaeb2a5..409631a59e 100644 --- a/src/rest-api/content-helper/class-endpoint-excerpt-generator.php +++ b/src/rest-api/content-helper/class-endpoint-excerpt-generator.php @@ -27,15 +27,6 @@ class Endpoint_Excerpt_Generator extends Base_Endpoint { use Content_Helper_Feature; - /** - * The endpoint. - * - * @since 3.17.0 - * - * @var string - */ - protected const ENDPOINT = 'excerpt-generator'; - /** * The Suggest Brief API instance. * @@ -57,6 +48,17 @@ public function __construct( Content_Helper_Controller $controller ) { $this->suggest_brief_api = new Suggest_Brief_API( $this->parsely ); } + /** + * Returns the name of the endpoint. + * + * @since 3.17.0 + * + * @return string The endpoint name. + */ + public function get_endpoint_name(): string { + return 'excerpt-generator'; + } + /** * Returns the name of the feature associated with the current endpoint. * diff --git a/src/rest-api/content-helper/class-endpoint-smart-linking.php b/src/rest-api/content-helper/class-endpoint-smart-linking.php index 902bc8d476..12822b6e71 100644 --- a/src/rest-api/content-helper/class-endpoint-smart-linking.php +++ b/src/rest-api/content-helper/class-endpoint-smart-linking.php @@ -29,15 +29,6 @@ class Endpoint_Smart_Linking extends Base_Endpoint { use Content_Helper_Feature; - /** - * The endpoint. - * - * @since 3.17.0 - * - * @var string - */ - protected const ENDPOINT = 'smart-linking'; - /** * The Suggest Linked Reference API instance. * @@ -59,6 +50,17 @@ public function __construct( Content_Helper_Controller $controller ) { $this->suggest_linked_reference_api = new Suggest_Linked_Reference_API( $this->parsely ); } + /** + * Returns the name of the endpoint. + * + * @since 3.17.0 + * + * @return string The endpoint name. + */ + public function get_endpoint_name(): string { + return 'smart-linking'; + } + /** * Returns the name of the feature associated with the current endpoint. * diff --git a/src/rest-api/content-helper/class-endpoint-title-suggestions.php b/src/rest-api/content-helper/class-endpoint-title-suggestions.php index 1f05f4411e..cf80953b0e 100644 --- a/src/rest-api/content-helper/class-endpoint-title-suggestions.php +++ b/src/rest-api/content-helper/class-endpoint-title-suggestions.php @@ -50,6 +50,17 @@ public function __construct( Content_Helper_Controller $controller ) { $this->suggest_headline_api = new Suggest_Headline_API( $this->parsely ); } + /** + * Returns the name of the endpoint. + * + * @since 3.17.0 + * + * @return string The endpoint name. + */ + public function get_endpoint_name(): string { + return 'title-suggestions'; + } + /** * Returns the name of the feature associated with the current endpoint. * diff --git a/tests/Integration/RestAPI/BaseAPIControllerTest.php b/tests/Integration/RestAPI/BaseAPIControllerTest.php index a584d1c516..3b5b037317 100644 --- a/tests/Integration/RestAPI/BaseAPIControllerTest.php +++ b/tests/Integration/RestAPI/BaseAPIControllerTest.php @@ -43,8 +43,28 @@ public function setUp(): void { TestCase::set_options(); $parsely = self::createMock( Parsely::class ); $this->test_controller = new class($parsely) extends Base_API_Controller { - public const NAMESPACE = 'test'; - public const VERSION = 'v1'; + + /** + * Get the namespace for the API. + * + * @since 3.17.0 + * + * @return string The namespace. + */ + protected function get_namespace(): string { + return 'test'; + } + + /** + * Get the version for the API. + * + * @since 3.17.0 + * + * @return string The version. + */ + protected function get_version(): string { + return 'v1'; + } /** * Initialize the test controller. @@ -73,40 +93,16 @@ public function testable_register_endpoint( Base_Endpoint $endpoint ): void { }; } - /** - * Test that the constructor throws an exception if the NAMESPACE is not defined. - * - * @since 3.17.0 - * - * @covers \Parsely\REST_API\Base_API_Controller::__construct - */ - public function test_constructor_throws_exception_without_namespace(): void { - self::expectException( \UnexpectedValueException::class ); - self::expectExceptionMessage( 'The API controller must define a namespace.' ); - - $parsely = self::createMock( Parsely::class ); - - // Use an anonymous class to avoid implementing abstract methods. - new class($parsely) extends Base_API_Controller { - /** - * Initialize the test controller. - * - * @since 3.17.0 - */ - protected function init(): void {} - }; - } - /** * Test the get_namespace method. * * @since 3.17.0 * - * @covers \Parsely\REST_API\Base_API_Controller::get_namespace + * @covers \Parsely\REST_API\Base_API_Controller::get_full_namespace * @uses \Parsely\REST_API\Base_API_Controller::__construct */ public function test_get_namespace(): void { - self::assertEquals( 'test/v1', $this->test_controller->get_namespace() ); + self::assertEquals( 'test/v1', $this->test_controller->get_full_namespace() ); } /** @@ -122,15 +118,34 @@ public function test_prefix_route(): void { $parsely = self::createMock( Parsely::class ); $controller_with_prefix = new class($parsely) extends Base_API_Controller { - public const NAMESPACE = 'test'; - public const ROUTE_PREFIX = 'prefix'; - /** * Initialize the test controller. * * @since 3.17.0 */ protected function init(): void {} + + /** + * Get the namespace for the API. + * + * @since 3.17.0 + * + * @return string The namespace. + */ + protected function get_namespace(): string { + return 'test'; + } + + /** + * Get the version for the API. + * + * @since 3.17.0 + * + * @return string The version. + */ + public function get_route_prefix(): string { + return 'prefix'; + } }; self::assertEquals( 'prefix/my-route', $controller_with_prefix->prefix_route( 'my-route' ) ); @@ -151,8 +166,9 @@ public function test_register_endpoint(): void { $this->test_controller->testable_register_endpoint( $endpoint ); // @phpstan-ignore-line - self::assertCount( 1, $this->test_controller->endpoints ); - self::assertSame( $endpoint, $this->test_controller->endpoints[0] ); + $endpoints = $this->test_controller->get_endpoints(); + self::assertCount( 1, $endpoints ); + self::assertSame( $endpoint, $endpoints[0] ); } /** @@ -174,8 +190,10 @@ public function test_register_multiple_endpoints(): void { $this->test_controller->testable_register_endpoints( array( $endpoint1, $endpoint2 ) ); // @phpstan-ignore-line - self::assertCount( 2, $this->test_controller->endpoints ); - self::assertSame( $endpoint1, $this->test_controller->endpoints[0] ); - self::assertSame( $endpoint2, $this->test_controller->endpoints[1] ); + $endpoints = $this->test_controller->get_endpoints(); + + self::assertCount( 2, $endpoints ); + self::assertSame( $endpoint1, $endpoints[0] ); + self::assertSame( $endpoint2, $endpoints[1] ); } } diff --git a/tests/Integration/RestAPI/BaseEndpointTest.php b/tests/Integration/RestAPI/BaseEndpointTest.php index a8b15e3775..62b57b51fc 100644 --- a/tests/Integration/RestAPI/BaseEndpointTest.php +++ b/tests/Integration/RestAPI/BaseEndpointTest.php @@ -16,9 +16,7 @@ use Parsely\REST_API\REST_API_Controller; use Parsely\Tests\Integration\TestCase; use ReflectionException; -use UnexpectedValueException; use WP_Error; -use WP_REST_Request; /** * Integration tests for the Base_Endpoint class. @@ -109,7 +107,17 @@ public function set_up(): void { // Create a concrete class for testing purposes. $this->test_endpoint = new class($this->api_controller) extends Base_Endpoint { - protected const ENDPOINT = 'test-endpoint'; + + /** + * Get the endpoint name. + * + * @since 3.17.0 + * + * @return string + */ + public function get_endpoint_name(): string { + return 'test'; + } /** * Register the test route. @@ -163,40 +171,6 @@ public function get_endpoint(): Base_Endpoint { return $this->test_endpoint; } - /** - * Test the init method throws an exception if ENDPOINT is not defined. - * - * @since 3.17.0 - * - * @covers \Parsely\REST_API\Base_Endpoint::init - * @uses \Parsely\REST_API\Base_Endpoint::__construct - * @uses \Parsely\REST_API\Base_API_Controller::__construct - * @uses \Parsely\Parsely::__construct - * @uses \Parsely\Parsely::allow_parsely_remote_requests - * @uses \Parsely\Parsely::are_credentials_managed - * @uses \Parsely\Parsely::set_managed_options - * @uses \Parsely\Endpoints\Base_Endpoint::__construct - * @uses \Parsely\Utils\Utils::convert_endpoint_to_filter_key - */ - public function test_init_throws_exception_without_endpoint(): void { - self::expectException( UnexpectedValueException::class ); - self::expectExceptionMessage( 'ENDPOINT constant must be defined in child class.' ); - - // Create an endpoint with no ENDPOINT constant defined. - $endpoint_without_endpoint = new class($this->api_controller) extends Base_Endpoint { - /** - * Register the routes. - * - * @since 3.17.0 - */ - public function register_routes(): void { - // Mock method for testing. - } - }; - - $endpoint_without_endpoint->init(); - } - /** * Test that the route is correctly registered in WordPress. * @@ -206,7 +180,7 @@ public function register_routes(): void { * @covers \Parsely\REST_API\Base_Endpoint::register_routes * @covers \Parsely\REST_API\Base_Endpoint::get_full_endpoint * @uses \Parsely\REST_API\Base_API_Controller::__construct - * @uses \Parsely\REST_API\Base_API_Controller::get_namespace + * @uses \Parsely\REST_API\Base_API_Controller::get_full_namespace * @uses \Parsely\REST_API\Base_API_Controller::prefix_route * @uses \Parsely\Utils\Utils::convert_endpoint_to_filter_key * @uses \Parsely\Parsely::__construct @@ -226,7 +200,7 @@ public function test_route_is_registered(): void { $routes = rest_get_server()->get_routes(); // Check that the namespace route is registered. - $expected_namespace = '/' . $this->api_controller->get_namespace(); + $expected_namespace = '/' . $this->api_controller->get_full_namespace(); self::assertArrayHasKey( $expected_namespace, $routes ); // Check that the test route is registered. @@ -262,10 +236,10 @@ public function test_route_is_registered(): void { * @uses \Parsely\Permissions::current_user_can_use_pch_feature * @uses \Parsely\Permissions::get_user_roles_with_edit_posts_cap * @uses \Parsely\REST_API\Base_API_Controller::__construct - * @uses \Parsely\REST_API\Base_API_Controller::get_namespace + * @uses \Parsely\REST_API\Base_API_Controller::get_full_namespace * @uses \Parsely\REST_API\Base_API_Controller::prefix_route * @uses \Parsely\REST_API\Base_Endpoint::__construct - * @uses \Parsely\REST_API\Base_Endpoint::get_endpoint + * @uses \Parsely\REST_API\Base_Endpoint::get_endpoint_name * @uses \Parsely\REST_API\Base_Endpoint::is_available_to_current_user * @uses \Parsely\REST_API\Base_Endpoint::register_rest_route * @uses \Parsely\REST_API\Base_Endpoint::validate_apikey_and_secret @@ -273,7 +247,7 @@ public function test_route_is_registered(): void { */ public function test_endpoint_is_registered_based_on_filter(): void { $filter_name = 'wp_parsely_api_' . - \Parsely\Utils\Utils::convert_endpoint_to_filter_key( $this->get_endpoint()->get_endpoint() ) . + \Parsely\Utils\Utils::convert_endpoint_to_filter_key( $this->get_endpoint()->get_endpoint_name() ) . '_endpoint_enabled'; // Test when the filter allows the endpoint to be enabled. diff --git a/tests/Integration/RestAPI/ContentHelper/ContentHelperControllerTest.php b/tests/Integration/RestAPI/ContentHelper/ContentHelperControllerTest.php index 87380d31d4..fcc563d076 100644 --- a/tests/Integration/RestAPI/ContentHelper/ContentHelperControllerTest.php +++ b/tests/Integration/RestAPI/ContentHelper/ContentHelperControllerTest.php @@ -54,10 +54,10 @@ public function setUp(): void { * @since 3.17.0 * * @covers \Parsely\REST_API\Content_Helper\Content_Helper_Controller::__construct - * @uses \Parsely\REST_API\Content_Helper\Content_Helper_Controller::get_namespace + * @uses \Parsely\REST_API\Content_Helper\Content_Helper_Controller::get_full_namespace */ public function test_constructor_sets_up_namespace_and_version(): void { - self::assertEquals( 'wp-parsely/v2', $this->content_helper_controller->get_namespace() ); + self::assertEquals( 'wp-parsely/v2', $this->content_helper_controller->get_full_namespace() ); } /** @@ -68,7 +68,7 @@ public function test_constructor_sets_up_namespace_and_version(): void { * @covers \Parsely\REST_API\Content_Helper\Content_Helper_Controller::ROUTE_PREFIX */ public function test_route_prefix(): void { - self::assertEquals( 'content-helper', Content_Helper_Controller::ROUTE_PREFIX ); + self::assertEquals( 'content-helper', $this->content_helper_controller->get_route_prefix() ); } /** @@ -90,10 +90,11 @@ public function test_route_prefix(): void { */ public function test_init_registers_endpoints(): void { $this->content_helper_controller->init(); + $endpoints = $this->content_helper_controller->get_endpoints(); - self::assertCount( 3, $this->content_helper_controller->endpoints ); - self::assertInstanceOf( Endpoint_Smart_Linking::class, $this->content_helper_controller->endpoints[0] ); - self::assertInstanceOf( Endpoint_Excerpt_Generator::class, $this->content_helper_controller->endpoints[1] ); - self::assertInstanceOf( Endpoint_Title_Suggestions::class, $this->content_helper_controller->endpoints[2] ); + self::assertCount( 3, $endpoints ); + self::assertInstanceOf( Endpoint_Smart_Linking::class, $endpoints[0] ); + self::assertInstanceOf( Endpoint_Excerpt_Generator::class, $endpoints[1] ); + self::assertInstanceOf( Endpoint_Title_Suggestions::class, $endpoints[2] ); } } diff --git a/tests/Integration/RestAPI/ContentHelper/EndpointExcerptGeneratorTest.php b/tests/Integration/RestAPI/ContentHelper/EndpointExcerptGeneratorTest.php index 2fe8e904c7..a489f194af 100644 --- a/tests/Integration/RestAPI/ContentHelper/EndpointExcerptGeneratorTest.php +++ b/tests/Integration/RestAPI/ContentHelper/EndpointExcerptGeneratorTest.php @@ -82,10 +82,10 @@ public function get_endpoint(): \Parsely\REST_API\Base_Endpoint { * @uses \Parsely\Permissions::current_user_can_use_pch_feature * @uses \Parsely\Permissions::get_user_roles_with_edit_posts_cap * @uses \Parsely\REST_API\Base_API_Controller::__construct - * @uses \Parsely\REST_API\Base_API_Controller::get_namespace + * @uses \Parsely\REST_API\Base_API_Controller::get_full_namespace * @uses \Parsely\REST_API\Base_API_Controller::prefix_route * @uses \Parsely\REST_API\Base_Endpoint::__construct - * @uses \Parsely\REST_API\Base_Endpoint::get_endpoint + * @uses \Parsely\REST_API\Base_Endpoint::get_endpoint_name * @uses \Parsely\REST_API\Base_Endpoint::get_full_endpoint * @uses \Parsely\REST_API\Base_Endpoint::init * @uses \Parsely\REST_API\Base_Endpoint::is_available_to_current_user diff --git a/tests/Integration/RestAPI/ContentHelper/EndpointSmartLinkingTest.php b/tests/Integration/RestAPI/ContentHelper/EndpointSmartLinkingTest.php index 476f3a8c7d..15a228d3b9 100644 --- a/tests/Integration/RestAPI/ContentHelper/EndpointSmartLinkingTest.php +++ b/tests/Integration/RestAPI/ContentHelper/EndpointSmartLinkingTest.php @@ -93,10 +93,10 @@ public function get_endpoint(): \Parsely\REST_API\Base_Endpoint { * @uses \Parsely\Permissions::current_user_can_use_pch_feature * @uses \Parsely\Permissions::get_user_roles_with_edit_posts_cap * @uses \Parsely\REST_API\Base_API_Controller::__construct - * @uses \Parsely\REST_API\Base_API_Controller::get_namespace + * @uses \Parsely\REST_API\Base_API_Controller::get_full_namespace * @uses \Parsely\REST_API\Base_API_Controller::prefix_route * @uses \Parsely\REST_API\Base_Endpoint::__construct - * @uses \Parsely\REST_API\Base_Endpoint::get_endpoint + * @uses \Parsely\REST_API\Base_Endpoint::get_endpoint_name * @uses \Parsely\REST_API\Base_Endpoint::get_full_endpoint * @uses \Parsely\REST_API\Base_Endpoint::init * @uses \Parsely\REST_API\Base_Endpoint::is_available_to_current_user @@ -237,11 +237,11 @@ public function test_generate_smart_links_returns_error_on_failure(): void { * @uses \Parsely\Permissions::current_user_can_use_pch_feature * @uses \Parsely\Permissions::get_user_roles_with_edit_posts_cap * @uses \Parsely\REST_API\Base_API_Controller::__construct - * @uses \Parsely\REST_API\Base_API_Controller::get_namespace + * @uses \Parsely\REST_API\Base_API_Controller::get_full_namespace * @uses \Parsely\REST_API\Base_API_Controller::prefix_route * @uses \Parsely\REST_API\Base_Endpoint::__construct * @uses \Parsely\REST_API\Base_Endpoint::apply_capability_filters - * @uses \Parsely\REST_API\Base_Endpoint::get_endpoint + * @uses \Parsely\REST_API\Base_Endpoint::get_endpoint_name * @uses \Parsely\REST_API\Base_Endpoint::get_full_endpoint * @uses \Parsely\REST_API\Base_Endpoint::init * @uses \Parsely\REST_API\Base_Endpoint::is_available_to_current_user @@ -334,11 +334,11 @@ public function test_add_smart_link_returns_valid_response(): void { * @uses \Parsely\Permissions::current_user_can_use_pch_feature * @uses \Parsely\Permissions::get_user_roles_with_edit_posts_cap * @uses \Parsely\REST_API\Base_API_Controller::__construct - * @uses \Parsely\REST_API\Base_API_Controller::get_namespace + * @uses \Parsely\REST_API\Base_API_Controller::get_full_namespace * @uses \Parsely\REST_API\Base_API_Controller::prefix_route * @uses \Parsely\REST_API\Base_Endpoint::__construct * @uses \Parsely\REST_API\Base_Endpoint::apply_capability_filters - * @uses \Parsely\REST_API\Base_Endpoint::get_endpoint + * @uses \Parsely\REST_API\Base_Endpoint::get_endpoint_name * @uses \Parsely\REST_API\Base_Endpoint::get_full_endpoint * @uses \Parsely\REST_API\Base_Endpoint::init * @uses \Parsely\REST_API\Base_Endpoint::is_available_to_current_user diff --git a/tests/Integration/RestAPI/ContentHelper/EndpointTitleSuggestionsTest.php b/tests/Integration/RestAPI/ContentHelper/EndpointTitleSuggestionsTest.php index 9e94182cf1..ec93d24046 100644 --- a/tests/Integration/RestAPI/ContentHelper/EndpointTitleSuggestionsTest.php +++ b/tests/Integration/RestAPI/ContentHelper/EndpointTitleSuggestionsTest.php @@ -81,10 +81,10 @@ public function get_endpoint(): \Parsely\REST_API\Base_Endpoint { * @uses \Parsely\Permissions::current_user_can_use_pch_feature * @uses \Parsely\Permissions::get_user_roles_with_edit_posts_cap * @uses \Parsely\REST_API\Base_API_Controller::__construct - * @uses \Parsely\REST_API\Base_API_Controller::get_namespace + * @uses \Parsely\REST_API\Base_API_Controller::get_full_namespace * @uses \Parsely\REST_API\Base_API_Controller::prefix_route * @uses \Parsely\REST_API\Base_Endpoint::__construct - * @uses \Parsely\REST_API\Base_Endpoint::get_endpoint + * @uses \Parsely\REST_API\Base_Endpoint::get_endpoint_name * @uses \Parsely\REST_API\Base_Endpoint::get_full_endpoint * @uses \Parsely\REST_API\Base_Endpoint::init * @uses \Parsely\REST_API\Base_Endpoint::is_available_to_current_user diff --git a/tests/Integration/RestAPI/RestAPIControllerTest.php b/tests/Integration/RestAPI/RestAPIControllerTest.php index 0a8968691d..0261c5ec40 100644 --- a/tests/Integration/RestAPI/RestAPIControllerTest.php +++ b/tests/Integration/RestAPI/RestAPIControllerTest.php @@ -50,9 +50,9 @@ public function setUp(): void { * @since 3.17.0 * * @covers \Parsely\REST_API\REST_API_Controller::__construct - * @uses \Parsely\REST_API\REST_API_Controller::get_namespace + * @uses \Parsely\REST_API\REST_API_Controller::get_full_namespace */ public function test_constructor_sets_up_namespace_and_version(): void { - self::assertEquals( 'wp-parsely/v2', $this->test_controller->get_namespace() ); + self::assertEquals( 'wp-parsely/v2', $this->test_controller->get_full_namespace() ); } } From 90c9808ac91fa4a1735315889f2851d60cb845c2 Mon Sep 17 00:00:00 2001 From: Henrique Mouta Date: Fri, 23 Aug 2024 11:26:45 +0100 Subject: [PATCH 08/49] Fix minor docblock issues per code review --- .../content-helper/class-content-helper-controller.php | 1 - src/rest-api/content-helper/trait-content-helper-feature.php | 3 +-- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/src/rest-api/content-helper/class-content-helper-controller.php b/src/rest-api/content-helper/class-content-helper-controller.php index 0093a5576a..b93f65fef9 100644 --- a/src/rest-api/content-helper/class-content-helper-controller.php +++ b/src/rest-api/content-helper/class-content-helper-controller.php @@ -20,7 +20,6 @@ * @since 3.17.0 */ class Content_Helper_Controller extends REST_API_Controller { - /** * Gets the prefix for this API route. * diff --git a/src/rest-api/content-helper/trait-content-helper-feature.php b/src/rest-api/content-helper/trait-content-helper-feature.php index b303383656..02e45c7606 100644 --- a/src/rest-api/content-helper/trait-content-helper-feature.php +++ b/src/rest-api/content-helper/trait-content-helper-feature.php @@ -3,8 +3,8 @@ * API Content Helper Feature Trait * To be used with content helper endpoints that require a feature to be enabled. * - * @since 3.17.0 * @package Parsely + * @since 3.17.0 */ declare(strict_types=1); @@ -12,7 +12,6 @@ namespace Parsely\REST_API\Content_Helper; use Parsely\Permissions; -use Parsely\REST_API\Base_Endpoint; use WP_Error; use WP_REST_Request; From 77dbd2b13eb446e38877dec8aefb93d1bdb087ea Mon Sep 17 00:00:00 2001 From: Henrique Mouta Date: Mon, 26 Aug 2024 11:35:26 +0100 Subject: [PATCH 09/49] Apply code review suggestions by @acicovic --- src/rest-api/class-base-api-controller.php | 1 - src/rest-api/class-base-endpoint.php | 16 +++++++--------- .../class-endpoint-excerpt-generator.php | 3 --- .../class-endpoint-smart-linking.php | 4 ++-- .../class-endpoint-title-suggestions.php | 5 ----- .../trait-content-helper-feature.php | 8 ++++---- tests/Integration/RestAPI/BaseEndpointTest.php | 12 ++++++------ .../EndpointExcerptGeneratorTest.php | 2 +- .../ContentHelper/EndpointSmartLinkingTest.php | 6 +++--- .../EndpointTitleSuggestionsTest.php | 2 +- 10 files changed, 24 insertions(+), 35 deletions(-) diff --git a/src/rest-api/class-base-api-controller.php b/src/rest-api/class-base-api-controller.php index a9f29f2ef2..7f06c98d4e 100644 --- a/src/rest-api/class-base-api-controller.php +++ b/src/rest-api/class-base-api-controller.php @@ -21,7 +21,6 @@ * @since 3.17.0 */ abstract class Base_API_Controller { - /** * The endpoints. * diff --git a/src/rest-api/class-base-endpoint.php b/src/rest-api/class-base-endpoint.php index 6c85681343..3229b4bb80 100644 --- a/src/rest-api/class-base-endpoint.php +++ b/src/rest-api/class-base-endpoint.php @@ -18,8 +18,8 @@ /** * Base class for API endpoints. * - * Most endpoint classes should derive from this class. Child classes must add a - * protected `ENDPOINT` constant. + * Most endpoint classes should derive from this class. Child classes should + * implement the `get_endpoint_name` and `register_routes` methods. * * @since 3.17.0 */ @@ -88,7 +88,6 @@ public function init(): void { add_action( 'rest_api_init', array( $this, 'register_routes' ) ); } - /** * Returns the endpoint name. * @@ -128,13 +127,13 @@ abstract public function register_routes(): void; /** * Registers a REST route. * + * @since 3.17.0 + * * @param string $route The route to register. * @param string[] $methods Array with the allowed methods. * @param callable $callback Callback function to call when the endpoint is hit. * @param array $args The endpoint arguments definition. - * * @return void - * @since 3.17.0 */ public function register_rest_route( string $route, array $methods, callable $callback, array $args = array() ): void { // Trim any possible slashes from the route. @@ -161,7 +160,6 @@ public function register_rest_route( string $route, array $methods, callable $ca ); } - /** * Returns the full endpoint path for a given route. * @@ -202,9 +200,8 @@ public function get_registered_routes(): array { * @return WP_Error|bool True if the endpoint is available. */ public function is_available_to_current_user( ?WP_REST_Request $request = null ) { // phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter.Found - // Validate the API key and secret. - $api_key_validation = $this->validate_apikey_and_secret(); + $api_key_validation = $this->validate_site_id_and_secret(); if ( is_wp_error( $api_key_validation ) ) { return $api_key_validation; } @@ -261,11 +258,12 @@ public function apply_capability_filters( string $capability ): string { * If the API secret is not required, it will not be validated. * * @since 3.13.0 + * @since 3.17.0 Moved to the new API structure and renamed from `validate_apikey_and_secret`. * * @param bool $require_api_secret Specifies if the API Secret is required. * @return WP_Error|bool */ - public function validate_apikey_and_secret( bool $require_api_secret = true ) { + public function validate_site_id_and_secret( bool $require_api_secret = true ) { if ( false === $this->parsely->site_id_is_set() ) { return new WP_Error( 'parsely_site_id_not_set', diff --git a/src/rest-api/content-helper/class-endpoint-excerpt-generator.php b/src/rest-api/content-helper/class-endpoint-excerpt-generator.php index 409631a59e..d2ee594240 100644 --- a/src/rest-api/content-helper/class-endpoint-excerpt-generator.php +++ b/src/rest-api/content-helper/class-endpoint-excerpt-generator.php @@ -73,9 +73,6 @@ public function get_pch_feature_name(): string { /** * Registers the routes for the endpoint. * - * This method should be overridden by child classes and used to register - * the routes for the endpoint. - * * @since 3.17.0 */ public function register_routes(): void { diff --git a/src/rest-api/content-helper/class-endpoint-smart-linking.php b/src/rest-api/content-helper/class-endpoint-smart-linking.php index 12822b6e71..ab1a2f14b0 100644 --- a/src/rest-api/content-helper/class-endpoint-smart-linking.php +++ b/src/rest-api/content-helper/class-endpoint-smart-linking.php @@ -101,11 +101,11 @@ public function register_routes(): void { 'type' => 'array', 'description' => __( 'The list of URLs to exclude from the smart links.', 'wp-parsely' ), 'validate_callback' => array( $this, 'validate_url_exclusion_list' ), + 'default' => array(), ), ) ); - /** * GET /smart-linking/{post_id}/get * Gets the smart links for a post. @@ -584,7 +584,7 @@ public function validate_post_id( string $param, WP_REST_Request $request ): boo * * The callback sets the URL exclusion list in the request object if the parameter is valid. * - * @since 3.16.0 + * @since 3.17.0 * @access private * * @param mixed $param The parameter value. diff --git a/src/rest-api/content-helper/class-endpoint-title-suggestions.php b/src/rest-api/content-helper/class-endpoint-title-suggestions.php index cf80953b0e..8b81fc9587 100644 --- a/src/rest-api/content-helper/class-endpoint-title-suggestions.php +++ b/src/rest-api/content-helper/class-endpoint-title-suggestions.php @@ -27,8 +27,6 @@ class Endpoint_Title_Suggestions extends Base_Endpoint { use Content_Helper_Feature; - protected const ENDPOINT = 'title-suggestions'; - /** * The Suggest Headline API. * @@ -75,9 +73,6 @@ public function get_pch_feature_name(): string { /** * Registers the routes for the endpoint. * - * This method should be overridden by child classes and used to register - * the routes for the endpoint. - * * @since 3.17.0 */ public function register_routes(): void { diff --git a/src/rest-api/content-helper/trait-content-helper-feature.php b/src/rest-api/content-helper/trait-content-helper-feature.php index 02e45c7606..383e7c2ce4 100644 --- a/src/rest-api/content-helper/trait-content-helper-feature.php +++ b/src/rest-api/content-helper/trait-content-helper-feature.php @@ -1,7 +1,8 @@ is_pch_feature_enabled_for_user(); diff --git a/tests/Integration/RestAPI/BaseEndpointTest.php b/tests/Integration/RestAPI/BaseEndpointTest.php index 62b57b51fc..4b0b015053 100644 --- a/tests/Integration/RestAPI/BaseEndpointTest.php +++ b/tests/Integration/RestAPI/BaseEndpointTest.php @@ -242,7 +242,7 @@ public function test_route_is_registered(): void { * @uses \Parsely\REST_API\Base_Endpoint::get_endpoint_name * @uses \Parsely\REST_API\Base_Endpoint::is_available_to_current_user * @uses \Parsely\REST_API\Base_Endpoint::register_rest_route - * @uses \Parsely\REST_API\Base_Endpoint::validate_apikey_and_secret + * @uses \Parsely\REST_API\Base_Endpoint::validate_site_id_and_secret * @uses \Parsely\Utils\Utils::convert_endpoint_to_filter_key */ public function test_endpoint_is_registered_based_on_filter(): void { @@ -283,7 +283,7 @@ public function test_endpoint_is_registered_based_on_filter(): void { * @since 3.17.0 * * @covers \Parsely\REST_API\Base_Endpoint::is_available_to_current_user - * @covers \Parsely\REST_API\Base_Endpoint::validate_apikey_and_secret + * @covers \Parsely\REST_API\Base_Endpoint::validate_site_id_and_secret * @uses \Parsely\REST_API\Base_API_Controller::__construct * @uses \Parsely\Parsely::__construct * @uses \Parsely\Parsely::allow_parsely_remote_requests @@ -322,7 +322,7 @@ public function test_is_available_to_current_user_returns_error_site_id_not_set( * @since 3.17.0 * * @covers \Parsely\REST_API\Base_Endpoint::is_available_to_current_user - * @covers \Parsely\REST_API\Base_Endpoint::validate_apikey_and_secret + * @covers \Parsely\REST_API\Base_Endpoint::validate_site_id_and_secret * @uses \Parsely\REST_API\Base_API_Controller::__construct * @uses \Parsely\Parsely::__construct * @uses \Parsely\Parsely::allow_parsely_remote_requests @@ -385,7 +385,7 @@ function () { /** * Test validate_apikey_and_secret returns true when API key and secret are set. * - * @covers \Parsely\REST_API\Base_Endpoint::validate_apikey_and_secret + * @covers \Parsely\REST_API\Base_Endpoint::validate_site_id_and_secret * @uses \Parsely\REST_API\Base_API_Controller::__construct * @uses \Parsely\Parsely::__construct * @uses \Parsely\Parsely::allow_parsely_remote_requests @@ -404,7 +404,7 @@ function () { * @uses \Parsely\REST_API\Base_Endpoint::init * @uses \Parsely\Utils\Utils::convert_endpoint_to_filter_key */ - public function test_validate_api_key_and_secret_returns_true(): void { + public function test_validate_site_id_and_secret_returns_true(): void { TestCase::set_options( array( 'apikey' => 'test-apikey', @@ -412,7 +412,7 @@ public function test_validate_api_key_and_secret_returns_true(): void { ) ); - $result = $this->get_endpoint()->validate_apikey_and_secret(); + $result = $this->get_endpoint()->validate_site_id_and_secret(); self::assertTrue( $result ); } diff --git a/tests/Integration/RestAPI/ContentHelper/EndpointExcerptGeneratorTest.php b/tests/Integration/RestAPI/ContentHelper/EndpointExcerptGeneratorTest.php index a489f194af..7d38eedc1b 100644 --- a/tests/Integration/RestAPI/ContentHelper/EndpointExcerptGeneratorTest.php +++ b/tests/Integration/RestAPI/ContentHelper/EndpointExcerptGeneratorTest.php @@ -90,7 +90,7 @@ public function get_endpoint(): \Parsely\REST_API\Base_Endpoint { * @uses \Parsely\REST_API\Base_Endpoint::init * @uses \Parsely\REST_API\Base_Endpoint::is_available_to_current_user * @uses \Parsely\REST_API\Base_Endpoint::register_rest_route - * @uses \Parsely\REST_API\Base_Endpoint::validate_apikey_and_secret + * @uses \Parsely\REST_API\Base_Endpoint::validate_site_id_and_secret * @uses \Parsely\Utils\Utils::convert_endpoint_to_filter_key */ public function test_route_is_registered(): void { diff --git a/tests/Integration/RestAPI/ContentHelper/EndpointSmartLinkingTest.php b/tests/Integration/RestAPI/ContentHelper/EndpointSmartLinkingTest.php index 15a228d3b9..3c52d72ed8 100644 --- a/tests/Integration/RestAPI/ContentHelper/EndpointSmartLinkingTest.php +++ b/tests/Integration/RestAPI/ContentHelper/EndpointSmartLinkingTest.php @@ -101,7 +101,7 @@ public function get_endpoint(): \Parsely\REST_API\Base_Endpoint { * @uses \Parsely\REST_API\Base_Endpoint::init * @uses \Parsely\REST_API\Base_Endpoint::is_available_to_current_user * @uses \Parsely\REST_API\Base_Endpoint::register_rest_route - * @uses \Parsely\REST_API\Base_Endpoint::validate_apikey_and_secret + * @uses \Parsely\REST_API\Base_Endpoint::validate_site_id_and_secret * @uses \Parsely\Utils\Utils::convert_endpoint_to_filter_key */ public function test_route_is_registered(): void { @@ -246,7 +246,7 @@ public function test_generate_smart_links_returns_error_on_failure(): void { * @uses \Parsely\REST_API\Base_Endpoint::init * @uses \Parsely\REST_API\Base_Endpoint::is_available_to_current_user * @uses \Parsely\REST_API\Base_Endpoint::register_rest_route - * @uses \Parsely\REST_API\Base_Endpoint::validate_apikey_and_secret + * @uses \Parsely\REST_API\Base_Endpoint::validate_site_id_and_secret * @uses \Parsely\Utils\Utils::convert_endpoint_to_filter_key */ public function test_add_smart_link_returns_valid_response(): void { @@ -343,7 +343,7 @@ public function test_add_smart_link_returns_valid_response(): void { * @uses \Parsely\REST_API\Base_Endpoint::init * @uses \Parsely\REST_API\Base_Endpoint::is_available_to_current_user * @uses \Parsely\REST_API\Base_Endpoint::register_rest_route - * @uses \Parsely\REST_API\Base_Endpoint::validate_apikey_and_secret + * @uses \Parsely\REST_API\Base_Endpoint::validate_site_id_and_secret * @uses \Parsely\Utils\Utils::convert_endpoint_to_filter_key */ public function test_add_multiple_smart_links_returns_valid_response(): void { diff --git a/tests/Integration/RestAPI/ContentHelper/EndpointTitleSuggestionsTest.php b/tests/Integration/RestAPI/ContentHelper/EndpointTitleSuggestionsTest.php index ec93d24046..a9c96e8fae 100644 --- a/tests/Integration/RestAPI/ContentHelper/EndpointTitleSuggestionsTest.php +++ b/tests/Integration/RestAPI/ContentHelper/EndpointTitleSuggestionsTest.php @@ -89,7 +89,7 @@ public function get_endpoint(): \Parsely\REST_API\Base_Endpoint { * @uses \Parsely\REST_API\Base_Endpoint::init * @uses \Parsely\REST_API\Base_Endpoint::is_available_to_current_user * @uses \Parsely\REST_API\Base_Endpoint::register_rest_route - * @uses \Parsely\REST_API\Base_Endpoint::validate_apikey_and_secret + * @uses \Parsely\REST_API\Base_Endpoint::validate_site_id_and_secret * @uses \Parsely\Utils\Utils::convert_endpoint_to_filter_key */ public function test_route_is_registered(): void { From 78395a701d491d69b6f0d58a9034b765bed50dcd Mon Sep 17 00:00:00 2001 From: Henrique Mouta Date: Mon, 26 Aug 2024 11:42:39 +0100 Subject: [PATCH 10/49] Rename API parameter `content` to `text` --- build/content-helper/editor-sidebar.asset.php | 2 +- build/content-helper/editor-sidebar.js | 4 ++-- build/content-helper/excerpt-generator.asset.php | 2 +- build/content-helper/excerpt-generator.js | 2 +- src/content-helper/editor-sidebar/smart-linking/provider.ts | 2 +- .../editor-sidebar/title-suggestions/provider.ts | 2 +- src/content-helper/excerpt-generator/provider.ts | 2 +- .../content-helper/class-endpoint-excerpt-generator.php | 4 ++-- src/rest-api/content-helper/class-endpoint-smart-linking.php | 4 ++-- .../content-helper/class-endpoint-title-suggestions.php | 4 ++-- .../RestAPI/ContentHelper/EndpointExcerptGeneratorTest.php | 4 ++-- .../RestAPI/ContentHelper/EndpointSmartLinkingTest.php | 4 ++-- .../RestAPI/ContentHelper/EndpointTitleSuggestionsTest.php | 4 ++-- 13 files changed, 20 insertions(+), 20 deletions(-) diff --git a/build/content-helper/editor-sidebar.asset.php b/build/content-helper/editor-sidebar.asset.php index 9aedc45478..0a96a20139 100644 --- a/build/content-helper/editor-sidebar.asset.php +++ b/build/content-helper/editor-sidebar.asset.php @@ -1 +1 @@ - array('react', 'wp-api-fetch', 'wp-block-editor', 'wp-blocks', 'wp-components', 'wp-compose', 'wp-core-data', 'wp-data', 'wp-dom-ready', 'wp-editor', 'wp-element', 'wp-hooks', 'wp-i18n', 'wp-plugins', 'wp-primitives', 'wp-url'), 'version' => '6a5829c6e4ff7bf83951'); + array('react', 'wp-api-fetch', 'wp-block-editor', 'wp-blocks', 'wp-components', 'wp-compose', 'wp-core-data', 'wp-data', 'wp-dom-ready', 'wp-editor', 'wp-element', 'wp-hooks', 'wp-i18n', 'wp-plugins', 'wp-primitives', 'wp-url'), 'version' => 'ceb704108c34274732ed'); diff --git a/build/content-helper/editor-sidebar.js b/build/content-helper/editor-sidebar.js index c44b4e6933..3036042520 100644 --- a/build/content-helper/editor-sidebar.js +++ b/build/content-helper/editor-sidebar.js @@ -1,5 +1,5 @@ !function(){"use strict";var e={20:function(e,t,n){var r=n(609),i=Symbol.for("react.element"),s=Symbol.for("react.fragment"),o=Object.prototype.hasOwnProperty,a=r.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED.ReactCurrentOwner,l={key:!0,ref:!0,__self:!0,__source:!0};function c(e,t,n){var r,s={},c=null,u=null;for(r in void 0!==n&&(c=""+n),void 0!==t.key&&(c=""+t.key),void 0!==t.ref&&(u=t.ref),t)o.call(t,r)&&!l.hasOwnProperty(r)&&(s[r]=t[r]);if(e&&e.defaultProps)for(r in t=e.defaultProps)void 0===s[r]&&(s[r]=t[r]);return{$$typeof:i,type:e,key:c,ref:u,props:s,_owner:a.current}}t.Fragment=s,t.jsx=c,t.jsxs=c},848:function(e,t,n){e.exports=n(20)},609:function(e){e.exports=window.React}},t={};function n(r){var i=t[r];if(void 0!==i)return i.exports;var s=t[r]={exports:{}};return e[r](s,s.exports,n),s.exports}n.n=function(e){var t=e&&e.__esModule?function(){return e.default}:function(){return e};return n.d(t,{a:t}),t},n.d=function(e,t){for(var r in t)n.o(t,r)&&!n.o(e,r)&&Object.defineProperty(e,r,{enumerable:!0,get:t[r]})},n.o=function(e,t){return Object.prototype.hasOwnProperty.call(e,t)},function(){n.d({},{_:function(){return ur}});var e,t,r,i,s,o,a,l,c,u,d,p=n(848),f=window.wp.components,h=window.wp.data,v=window.wp.domReady,g=n.n(v);void 0!==window.wp&&(null!==(t=null===(e=window.wp.editor)||void 0===e?void 0:e.PluginDocumentSettingPanel)&&void 0!==t||(null!==(i=null===(r=window.wp.editPost)||void 0===r?void 0:r.PluginDocumentSettingPanel)&&void 0!==i||(null===(s=window.wp.editSite)||void 0===s||s.PluginDocumentSettingPanel)),d=null!==(a=null===(o=window.wp.editor)||void 0===o?void 0:o.PluginSidebar)&&void 0!==a?a:null!==(c=null===(l=window.wp.editPost)||void 0===l?void 0:l.PluginSidebar)&&void 0!==c?c:null===(u=window.wp.editSite)||void 0===u?void 0:u.PluginSidebar);var y,m,w,b=window.wp.element,_=window.wp.i18n,x=window.wp.primitives,k=(0,p.jsx)(x.SVG,{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 24 24",children:(0,p.jsx)(x.Path,{fillRule:"evenodd",d:"M11.25 5h1.5v15h-1.5V5zM6 10h1.5v10H6V10zm12 4h-1.5v6H18v-6z",clipRule:"evenodd"})}),S=window.wp.plugins,P=function(){function e(){this._tkq=[],this.isLoaded=!1,this.isEnabled=!1,"undefined"!=typeof wpParselyTracksTelemetry&&(this.isEnabled=!0,this.loadTrackingLibrary())}return e.getInstance=function(){return window.wpParselyTelemetryInstance||Object.defineProperty(window,"wpParselyTelemetryInstance",{value:new e,writable:!1,configurable:!1,enumerable:!1}),window.wpParselyTelemetryInstance},e.prototype.loadTrackingLibrary=function(){var e=this,t=document.createElement("script");t.async=!0,t.src="//stats.wp.com/w.js",t.onload=function(){e.isLoaded=!0,e._tkq=window._tkq||[]},document.head.appendChild(t)},e.trackEvent=function(t){return n=this,r=arguments,s=function(t,n){var r;return void 0===n&&(n={}),function(e,t){var n,r,i,s,o={label:0,sent:function(){if(1&i[0])throw i[1];return i[1]},trys:[],ops:[]};return s={next:a(0),throw:a(1),return:a(2)},"function"==typeof Symbol&&(s[Symbol.iterator]=function(){return this}),s;function a(a){return function(l){return function(a){if(n)throw new TypeError("Generator is already executing.");for(;s&&(s=0,a[0]&&(o=0)),o;)try{if(n=1,r&&(i=2&a[0]?r.return:a[0]?r.throw||((i=r.return)&&i.call(r),0):r.next)&&!(i=i.call(r,a[1])).done)return i;switch(r=0,i&&(a=[2&a[0],i.value]),a[0]){case 0:case 1:i=a;break;case 4:return o.label++,{value:a[1],done:!1};case 5:o.label++,r=a[1],a=[0];continue;case 7:a=o.ops.pop(),o.trys.pop();continue;default:if(!((i=(i=o.trys).length>0&&i[i.length-1])||6!==a[0]&&2!==a[0])){o=0;continue}if(3===a[0]&&(!i||a[1]>i[0]&&a[1]=1e4&&(clearInterval(s),n("Telemetry library not loaded"))}),100);else n("Telemetry not enabled")}))},e.prototype.trackEvent=function(t,n){var r;this.isLoaded?(0!==t.indexOf(e.TRACKS_PREFIX)&&(t=e.TRACKS_PREFIX+t),this.isEventNameValid(t)?(n=this.prepareProperties(n),null===(r=this._tkq)||void 0===r||r.push(["recordEvent",t,n])):console.error("Error tracking event: Invalid event name")):console.error("Error tracking event: Telemetry not loaded")},e.prototype.isTelemetryEnabled=function(){return this.isEnabled},e.prototype.isProprietyValid=function(t){return e.PROPERTY_REGEX.test(t)},e.prototype.isEventNameValid=function(t){return e.EVENT_NAME_REGEX.test(t)},e.prototype.prepareProperties=function(e){return(e=this.sanitizeProperties(e)).parsely_version=wpParselyTracksTelemetry.version,wpParselyTracksTelemetry.user&&(e._ut=wpParselyTracksTelemetry.user.type,e._ui=wpParselyTracksTelemetry.user.id),wpParselyTracksTelemetry.vipgo_env&&(e.vipgo_env=wpParselyTracksTelemetry.vipgo_env),this.sanitizeProperties(e)},e.prototype.sanitizeProperties=function(e){var t=this,n={};return Object.keys(e).forEach((function(r){t.isProprietyValid(r)&&(n[r]=e[r])})),n},e.TRACKS_PREFIX="wpparsely_",e.EVENT_NAME_REGEX=/^(([a-z0-9]+)_){2}([a-z0-9_]+)$/,e.PROPERTY_REGEX=/^[a-z_][a-z0-9_]*$/,e}(),j=(P.trackEvent,function(){return(0,p.jsx)(f.SVG,{"aria-hidden":"true",version:"1.1",viewBox:"0 0 15 15",width:"15",height:"15",xmlns:"http://www.w3.org/2000/svg",children:(0,p.jsx)(f.Path,{d:"M0 14.0025V11.0025L7.5 3.5025L10.5 6.5025L3 14.0025H0ZM12 5.0025L13.56 3.4425C14.15 2.8525 14.15 1.9025 13.56 1.3225L12.68 0.4425C12.09 -0.1475 11.14 -0.1475 10.56 0.4425L9 2.0025L12 5.0025Z"})})}),T=function(e){var t=e.size,n=void 0===t?24:t,r=e.className,i=void 0===r?"wp-parsely-icon":r;return(0,p.jsxs)(f.SVG,{className:i,height:n,viewBox:"0 0 60 65",width:n,xmlns:"http://www.w3.org/2000/svg",children:[(0,p.jsx)(f.Path,{fill:"#5ba745",d:"M23.72,51.53c0-.18,0-.34-.06-.52a13.11,13.11,0,0,0-2.1-5.53A14.74,14.74,0,0,0,19.12,43c-.27-.21-.5-.11-.51.22l-.24,3.42c0,.33-.38.35-.49,0l-1.5-4.8a1.4,1.4,0,0,0-.77-.78,23.91,23.91,0,0,0-3.1-.84c-1.38-.24-3.39-.39-3.39-.39-.34,0-.45.21-.25.49l2.06,3.76c.2.27,0,.54-.29.33l-4.51-3.6a3.68,3.68,0,0,0-2.86-.48c-1,.16-2.44.46-2.44.46a.68.68,0,0,0-.39.25.73.73,0,0,0-.14.45S.41,43,.54,44a3.63,3.63,0,0,0,1.25,2.62L6.48,50c.28.2.09.49-.23.37l-4.18-.94c-.32-.12-.5,0-.4.37,0,0,.69,1.89,1.31,3.16a24,24,0,0,0,1.66,2.74,1.34,1.34,0,0,0,1,.52l5,.13c.33,0,.41.38.1.48L7.51,58c-.31.1-.34.35-.07.55a14.29,14.29,0,0,0,3.05,1.66,13.09,13.09,0,0,0,5.9.5,25.13,25.13,0,0,0,4.34-1,9.55,9.55,0,0,1-.08-1.2,9.32,9.32,0,0,1,3.07-6.91"}),(0,p.jsx)(f.Path,{fill:"#5ba745",d:"M59.7,41.53a.73.73,0,0,0-.14-.45.68.68,0,0,0-.39-.25s-1.43-.3-2.44-.46a3.64,3.64,0,0,0-2.86.48l-4.51,3.6c-.26.21-.49-.06-.29-.33l2.06-3.76c.2-.28.09-.49-.25-.49,0,0-2,.15-3.39.39a23.91,23.91,0,0,0-3.1.84,1.4,1.4,0,0,0-.77.78l-1.5,4.8c-.11.32-.48.3-.49,0l-.24-3.42c0-.33-.24-.43-.51-.22a14.74,14.74,0,0,0-2.44,2.47A13.11,13.11,0,0,0,36.34,51c0,.18,0,.34-.06.52a9.26,9.26,0,0,1,3,8.1,24.1,24.1,0,0,0,4.34,1,13.09,13.09,0,0,0,5.9-.5,14.29,14.29,0,0,0,3.05-1.66c.27-.2.24-.45-.07-.55l-3.22-1.17c-.31-.1-.23-.47.1-.48l5-.13a1.38,1.38,0,0,0,1-.52A24.6,24.6,0,0,0,57,52.92c.61-1.27,1.31-3.16,1.31-3.16.1-.33-.08-.49-.4-.37l-4.18.94c-.32.12-.51-.17-.23-.37l4.69-3.34A3.63,3.63,0,0,0,59.46,44c.13-1,.24-2.47.24-2.47"}),(0,p.jsx)(f.Path,{fill:"#5ba745",d:"M46.5,25.61c0-.53-.35-.72-.8-.43l-4.86,2.66c-.45.28-.56-.27-.23-.69l4.66-6.23a2,2,0,0,0,.28-1.68,36.51,36.51,0,0,0-2.19-4.89,34,34,0,0,0-2.81-3.94c-.33-.41-.74-.35-.91.16l-2.28,5.68c-.16.5-.6.48-.59-.05l.28-8.93a2.54,2.54,0,0,0-.66-1.64S35,4.27,33.88,3.27,30.78.69,30.78.69a1.29,1.29,0,0,0-1.54,0s-1.88,1.49-3.12,2.59-2.48,2.35-2.48,2.35A2.5,2.5,0,0,0,23,7.27l.27,8.93c0,.53-.41.55-.58.05l-2.29-5.69c-.17-.5-.57-.56-.91-.14a35.77,35.77,0,0,0-3,4.2,35.55,35.55,0,0,0-2,4.62,2,2,0,0,0,.27,1.67l4.67,6.24c.33.42.23,1-.22.69l-4.87-2.66c-.45-.29-.82-.1-.82.43a18.6,18.6,0,0,0,.83,5.07,20.16,20.16,0,0,0,5.37,7.77c3.19,3,5.93,7.8,7.45,11.08A9.6,9.6,0,0,1,30,49.09a9.31,9.31,0,0,1,2.86.45c1.52-3.28,4.26-8.11,7.44-11.09a20.46,20.46,0,0,0,5.09-7,19,19,0,0,0,1.11-5.82"}),(0,p.jsx)(f.Path,{fill:"#5ba745",d:"M36.12,58.44A6.12,6.12,0,1,1,30,52.32a6.11,6.11,0,0,1,6.12,6.12"})]})},L=function(){return L=Object.assign||function(e){for(var t,n=1,r=arguments.length;nhere.',"wp-parsely"):s.code===$.ParselySuggestionsApiOpenAiError||s.code===$.ParselySuggestionsApiOpenAiUnavailable?s.message=(0,_.__)("The Parse.ly API returned an internal server error. Please retry with a different input, or try again later.","wp-parsely"):s.code===$.HttpRequestFailed&&s.message.includes("cURL error 28")?s.message=(0,_.__)("The Parse.ly API did not respond in a timely manner. Please try again later.","wp-parsely"):s.code===$.ParselySuggestionsApiSchemaError?s.message=(0,_.__)("The Parse.ly API returned a validation error. Please try again with different parameters.","wp-parsely"):s.code===$.ParselySuggestionsApiNoData?s.message=(0,_.__)("The Parse.ly API couldn't find any relevant data to fulfill the request. Please retry with a different input.","wp-parsely"):s.code===$.ParselySuggestionsApiOpenAiSchema?s.message=(0,_.__)("The Parse.ly API returned an incorrect response. Please try again later.","wp-parsely"):s.code===$.ParselySuggestionsApiAuthUnavailable&&(s.message=(0,_.__)("The Parse.ly API is currently unavailable. Please try again later.","wp-parsely")),s}return re(t,e),t.prototype.Message=function(e){return void 0===e&&(e=null),[$.PluginCredentialsNotSetMessageDetected,$.PluginSettingsSiteIdNotSet,$.PluginSettingsApiSecretNotSet].includes(this.code)?K(e):(this.code===$.FetchError&&(this.hint=this.Hint((0,_.__)("This error can sometimes be caused by ad-blockers or browser tracking protections. Please add this site to any applicable allow lists and try again.","wp-parsely"))),this.code!==$.ParselyApiForbidden&&this.code!==$.ParselySuggestionsApiNoAuthentication||(this.hint=this.Hint((0,_.__)("Please ensure that the Site ID and API Secret given in the plugin's settings are correct.","wp-parsely"))),this.code===$.HttpRequestFailed&&(this.hint=this.Hint((0,_.__)("The Parse.ly API cannot be reached. Please verify that you are online.","wp-parsely"))),(0,p.jsx)(W,{className:null==e?void 0:e.className,testId:"error",children:"

".concat(this.message,"

").concat(this.hint?this.hint:"")}))},t.prototype.Hint=function(e){return'

'.concat((0,_.__)("Hint:","wp-parsely")," ").concat(e,"

")},t.prototype.createErrorSnackbar=function(){//.test(this.message)||(0,h.dispatch)("core/notices").createNotice("error",this.message,{type:"snackbar"})},t}(Error),se=function(e){var t=e.isDetectingEnabled,n=e.onLinkChange,r=e.onLinkRemove,i=e.onLinkAdd,s=e.debounceValue,o=void 0===s?500:s,a=(0,h.useSelect)((function(e){return{blocks:(0,e("core/block-editor").getBlocks)()}}),[]).blocks,l=(0,b.useRef)(a),c=(0,b.useRef)(t);return(0,b.useEffect)((function(){var e=(0,z.debounce)((function(){for(var t=[],s=0;s0)return r(e.innerBlocks,t[s].innerBlocks);if(JSON.stringify(e)!==JSON.stringify(t[s])){var o=t[s],a=i.parseFromString(e.attributes.content||"","text/html"),l=i.parseFromString((null==o?void 0:o.attributes.content)||"","text/html"),c=Array.from(a.querySelectorAll("a[data-smartlink]")),u=Array.from(l.querySelectorAll("a[data-smartlink]")),d=c.filter((function(e){return!u.some((function(t){return t.dataset.smartlink===e.dataset.smartlink}))})),p=u.filter((function(e){return!c.some((function(t){return t.dataset.smartlink===e.dataset.smartlink}))})),f=c.filter((function(e){var t=u.find((function(t){return t.dataset.smartlink===e.dataset.smartlink}));return t&&t.outerHTML!==e.outerHTML}));(d.length>0||p.length>0||f.length>0)&&n.push({block:e,prevBlock:o,addedLinks:d,removedLinks:p,changedLinks:f})}}}))};return r(e,t),n}(a,l.current);o.length>0&&(o.forEach((function(e){e.changedLinks.length>0&&n&&n(e),e.addedLinks.length>0&&i&&i(e),e.removedLinks.length>0&&r&&r(e)})),l.current=a)}),o);return e(t),function(){e.cancel()}}),[a,o,t,i,n,r]),null},oe=function(e){var t=e.value,n=e.onChange,r=e.max,i=e.min,s=e.suffix,o=e.size,a=e.label,l=e.initialPosition,c=e.disabled,u=e.className;return(0,p.jsxs)("div",{className:"parsely-inputrange-control ".concat(u||""),children:[(0,p.jsx)(f.__experimentalHeading,{className:"parsely-inputrange-control__label",level:3,children:a}),(0,p.jsxs)("div",{className:"parsely-inputrange-control__controls",children:[(0,p.jsx)(f.__experimentalNumberControl,{disabled:c,value:t,suffix:(0,p.jsx)(f.__experimentalInputControlSuffixWrapper,{children:s}),size:null!=o?o:"__unstable-large",min:i,max:r,onChange:function(e){var t=parseInt(e,10);isNaN(t)||n(t)}}),(0,p.jsx)(f.RangeControl,{disabled:c,value:t,showTooltip:!1,initialPosition:l,onChange:function(e){n(e)},withInputField:!1,min:i,max:r})]})]})},ae=function(e,t,n,r){return new(n||(n=Promise))((function(i,s){function o(e){try{l(r.next(e))}catch(e){s(e)}}function a(e){try{l(r.throw(e))}catch(e){s(e)}}function l(e){var t;e.done?i(e.value):(t=e.value,t instanceof n?t:new n((function(e){e(t)}))).then(o,a)}l((r=r.apply(e,t||[])).next())}))},le=function(e,t){var n,r,i,s,o={label:0,sent:function(){if(1&i[0])throw i[1];return i[1]},trys:[],ops:[]};return s={next:a(0),throw:a(1),return:a(2)},"function"==typeof Symbol&&(s[Symbol.iterator]=function(){return this}),s;function a(a){return function(l){return function(a){if(n)throw new TypeError("Generator is already executing.");for(;s&&(s=0,a[0]&&(o=0)),o;)try{if(n=1,r&&(i=2&a[0]?r.return:a[0]?r.throw||((i=r.return)&&i.call(r),0):r.next)&&!(i=i.call(r,a[1])).done)return i;switch(r=0,i&&(a=[2&a[0],i.value]),a[0]){case 0:case 1:i=a;break;case 4:return o.label++,{value:a[1],done:!1};case 5:o.label++,r=a[1],a=[0];continue;case 7:a=o.ops.pop(),o.trys.pop();continue;default:if(!((i=(i=o.trys).length>0&&i[i.length-1])||6!==a[0]&&2!==a[0])){o=0;continue}if(3===a[0]&&(!i||a[1]>i[0]&&a[1]0&&i[i.length-1])||6!==a[0]&&2!==a[0])){o=0;continue}if(3===a[0]&&(!i||a[1]>i[0]&&a[1]0&&i[i.length-1])||6!==a[0]&&2!==a[0])){o=0;continue}if(3===a[0]&&(!i||a[1]>i[0]&&a[1]0&&i[i.length-1])||6!==a[0]&&2!==a[0])){o=0;continue}if(3===a[0]&&(!i||a[1]>i[0]&&a[1]0&&i[i.length-1])||6!==a[0]&&2!==a[0])){o=0;continue}if(3===a[0]&&(!i||a[1]>i[0]&&a[1]

","\n\x3c!-- /wp:paragraph --\x3e");t&&h((0,Q.parse)(n))}),[s]),(0,p.jsxs)("div",{className:"smart-linking-review-suggestion",children:[(0,p.jsx)(f.KeyboardShortcuts,{shortcuts:{left:o,right:a,up:o,down:a}}),(0,p.jsx)("div",{className:"review-suggestion-post-title",children:null===(t=s.post_data)||void 0===t?void 0:t.title}),(0,p.jsxs)("div",{className:"review-suggestion-preview",children:[!(null===(n=s.post_data)||void 0===n?void 0:n.is_first_paragraph)&&(0,p.jsx)($e,{topOrBottom:"top"}),(0,p.jsx)(Ze,{block:d[0],link:s,useOriginalBlock:!0}),!(null===(r=s.post_data)||void 0===r?void 0:r.is_last_paragraph)&&(0,p.jsx)($e,{topOrBottom:"bottom"})]}),(0,p.jsx)(f.__experimentalDivider,{}),(0,p.jsx)(We,{link:s}),(0,p.jsxs)("div",{className:"review-controls",children:[(0,p.jsx)(f.Tooltip,{shortcut:"←",text:(0,_.__)("Previous","wp-parsely"),children:(0,p.jsx)(f.Button,{disabled:!l,className:"wp-parsely-review-suggestion-previous",onClick:o,icon:He,children:(0,_.__)("Previous","wp-parsely")})}),(0,p.jsx)("div",{className:"reviews-controls-middle",children:(0,p.jsx)(f.Button,{target:"_blank",href:(null===(i=s.post_data)||void 0===i?void 0:i.edit_link)+"&smart-link="+s.uid,variant:"secondary",onClick:function(){P.trackEvent("smart_linking_open_in_editor_pressed",{type:"inbound",uid:s.uid})},children:(0,_.__)("Open in the Editor","wp-parsely")})}),(0,p.jsx)(f.Tooltip,{shortcut:"→",text:(0,_.__)("Next","wp-parsely"),children:(0,p.jsxs)(f.Button,{disabled:!c,onClick:a,className:"wp-parsely-review-suggestion-next",children:[(0,_.__)("Next","wp-parsely"),(0,p.jsx)(X,{icon:ze})]})})]})]})},Ye=function(e){var t=e.size,n=void 0===t?24:t,r=e.className,i=void 0===r?"wp-parsely-icon":r;return(0,p.jsxs)(f.SVG,{xmlns:"http://www.w3.org/2000/svg",className:i,width:n,height:n,viewBox:"0 0 24 24",fill:"none",children:[(0,p.jsx)(f.Path,{d:"M8.18983 5.90381L8.83642 7.54325L10.4758 8.18983L8.83642 8.8364L8.18983 10.4759L7.54324 8.8364L5.90381 8.18983L7.54324 7.54325L8.18983 5.90381Z"}),(0,p.jsx)(f.Path,{d:"M15.048 5.90381L15.9101 8.08972L18.0961 8.95186L15.9101 9.81397L15.048 11.9999L14.1859 9.81397L12 8.95186L14.1859 8.08972L15.048 5.90381Z"}),(0,p.jsx)(f.Path,{d:"M11.238 10.4761L12.3157 13.2085L15.048 14.2861L12.3157 15.3638L11.238 18.0962L10.1603 15.3638L7.42798 14.2861L10.1603 13.2085L11.238 10.4761Z"})]})},Je=function(e,t,n){if(n||2===arguments.length)for(var r,i=0,s=t.length;ii.bottom)&&(n.scrollTop=r.offsetTop-n.offsetTop)}}}}),[t,l]);var u=function(){var e=document.querySelector(".smart-linking-review-sidebar-tabs [data-active-item]"),t=null==e?void 0:e.nextElementSibling;t||(t=document.querySelector('.smart-linking-review-sidebar-tabs [role="tab"]')),t&&t.click()},d=(0,p.jsxs)("span",{className:"smart-linking-menu-label",children:[(0,_.__)("NEW","wp-parsely"),(0,p.jsx)(Ye,{})]}),h=[];n&&n.length>0&&h.push({name:"outbound",title:(0,_.__)("Outbound","wp-parsely")}),r&&r.length>0&&h.push({name:"inbound",title:(0,_.__)("Inbound","wp-parsely")});var v="outbound";return h=h.filter((function(e){return"outbound"===e.name&&r&&0===r.length&&(e.title=(0,_.__)("Outbound Smart Links","wp-parsely"),v="outbound"),"inbound"===e.name&&n&&0===n.length&&(e.title=(0,_.__)("Inbound Smart Links","wp-parsely"),v="inbound"),e})),(0,p.jsxs)("div",{className:"smart-linking-review-sidebar",ref:s,children:[(0,p.jsx)(f.KeyboardShortcuts,{shortcuts:{tab:function(){return u()},"shift+tab":function(){return u()}}}),(0,p.jsx)(f.TabPanel,{className:"smart-linking-review-sidebar-tabs",initialTabName:v,tabs:h,onSelect:function(e){var t,s;"outbound"===e&&n&&n.length>0&&i(n[0]),"inbound"===e&&r&&r.length>0&&i(r[0]),P.trackEvent("smart_linking_modal_tab_selected",{tab:e,total_inbound:null!==(t=null==r?void 0:r.length)&&void 0!==t?t:0,total_outbound:null!==(s=null==n?void 0:n.length)&&void 0!==s?s:0})},children:function(e){return(0,p.jsxs)(p.Fragment,{children:["outbound"===e.name&&(0,p.jsx)(p.Fragment,{children:n&&0!==n.length?n.map((function(e,n){return(0,p.jsxs)(f.MenuItem,{ref:function(e){o.current[n]=e},className:(null==t?void 0:t.uid)===e.uid?"is-selected":"",role:"menuitemradio",isSelected:(null==t?void 0:t.uid)===e.uid,onClick:function(){return i(e)},children:[(0,p.jsx)("span",{className:"smart-linking-menu-item",children:e.text}),!e.applied&&d]},e.uid)})):(0,p.jsxs)(p.Fragment,{children:[" ",(0,_.__)("No outbound links found.","wp-parsely")]})}),"inbound"===e.name&&(0,p.jsxs)(p.Fragment,{children:[(0,p.jsx)("div",{className:"review-sidebar-tip",children:(0,_.__)("This section shows external posts that link back to the current post.","wp-parsely")}),r&&0!==r.length?r.map((function(e,r){var s;return(0,p.jsx)(f.MenuItem,{ref:function(e){o.current[(n?n.length:0)+r]=e},className:(null==t?void 0:t.uid)===e.uid?"is-selected":"",role:"menuitemradio",isSelected:(null==t?void 0:t.uid)===e.uid,onClick:function(){return i(e)},children:(0,p.jsx)("span",{className:"smart-linking-menu-item",children:null===(s=e.post_data)||void 0===s?void 0:s.title})},e.uid)})):(0,p.jsxs)(p.Fragment,{children:[" ",(0,_.__)("No inbound links found.","wp-parsely")]})]})]})}})]})},Xe=(0,p.jsx)(x.SVG,{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 24 24",children:(0,p.jsx)(x.Path,{d:"M12 13.06l3.712 3.713 1.061-1.06L13.061 12l3.712-3.712-1.06-1.06L12 10.938 8.288 7.227l-1.061 1.06L10.939 12l-3.712 3.712 1.06 1.061L12 13.061z"})}),et=(0,p.jsx)(x.SVG,{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 24 24",children:(0,p.jsx)(x.Path,{d:"M16.7 7.1l-6.3 8.5-3.3-2.5-.9 1.2 4.5 3.4L17.9 8z"})}),tt=function(e){var t,n,r,i,s=null===(t=e.link.match)||void 0===t?void 0:t.blockId,o=(0,h.useSelect)((function(e){var t=e("core/block-editor"),n=t.getBlock,r=t.getBlockParents;return s?{block:n(s),parents:r(s).map((function(e){return n(e)})).filter((function(e){return void 0!==e}))}:{block:void 0,parents:[]}}),[s]),a=o.block,l=o.parents;return a?(0,p.jsxs)("div",{className:"review-suggestions-breadcrumbs",children:[l.map((function(e,t){var n;return(0,p.jsxs)("span",{children:[(0,p.jsx)("span",{className:"breadcrumbs-parent-block",children:null===(n=(0,Q.getBlockType)(e.name))||void 0===n?void 0:n.title}),(0,p.jsx)("span",{className:"breadcrumbs-parent-separator",children:" / "})]},t)})),(0,p.jsxs)("span",{className:"breadcrumbs-current-block",children:[(0,p.jsx)("span",{className:"breadcrumbs-current-block-type",children:null===(n=(0,Q.getBlockType)(a.name))||void 0===n?void 0:n.title}),(null===(i=null===(r=a.attributes)||void 0===r?void 0:r.metadata)||void 0===i?void 0:i.name)&&(0,p.jsx)("span",{className:"breadcrumbs-current-block-name",children:a.attributes.metadata.name})]})]}):(0,p.jsx)(p.Fragment,{})},nt=function(e){var t,n=e.link,r=(0,b.useState)(n.href),i=r[0],s=r[1],o=(0,b.useState)(null===(t=n.destination)||void 0===t?void 0:t.post_type),a=o[0],l=o[1],c=(0,b.useRef)(null),u=(0,h.useDispatch)(Te).updateSmartLink;return(0,b.useEffect)((function(){n.destination?l(n.destination.post_type):(l((0,_.__)("External","wp-parsely")),De.getInstance().getPostTypeByURL(n.href).then((function(e){e&&l(e.post_type),n.destination=e,u(n)})))}),[n,u]),(0,b.useEffect)((function(){var e=function(){if(c.current){var e=c.current.offsetWidth,t=Math.floor(e/8);s(function(e,t){var n=e.replace(/(^\w+:|^)\/\//,"").replace(/^www\./,"");if(!t||n.length<=t)return n;var r=n.split("/")[0],i=n.substring(r.length);t-=r.length;var s=Math.floor((t-3)/2),o=i.substring(0,s),a=i.substring(i.length-s);return"".concat(r).concat(o,"...").concat(a)}(n.href,t))}};return e(),window.addEventListener("resize",e),function(){window.removeEventListener("resize",e)}}),[n]),(0,p.jsx)(f.MenuItem,{ref:c,info:i,iconPosition:"left",icon:Ge,shortcut:a,className:"block-editor-link-control__search-item wp-parsely-link-suggestion-link-details",children:n.title})},rt=function(e){var t=e.link,n=e.onNext,r=e.onPrevious,i=e.onAccept,s=e.onReject,o=e.onRemove,a=e.onSelectInEditor,l=e.hasPrevious,c=e.hasNext;if(t&&void 0!==t.post_data)return(0,p.jsx)(Ke,{link:t,onNext:n,onPrevious:r,onAccept:i,onReject:s,onRemove:o,onSelectInEditor:a,hasPrevious:l,hasNext:c});if(!(null==t?void 0:t.match))return(0,p.jsx)(p.Fragment,{children:(0,_.__)("This Smart Link does not have any matches in the current content.","wp-parsely")});var u=t.match.blockId,d=(0,h.select)("core/block-editor").getBlock(u),v=t.applied;return d?(0,p.jsxs)("div",{className:"smart-linking-review-suggestion",children:[(0,p.jsx)(f.KeyboardShortcuts,{shortcuts:{left:r,right:n,up:r,down:n,a:function(){t&&!t.applied&&i()},r:function(){t&&(t.applied?o():s())}}}),(0,p.jsx)(tt,{link:t}),(0,p.jsx)("div",{className:"review-suggestion-preview",children:(0,p.jsx)(Ze,{block:d,link:t})}),(0,p.jsx)(f.__experimentalDivider,{}),(0,p.jsx)(nt,{link:t}),(0,p.jsxs)("div",{className:"review-controls",children:[(0,p.jsx)(f.Tooltip,{shortcut:"←",text:(0,_.__)("Previous","wp-parsely"),children:(0,p.jsx)(f.Button,{disabled:!l,className:"wp-parsely-review-suggestion-previous",onClick:r,icon:He,children:(0,_.__)("Previous","wp-parsely")})}),(0,p.jsxs)("div",{className:"reviews-controls-middle",children:[!v&&(0,p.jsxs)(p.Fragment,{children:[(0,p.jsx)(f.Tooltip,{shortcut:"R",text:(0,_.__)("Reject","wp-parsely"),children:(0,p.jsx)(f.Button,{className:"wp-parsely-review-suggestion-reject",icon:Xe,onClick:s,variant:"secondary",children:(0,_.__)("Reject","wp-parsely")})}),(0,p.jsx)(f.Tooltip,{shortcut:"A",text:(0,_.__)("Accept","wp-parsely"),children:(0,p.jsx)(f.Button,{className:"wp-parsely-review-suggestion-accept",icon:et,onClick:i,variant:"secondary",children:(0,_.__)("Accept","wp-parsely")})})]}),v&&(0,p.jsxs)(p.Fragment,{children:[(0,p.jsx)(f.Tooltip,{shortcut:"R",text:(0,_.__)("Remove","wp-parsely"),children:(0,p.jsx)(f.Button,{className:"wp-parsely-review-suggestion-reject",icon:Xe,onClick:o,variant:"secondary",children:(0,_.__)("Remove","wp-parsely")})}),(0,p.jsx)(f.Button,{className:"wp-parsely-review-suggestion-accept",onClick:a,variant:"secondary",children:(0,_.__)("Select in Editor","wp-parsely")})]})]}),(0,p.jsx)(f.Tooltip,{shortcut:"→",text:(0,_.__)("Next","wp-parsely"),children:(0,p.jsxs)(f.Button,{disabled:!c,onClick:n,className:"wp-parsely-review-suggestion-next",children:[(0,_.__)("Next","wp-parsely"),(0,p.jsx)(X,{icon:ze})]})})]})]}):(0,p.jsx)(p.Fragment,{children:(0,_.__)("No block is selected.","wp-parsely")})},it=function(e,t,n,r){return new(n||(n=Promise))((function(i,s){function o(e){try{l(r.next(e))}catch(e){s(e)}}function a(e){try{l(r.throw(e))}catch(e){s(e)}}function l(e){var t;e.done?i(e.value):(t=e.value,t instanceof n?t:new n((function(e){e(t)}))).then(o,a)}l((r=r.apply(e,t||[])).next())}))},st=function(e,t){var n,r,i,s,o={label:0,sent:function(){if(1&i[0])throw i[1];return i[1]},trys:[],ops:[]};return s={next:a(0),throw:a(1),return:a(2)},"function"==typeof Symbol&&(s[Symbol.iterator]=function(){return this}),s;function a(a){return function(l){return function(a){if(n)throw new TypeError("Generator is already executing.");for(;s&&(s=0,a[0]&&(o=0)),o;)try{if(n=1,r&&(i=2&a[0]?r.return:a[0]?r.throw||((i=r.return)&&i.call(r),0):r.next)&&!(i=i.call(r,a[1])).done)return i;switch(r=0,i&&(a=[2&a[0],i.value]),a[0]){case 0:case 1:i=a;break;case 4:return o.label++,{value:a[1],done:!1};case 5:o.label++,r=a[1],a=[0];continue;case 7:a=o.ops.pop(),o.trys.pop();continue;default:if(!((i=(i=o.trys).length>0&&i[i.length-1])||6!==a[0]&&2!==a[0])){o=0;continue}if(3===a[0]&&(!i||a[1]>i[0]&&a[1]0&&(a=o[0],(l=a.parentNode)&&(c=document.createTextNode(null!==(u=a.textContent)&&void 0!==u?u:""),l.replaceChild(c,a),te.updateBlockAttributes(n,{content:s.innerHTML}))),[4,E(t.uid)]):[2]):[2];case 1:return d.sent(),[2]}}))}))},C=(0,b.useCallback)((function(){c(!1),w().filter((function(e){return!e.applied})).length>0?o(!0):(ne.unlockPostAutosaving("smart-linking-review-modal"),t())}),[w,t]),A=function(e){o(!1),e?(c(!1),T().then((function(){C()}))):c(!0)},O=function(){if(ue(k)){var e=g.indexOf(k);if(!g[t=e+1])return;S(g[t])}else{var t;if(e=v.indexOf(k),!v[t=e+1])return;S(v[t])}},R=function(){if(ue(k)){var e=g.indexOf(k);if(!g[t=e-1])return;S(g[t])}else{var t;if(e=v.indexOf(k),!v[t=e-1])return;S(v[t])}};return(0,b.useEffect)((function(){l?ne.lockPostAutosaving("smart-linking-review-modal"):l&&0===d.length&&C()}),[l,t,d,C]),(0,b.useEffect)((function(){c(n)}),[n]),(0,p.jsxs)(p.Fragment,{children:[l&&(0,p.jsx)(f.Modal,{title:(0,_.__)("Review Smart Links","wp-parsely"),className:"wp-parsely-smart-linking-review-modal",onRequestClose:C,shouldCloseOnClickOutside:!1,shouldCloseOnEsc:!1,children:(0,p.jsxs)("div",{className:"smart-linking-modal-body",children:[(0,p.jsx)(Qe,{outboundLinks:v,inboundLinks:g,activeLink:k,setSelectedLink:S}),k&&(ue(k)?(0,p.jsx)(Ke,{link:k,onNext:O,onPrevious:R,hasNext:g.indexOf(k)0}):(0,p.jsx)(rt,{link:k,hasNext:m().indexOf(k)0,onNext:O,onPrevious:R,onAccept:function(){return it(void 0,void 0,void 0,(function(){var e,t;return st(this,(function(n){switch(n.label){case 0:return k.match?(r(k),[4,(i=k.match.blockId,s=k,it(void 0,void 0,void 0,(function(){var e,t;return st(this,(function(n){switch(n.label){case 0:return(e=document.createElement("a")).href=s.href,e.title=s.title,e.setAttribute("data-smartlink",s.uid),(t=(0,h.select)("core/block-editor").getBlock(i))?(fe(t,s,e),s.applied=!0,[4,L(s)]):[2];case 1:return n.sent(),[2]}}))})))]):[2];case 1:return n.sent(),P.trackEvent("smart_linking_link_accepted",{link:k.href,title:k.title,text:k.text,uid:k.uid}),0===y().length?(C(),[2]):(e=v.indexOf(k),v[t=e+1]?S(v[t]):S(v[0]),[2])}var i,s}))}))},onReject:function(){return it(void 0,void 0,void 0,(function(){var e,t;return st(this,(function(n){switch(n.label){case 0:return e=v.indexOf(k),v[t=e+1]?S(v[t]):v[0]?S(v[0]):C(),[4,E(k.uid)];case 1:return n.sent(),P.trackEvent("smart_linking_link_rejected",{link:k.href,title:k.title,text:k.text,uid:k.uid}),[2]}}))}))},onRemove:function(){return it(void 0,void 0,void 0,(function(){var e,t,n,r;return st(this,(function(i){switch(i.label){case 0:return k.match?(e=(0,h.select)("core/block-editor").getBlock(k.match.blockId))?(t=m(),n=t.indexOf(k),r=n-1,[4,N(e,k)]):[3,2]:[2];case 1:if(i.sent(),P.trackEvent("smart_linking_link_removed",{link:k.href,title:k.title,text:k.text,uid:k.uid}),0===(t=m()).length&&g.length>0)return S(g[0]),[2];if(0===t.length&&0===g.length)return C(),[2];if(t[r])return S(t[r]),[2];S(t[0]),i.label=2;case 2:return[2]}}))}))},onSelectInEditor:function(){if(k.match){var e=(0,h.select)("core/block-editor").getBlock(k.match.blockId);if(e){te.selectBlock(e.clientId);var t=document.querySelector('[data-block="'.concat(e.clientId,'"]'));t&&ke(t,k.uid),P.trackEvent("smart_linking_select_in_editor_pressed",{type:"outbound",uid:k.uid}),C()}}}}))]})}),s&&(0,p.jsxs)(f.Modal,{title:(0,_.__)("Review Smart Links","wp-parsely"),onRequestClose:function(){return A(!1)},className:"wp-parsely-smart-linking-close-dialog",children:[(0,_.__)("Are you sure you want to close? All un-accepted smart links will not be added.","wp-parsely"),(0,p.jsxs)("div",{className:"smart-linking-close-dialog-actions",children:[(0,p.jsx)(f.Button,{variant:"secondary",onClick:function(){return A(!1)},children:(0,_.__)("Go Back","wp-parsely")}),(0,p.jsx)(f.Button,{variant:"primary",onClick:function(){return A(!0)},children:(0,_.__)("Close","wp-parsely")})]})]})]})})),at=function(){return at=Object.assign||function(e){for(var t,n=1,r=arguments.length;n0&&i[i.length-1])||6!==a[0]&&2!==a[0])){o=0;continue}if(3===a[0]&&(!i||a[1]>i[0]&&a[1]0&&k("success",/* translators: %d: number of smart links applied */ /* translators: %d: number of smart links applied */ +/* translators: %s: block name */n((0,_.sprintf)((0,_.__)("%s blocks are not supported for Smart Links.","wp-parsely"),s))}T(_e.All===C)}}),[d,S,C,r,i,y,E,T,c]),(0,b.useEffect)((function(){if(!r&&o.current&&C&&!k&&y){var e=o.current.querySelector('button[data-value="'.concat(C,'"]'));e&&"true"!==e.getAttribute("aria-checked")&&(E(C),L(!0))}}),[y,x,r,S]),(0,b.useEffect)((function(){c(null)}),[y]),(0,p.jsx)("div",{className:"parsely-panel-settings",children:(0,p.jsxs)("div",{className:"parsely-panel-settings-body",children:[(0,p.jsxs)("div",{className:"smart-linking-block-select",children:[(0,p.jsx)(f.Disabled,{isDisabled:r,children:(0,p.jsxs)(f.__experimentalToggleGroupControl,{ref:o,__nextHasNoMarginBottom:!0,__next40pxDefaultSize:!0,isBlock:!0,value:C,label:(0,_.__)("Apply Smart Links to","wp-parsely"),onChange:function(e){return Ee(void 0,void 0,void 0,(function(){return Ne(this,(function(t){switch(t.label){case 0:return r?[2]:(v(!0),[4,T(_e.All===e)]);case 1:return t.sent(),[4,E(e)];case 2:return t.sent(),setTimeout((function(){v(!1)}),500),[2]}}))}))},children:[(0,p.jsx)(f.__experimentalToggleGroupControlOption,{label:(0,_.__)("Selected Block","wp-parsely"),value:"selected"}),(0,p.jsx)(f.__experimentalToggleGroupControlOption,{label:(0,_.__)("All Blocks","wp-parsely"),value:"all"})]})}),l&&(0,p.jsxs)("div",{className:"wp-parsely-smart-linking-hint",children:[(0,p.jsx)("strong",{children:(0,_.__)("Hint:","wp-parsely")})," ",l]})]}),(0,p.jsx)("div",{className:"smart-linking-settings",children:(0,p.jsx)(oe,{value:w,onChange:function(e){j(null!=e?e:1),s("MaxLinks",null!=e?e:mt)},label:(0,_.__)("Target Number of Links","wp-parsely"),suffix:(0,_.__)("Links","wp-parsely"),min:1,max:20,initialPosition:w,disabled:r})})]})})},Ae=window.wp.editor,Oe=window.wp.url,Re=function(){function e(){this.abortControllers=new Map}return e.prototype.cancelRequest=function(e){if(e)(t=this.abortControllers.get(e))&&(t.abort(),this.abortControllers.delete(e));else{var t,n=Array.from(this.abortControllers.keys()).pop();n&&(t=this.abortControllers.get(n))&&(t.abort(),this.abortControllers.delete(n))}},e.prototype.cancelAll=function(){this.abortControllers.forEach((function(e){return e.abort()})),this.abortControllers.clear()},e.prototype.getOrCreateController=function(e){if(e&&this.abortControllers.has(e))return{abortController:this.abortControllers.get(e),abortId:e};var t=null!=e?e:"auto-"+Date.now(),n=new AbortController;return this.abortControllers.set(t,n),{abortController:n,abortId:t}},e.prototype.fetch=function(e,t){return n=this,r=void 0,s=function(){var n,r,i,s,o,a;return function(e,t){var n,r,i,s,o={label:0,sent:function(){if(1&i[0])throw i[1];return i[1]},trys:[],ops:[]};return s={next:a(0),throw:a(1),return:a(2)},"function"==typeof Symbol&&(s[Symbol.iterator]=function(){return this}),s;function a(a){return function(l){return function(a){if(n)throw new TypeError("Generator is already executing.");for(;s&&(s=0,a[0]&&(o=0)),o;)try{if(n=1,r&&(i=2&a[0]?r.return:a[0]?r.throw||((i=r.return)&&i.call(r),0):r.next)&&!(i=i.call(r,a[1])).done)return i;switch(r=0,i&&(a=[2&a[0],i.value]),a[0]){case 0:case 1:i=a;break;case 4:return o.label++,{value:a[1],done:!1};case 5:o.label++,r=a[1],a=[0];continue;case 7:a=o.ops.pop(),o.trys.pop();continue;default:if(!((i=(i=o.trys).length>0&&i[i.length-1])||6!==a[0]&&2!==a[0])){o=0;continue}if(3===a[0]&&(!i||a[1]>i[0]&&a[1]0&&i[i.length-1])||6!==a[0]&&2!==a[0])){o=0;continue}if(3===a[0]&&(!i||a[1]>i[0]&&a[1]0&&i[i.length-1])||6!==a[0]&&2!==a[0])){o=0;continue}if(3===a[0]&&(!i||a[1]>i[0]&&a[1]

","\n\x3c!-- /wp:paragraph --\x3e");t&&h((0,Q.parse)(n))}),[s]),(0,p.jsxs)("div",{className:"smart-linking-review-suggestion",children:[(0,p.jsx)(f.KeyboardShortcuts,{shortcuts:{left:o,right:a,up:o,down:a}}),(0,p.jsx)("div",{className:"review-suggestion-post-title",children:null===(t=s.post_data)||void 0===t?void 0:t.title}),(0,p.jsxs)("div",{className:"review-suggestion-preview",children:[!(null===(n=s.post_data)||void 0===n?void 0:n.is_first_paragraph)&&(0,p.jsx)($e,{topOrBottom:"top"}),(0,p.jsx)(Ze,{block:d[0],link:s,useOriginalBlock:!0}),!(null===(r=s.post_data)||void 0===r?void 0:r.is_last_paragraph)&&(0,p.jsx)($e,{topOrBottom:"bottom"})]}),(0,p.jsx)(f.__experimentalDivider,{}),(0,p.jsx)(We,{link:s}),(0,p.jsxs)("div",{className:"review-controls",children:[(0,p.jsx)(f.Tooltip,{shortcut:"←",text:(0,_.__)("Previous","wp-parsely"),children:(0,p.jsx)(f.Button,{disabled:!l,className:"wp-parsely-review-suggestion-previous",onClick:o,icon:He,children:(0,_.__)("Previous","wp-parsely")})}),(0,p.jsx)("div",{className:"reviews-controls-middle",children:(0,p.jsx)(f.Button,{target:"_blank",href:(null===(i=s.post_data)||void 0===i?void 0:i.edit_link)+"&smart-link="+s.uid,variant:"secondary",onClick:function(){P.trackEvent("smart_linking_open_in_editor_pressed",{type:"inbound",uid:s.uid})},children:(0,_.__)("Open in the Editor","wp-parsely")})}),(0,p.jsx)(f.Tooltip,{shortcut:"→",text:(0,_.__)("Next","wp-parsely"),children:(0,p.jsxs)(f.Button,{disabled:!c,onClick:a,className:"wp-parsely-review-suggestion-next",children:[(0,_.__)("Next","wp-parsely"),(0,p.jsx)(X,{icon:ze})]})})]})]})},Ye=function(e){var t=e.size,n=void 0===t?24:t,r=e.className,i=void 0===r?"wp-parsely-icon":r;return(0,p.jsxs)(f.SVG,{xmlns:"http://www.w3.org/2000/svg",className:i,width:n,height:n,viewBox:"0 0 24 24",fill:"none",children:[(0,p.jsx)(f.Path,{d:"M8.18983 5.90381L8.83642 7.54325L10.4758 8.18983L8.83642 8.8364L8.18983 10.4759L7.54324 8.8364L5.90381 8.18983L7.54324 7.54325L8.18983 5.90381Z"}),(0,p.jsx)(f.Path,{d:"M15.048 5.90381L15.9101 8.08972L18.0961 8.95186L15.9101 9.81397L15.048 11.9999L14.1859 9.81397L12 8.95186L14.1859 8.08972L15.048 5.90381Z"}),(0,p.jsx)(f.Path,{d:"M11.238 10.4761L12.3157 13.2085L15.048 14.2861L12.3157 15.3638L11.238 18.0962L10.1603 15.3638L7.42798 14.2861L10.1603 13.2085L11.238 10.4761Z"})]})},Je=function(e,t,n){if(n||2===arguments.length)for(var r,i=0,s=t.length;ii.bottom)&&(n.scrollTop=r.offsetTop-n.offsetTop)}}}}),[t,l]);var u=function(){var e=document.querySelector(".smart-linking-review-sidebar-tabs [data-active-item]"),t=null==e?void 0:e.nextElementSibling;t||(t=document.querySelector('.smart-linking-review-sidebar-tabs [role="tab"]')),t&&t.click()},d=(0,p.jsxs)("span",{className:"smart-linking-menu-label",children:[(0,_.__)("NEW","wp-parsely"),(0,p.jsx)(Ye,{})]}),h=[];n&&n.length>0&&h.push({name:"outbound",title:(0,_.__)("Outbound","wp-parsely")}),r&&r.length>0&&h.push({name:"inbound",title:(0,_.__)("Inbound","wp-parsely")});var v="outbound";return h=h.filter((function(e){return"outbound"===e.name&&r&&0===r.length&&(e.title=(0,_.__)("Outbound Smart Links","wp-parsely"),v="outbound"),"inbound"===e.name&&n&&0===n.length&&(e.title=(0,_.__)("Inbound Smart Links","wp-parsely"),v="inbound"),e})),(0,p.jsxs)("div",{className:"smart-linking-review-sidebar",ref:s,children:[(0,p.jsx)(f.KeyboardShortcuts,{shortcuts:{tab:function(){return u()},"shift+tab":function(){return u()}}}),(0,p.jsx)(f.TabPanel,{className:"smart-linking-review-sidebar-tabs",initialTabName:v,tabs:h,onSelect:function(e){var t,s;"outbound"===e&&n&&n.length>0&&i(n[0]),"inbound"===e&&r&&r.length>0&&i(r[0]),P.trackEvent("smart_linking_modal_tab_selected",{tab:e,total_inbound:null!==(t=null==r?void 0:r.length)&&void 0!==t?t:0,total_outbound:null!==(s=null==n?void 0:n.length)&&void 0!==s?s:0})},children:function(e){return(0,p.jsxs)(p.Fragment,{children:["outbound"===e.name&&(0,p.jsx)(p.Fragment,{children:n&&0!==n.length?n.map((function(e,n){return(0,p.jsxs)(f.MenuItem,{ref:function(e){o.current[n]=e},className:(null==t?void 0:t.uid)===e.uid?"is-selected":"",role:"menuitemradio",isSelected:(null==t?void 0:t.uid)===e.uid,onClick:function(){return i(e)},children:[(0,p.jsx)("span",{className:"smart-linking-menu-item",children:e.text}),!e.applied&&d]},e.uid)})):(0,p.jsxs)(p.Fragment,{children:[" ",(0,_.__)("No outbound links found.","wp-parsely")]})}),"inbound"===e.name&&(0,p.jsxs)(p.Fragment,{children:[(0,p.jsx)("div",{className:"review-sidebar-tip",children:(0,_.__)("This section shows external posts that link back to the current post.","wp-parsely")}),r&&0!==r.length?r.map((function(e,r){var s;return(0,p.jsx)(f.MenuItem,{ref:function(e){o.current[(n?n.length:0)+r]=e},className:(null==t?void 0:t.uid)===e.uid?"is-selected":"",role:"menuitemradio",isSelected:(null==t?void 0:t.uid)===e.uid,onClick:function(){return i(e)},children:(0,p.jsx)("span",{className:"smart-linking-menu-item",children:null===(s=e.post_data)||void 0===s?void 0:s.title})},e.uid)})):(0,p.jsxs)(p.Fragment,{children:[" ",(0,_.__)("No inbound links found.","wp-parsely")]})]})]})}})]})},Xe=(0,p.jsx)(x.SVG,{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 24 24",children:(0,p.jsx)(x.Path,{d:"M12 13.06l3.712 3.713 1.061-1.06L13.061 12l3.712-3.712-1.06-1.06L12 10.938 8.288 7.227l-1.061 1.06L10.939 12l-3.712 3.712 1.06 1.061L12 13.061z"})}),et=(0,p.jsx)(x.SVG,{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 24 24",children:(0,p.jsx)(x.Path,{d:"M16.7 7.1l-6.3 8.5-3.3-2.5-.9 1.2 4.5 3.4L17.9 8z"})}),tt=function(e){var t,n,r,i,s=null===(t=e.link.match)||void 0===t?void 0:t.blockId,o=(0,h.useSelect)((function(e){var t=e("core/block-editor"),n=t.getBlock,r=t.getBlockParents;return s?{block:n(s),parents:r(s).map((function(e){return n(e)})).filter((function(e){return void 0!==e}))}:{block:void 0,parents:[]}}),[s]),a=o.block,l=o.parents;return a?(0,p.jsxs)("div",{className:"review-suggestions-breadcrumbs",children:[l.map((function(e,t){var n;return(0,p.jsxs)("span",{children:[(0,p.jsx)("span",{className:"breadcrumbs-parent-block",children:null===(n=(0,Q.getBlockType)(e.name))||void 0===n?void 0:n.title}),(0,p.jsx)("span",{className:"breadcrumbs-parent-separator",children:" / "})]},t)})),(0,p.jsxs)("span",{className:"breadcrumbs-current-block",children:[(0,p.jsx)("span",{className:"breadcrumbs-current-block-type",children:null===(n=(0,Q.getBlockType)(a.name))||void 0===n?void 0:n.title}),(null===(i=null===(r=a.attributes)||void 0===r?void 0:r.metadata)||void 0===i?void 0:i.name)&&(0,p.jsx)("span",{className:"breadcrumbs-current-block-name",children:a.attributes.metadata.name})]})]}):(0,p.jsx)(p.Fragment,{})},nt=function(e){var t,n=e.link,r=(0,b.useState)(n.href),i=r[0],s=r[1],o=(0,b.useState)(null===(t=n.destination)||void 0===t?void 0:t.post_type),a=o[0],l=o[1],c=(0,b.useRef)(null),u=(0,h.useDispatch)(Te).updateSmartLink;return(0,b.useEffect)((function(){n.destination?l(n.destination.post_type):(l((0,_.__)("External","wp-parsely")),De.getInstance().getPostTypeByURL(n.href).then((function(e){e&&l(e.post_type),n.destination=e,u(n)})))}),[n,u]),(0,b.useEffect)((function(){var e=function(){if(c.current){var e=c.current.offsetWidth,t=Math.floor(e/8);s(function(e,t){var n=e.replace(/(^\w+:|^)\/\//,"").replace(/^www\./,"");if(!t||n.length<=t)return n;var r=n.split("/")[0],i=n.substring(r.length);t-=r.length;var s=Math.floor((t-3)/2),o=i.substring(0,s),a=i.substring(i.length-s);return"".concat(r).concat(o,"...").concat(a)}(n.href,t))}};return e(),window.addEventListener("resize",e),function(){window.removeEventListener("resize",e)}}),[n]),(0,p.jsx)(f.MenuItem,{ref:c,info:i,iconPosition:"left",icon:Ge,shortcut:a,className:"block-editor-link-control__search-item wp-parsely-link-suggestion-link-details",children:n.title})},rt=function(e){var t=e.link,n=e.onNext,r=e.onPrevious,i=e.onAccept,s=e.onReject,o=e.onRemove,a=e.onSelectInEditor,l=e.hasPrevious,c=e.hasNext;if(t&&void 0!==t.post_data)return(0,p.jsx)(Ke,{link:t,onNext:n,onPrevious:r,onAccept:i,onReject:s,onRemove:o,onSelectInEditor:a,hasPrevious:l,hasNext:c});if(!(null==t?void 0:t.match))return(0,p.jsx)(p.Fragment,{children:(0,_.__)("This Smart Link does not have any matches in the current content.","wp-parsely")});var u=t.match.blockId,d=(0,h.select)("core/block-editor").getBlock(u),v=t.applied;return d?(0,p.jsxs)("div",{className:"smart-linking-review-suggestion",children:[(0,p.jsx)(f.KeyboardShortcuts,{shortcuts:{left:r,right:n,up:r,down:n,a:function(){t&&!t.applied&&i()},r:function(){t&&(t.applied?o():s())}}}),(0,p.jsx)(tt,{link:t}),(0,p.jsx)("div",{className:"review-suggestion-preview",children:(0,p.jsx)(Ze,{block:d,link:t})}),(0,p.jsx)(f.__experimentalDivider,{}),(0,p.jsx)(nt,{link:t}),(0,p.jsxs)("div",{className:"review-controls",children:[(0,p.jsx)(f.Tooltip,{shortcut:"←",text:(0,_.__)("Previous","wp-parsely"),children:(0,p.jsx)(f.Button,{disabled:!l,className:"wp-parsely-review-suggestion-previous",onClick:r,icon:He,children:(0,_.__)("Previous","wp-parsely")})}),(0,p.jsxs)("div",{className:"reviews-controls-middle",children:[!v&&(0,p.jsxs)(p.Fragment,{children:[(0,p.jsx)(f.Tooltip,{shortcut:"R",text:(0,_.__)("Reject","wp-parsely"),children:(0,p.jsx)(f.Button,{className:"wp-parsely-review-suggestion-reject",icon:Xe,onClick:s,variant:"secondary",children:(0,_.__)("Reject","wp-parsely")})}),(0,p.jsx)(f.Tooltip,{shortcut:"A",text:(0,_.__)("Accept","wp-parsely"),children:(0,p.jsx)(f.Button,{className:"wp-parsely-review-suggestion-accept",icon:et,onClick:i,variant:"secondary",children:(0,_.__)("Accept","wp-parsely")})})]}),v&&(0,p.jsxs)(p.Fragment,{children:[(0,p.jsx)(f.Tooltip,{shortcut:"R",text:(0,_.__)("Remove","wp-parsely"),children:(0,p.jsx)(f.Button,{className:"wp-parsely-review-suggestion-reject",icon:Xe,onClick:o,variant:"secondary",children:(0,_.__)("Remove","wp-parsely")})}),(0,p.jsx)(f.Button,{className:"wp-parsely-review-suggestion-accept",onClick:a,variant:"secondary",children:(0,_.__)("Select in Editor","wp-parsely")})]})]}),(0,p.jsx)(f.Tooltip,{shortcut:"→",text:(0,_.__)("Next","wp-parsely"),children:(0,p.jsxs)(f.Button,{disabled:!c,onClick:n,className:"wp-parsely-review-suggestion-next",children:[(0,_.__)("Next","wp-parsely"),(0,p.jsx)(X,{icon:ze})]})})]})]}):(0,p.jsx)(p.Fragment,{children:(0,_.__)("No block is selected.","wp-parsely")})},it=function(e,t,n,r){return new(n||(n=Promise))((function(i,s){function o(e){try{l(r.next(e))}catch(e){s(e)}}function a(e){try{l(r.throw(e))}catch(e){s(e)}}function l(e){var t;e.done?i(e.value):(t=e.value,t instanceof n?t:new n((function(e){e(t)}))).then(o,a)}l((r=r.apply(e,t||[])).next())}))},st=function(e,t){var n,r,i,s,o={label:0,sent:function(){if(1&i[0])throw i[1];return i[1]},trys:[],ops:[]};return s={next:a(0),throw:a(1),return:a(2)},"function"==typeof Symbol&&(s[Symbol.iterator]=function(){return this}),s;function a(a){return function(l){return function(a){if(n)throw new TypeError("Generator is already executing.");for(;s&&(s=0,a[0]&&(o=0)),o;)try{if(n=1,r&&(i=2&a[0]?r.return:a[0]?r.throw||((i=r.return)&&i.call(r),0):r.next)&&!(i=i.call(r,a[1])).done)return i;switch(r=0,i&&(a=[2&a[0],i.value]),a[0]){case 0:case 1:i=a;break;case 4:return o.label++,{value:a[1],done:!1};case 5:o.label++,r=a[1],a=[0];continue;case 7:a=o.ops.pop(),o.trys.pop();continue;default:if(!((i=(i=o.trys).length>0&&i[i.length-1])||6!==a[0]&&2!==a[0])){o=0;continue}if(3===a[0]&&(!i||a[1]>i[0]&&a[1]0&&(a=o[0],(l=a.parentNode)&&(c=document.createTextNode(null!==(u=a.textContent)&&void 0!==u?u:""),l.replaceChild(c,a),te.updateBlockAttributes(n,{content:s.innerHTML}))),[4,E(t.uid)]):[2]):[2];case 1:return d.sent(),[2]}}))}))},C=(0,b.useCallback)((function(){c(!1),w().filter((function(e){return!e.applied})).length>0?o(!0):(ne.unlockPostAutosaving("smart-linking-review-modal"),t())}),[w,t]),A=function(e){o(!1),e?(c(!1),T().then((function(){C()}))):c(!0)},O=function(){if(ue(k)){var e=g.indexOf(k);if(!g[t=e+1])return;S(g[t])}else{var t;if(e=v.indexOf(k),!v[t=e+1])return;S(v[t])}},R=function(){if(ue(k)){var e=g.indexOf(k);if(!g[t=e-1])return;S(g[t])}else{var t;if(e=v.indexOf(k),!v[t=e-1])return;S(v[t])}};return(0,b.useEffect)((function(){l?ne.lockPostAutosaving("smart-linking-review-modal"):l&&0===d.length&&C()}),[l,t,d,C]),(0,b.useEffect)((function(){c(n)}),[n]),(0,p.jsxs)(p.Fragment,{children:[l&&(0,p.jsx)(f.Modal,{title:(0,_.__)("Review Smart Links","wp-parsely"),className:"wp-parsely-smart-linking-review-modal",onRequestClose:C,shouldCloseOnClickOutside:!1,shouldCloseOnEsc:!1,children:(0,p.jsxs)("div",{className:"smart-linking-modal-body",children:[(0,p.jsx)(Qe,{outboundLinks:v,inboundLinks:g,activeLink:k,setSelectedLink:S}),k&&(ue(k)?(0,p.jsx)(Ke,{link:k,onNext:O,onPrevious:R,hasNext:g.indexOf(k)0}):(0,p.jsx)(rt,{link:k,hasNext:m().indexOf(k)0,onNext:O,onPrevious:R,onAccept:function(){return it(void 0,void 0,void 0,(function(){var e,t;return st(this,(function(n){switch(n.label){case 0:return k.match?(r(k),[4,(i=k.match.blockId,s=k,it(void 0,void 0,void 0,(function(){var e,t;return st(this,(function(n){switch(n.label){case 0:return(e=document.createElement("a")).href=s.href,e.title=s.title,e.setAttribute("data-smartlink",s.uid),(t=(0,h.select)("core/block-editor").getBlock(i))?(fe(t,s,e),s.applied=!0,[4,L(s)]):[2];case 1:return n.sent(),[2]}}))})))]):[2];case 1:return n.sent(),P.trackEvent("smart_linking_link_accepted",{link:k.href,title:k.title,text:k.text,uid:k.uid}),0===y().length?(C(),[2]):(e=v.indexOf(k),v[t=e+1]?S(v[t]):S(v[0]),[2])}var i,s}))}))},onReject:function(){return it(void 0,void 0,void 0,(function(){var e,t;return st(this,(function(n){switch(n.label){case 0:return e=v.indexOf(k),v[t=e+1]?S(v[t]):v[0]?S(v[0]):C(),[4,E(k.uid)];case 1:return n.sent(),P.trackEvent("smart_linking_link_rejected",{link:k.href,title:k.title,text:k.text,uid:k.uid}),[2]}}))}))},onRemove:function(){return it(void 0,void 0,void 0,(function(){var e,t,n,r;return st(this,(function(i){switch(i.label){case 0:return k.match?(e=(0,h.select)("core/block-editor").getBlock(k.match.blockId))?(t=m(),n=t.indexOf(k),r=n-1,[4,N(e,k)]):[3,2]:[2];case 1:if(i.sent(),P.trackEvent("smart_linking_link_removed",{link:k.href,title:k.title,text:k.text,uid:k.uid}),0===(t=m()).length&&g.length>0)return S(g[0]),[2];if(0===t.length&&0===g.length)return C(),[2];if(t[r])return S(t[r]),[2];S(t[0]),i.label=2;case 2:return[2]}}))}))},onSelectInEditor:function(){if(k.match){var e=(0,h.select)("core/block-editor").getBlock(k.match.blockId);if(e){te.selectBlock(e.clientId);var t=document.querySelector('[data-block="'.concat(e.clientId,'"]'));t&&ke(t,k.uid),P.trackEvent("smart_linking_select_in_editor_pressed",{type:"outbound",uid:k.uid}),C()}}}}))]})}),s&&(0,p.jsxs)(f.Modal,{title:(0,_.__)("Review Smart Links","wp-parsely"),onRequestClose:function(){return A(!1)},className:"wp-parsely-smart-linking-close-dialog",children:[(0,_.__)("Are you sure you want to close? All un-accepted smart links will not be added.","wp-parsely"),(0,p.jsxs)("div",{className:"smart-linking-close-dialog-actions",children:[(0,p.jsx)(f.Button,{variant:"secondary",onClick:function(){return A(!1)},children:(0,_.__)("Go Back","wp-parsely")}),(0,p.jsx)(f.Button,{variant:"primary",onClick:function(){return A(!0)},children:(0,_.__)("Close","wp-parsely")})]})]})]})})),at=function(){return at=Object.assign||function(e){for(var t,n=1,r=arguments.length;n0&&i[i.length-1])||6!==a[0]&&2!==a[0])){o=0;continue}if(3===a[0]&&(!i||a[1]>i[0]&&a[1]0&&k("success",/* translators: %d: number of smart links applied */ /* translators: %d: number of smart links applied */ (0,_.sprintf)((0,_.__)("%s smart links successfully applied.","wp-parsely"),g),{type:"snackbar"}):y(0)}),[w]),(0,b.useEffect)((function(){if(!(Object.keys(R).length>0)){var e={maxLinksPerPost:a.SmartLinking.MaxLinks};te(e)}}),[te,a]);var he=(0,h.useSelect)((function(e){var t=e("core/block-editor"),r=t.getSelectedBlock,i=t.getBlock,s=t.getBlocks,o=e("core/editor"),a=o.getEditedPostContent,l=o.getCurrentPostAttribute;return{allBlocks:s(),selectedBlock:n?i(n):r(),postContent:a(),postPermalink:l("link")}}),[n]),ve=he.allBlocks,me=he.selectedBlock,xe=he.postContent,ke=he.postPermalink,Se=function(e){return lt(void 0,void 0,void 0,(function(){var t,n,r,i,s;return ct(this,(function(o){switch(o.label){case 0:t=[],o.label=1;case 1:return o.trys.push([1,4,,9]),[4,re((n=E||!me)?_e.All:_e.Selected)];case 2:return o.sent(),a=ke.replace(/^https?:\/\//i,""),r=["http://"+a,"https://"+a],i=function(e){return e.map((function(e){return e.href}))}(F),r.push.apply(r,i),[4,De.getInstance().generateSmartLinks(me&&!n?(0,Q.getBlockContent)(me):xe,O,r)];case 3:return t=o.sent(),[3,9];case 4:if((s=o.sent()).code&&s.code===$.ParselyAborted)throw s.numRetries=3-e,s;return e>0&&s.retryFetch?(console.error(s),[4,ce(!0)]):[3,8];case 5:return o.sent(),[4,ue()];case 6:return o.sent(),[4,Se(e-1)];case 7:return[2,o.sent()];case 8:throw s;case 9:return[2,t]}var a}))}))},Pe=function(){for(var e=[],t=0;t[type="button"]').forEach((function(e){e.setAttribute("disabled","disabled")}))},Ne=function(){document.querySelectorAll('.edit-post-header__settings>[type="button"]').forEach((function(e){e.removeAttribute("disabled")})),ne.unlockPostSaving("wp-parsely-block-overlay")};return(0,p.jsxs)("div",{className:"wp-parsely-smart-linking",children:[(0,p.jsx)(se,{isDetectingEnabled:!L,onLinkRemove:function(e){!function(e){ae(this,void 0,void 0,(function(){var t,n,r;return le(this,(function(i){switch(i.label){case 0:return[4,we((0,Q.getBlockContent)(e),e.clientId)];case 1:return t=i.sent(),n=t.missingSmartLinks,r=t.didAnyFixes,n.forEach((function(e){(0,h.dispatch)(Te).removeSmartLink(e.uid)})),[2,r]}}))}))}(e.block)}}),(0,p.jsxs)(f.PanelRow,{className:t,children:[(0,p.jsxs)("div",{className:"smart-linking-text",children:[(0,_.__)("Automatically insert links to your most relevant, top performing content.","wp-parsely"),(0,p.jsxs)(f.Button,{href:"https://docs.parse.ly/plugin-content-helper/#h-smart-linking-beta",target:"_blank",variant:"link",children:[(0,_.__)("Learn more about Parse.ly AI","wp-parsely"),(0,p.jsx)(X,{icon:ee,size:18,className:"parsely-external-link-icon"})]})]}),C&&(0,p.jsx)(f.Notice,{status:"info",onRemove:function(){return Z(null)},className:"wp-parsely-content-helper-error",children:C.Message()}),w&&g>0&&(0,p.jsx)(f.Notice,{status:"success",onRemove:function(){return x(!1)},className:"wp-parsely-smart-linking-suggested-links",children:(0,_.sprintf)(/* translators: 1 - number of smart links generated */ /* translators: 1 - number of smart links generated */ (0,_.__)("Successfully added %s smart links.","wp-parsely"),g>0?g:A.length)}),(0,p.jsx)(Ce,{disabled:T,selectedBlock:me,onSettingChange:function(e,t){var n;d({SmartLinking:at(at({},a.SmartLinking),(n={},n[e]=t,n))}),"MaxLinks"===e&&oe(t)}}),(0,p.jsx)("div",{className:"smart-linking-generate",children:(0,p.jsx)(f.Button,{onClick:function(){return lt(void 0,void 0,void 0,(function(){var e,t,n,r,s,o,a,l;return ct(this,(function(c){switch(c.label){case 0:return[4,q(!0)];case 1:return c.sent(),[4,de()];case 2:return c.sent(),[4,Z(null)];case 3:return c.sent(),x(!1),P.trackEvent("smart_linking_generate_pressed",{is_full_content:E,selected_block:null!==(o=null==me?void 0:me.name)&&void 0!==o?o:"none",context:i}),[4,Pe(E?"all":null==me?void 0:me.clientId)];case 4:c.sent(),e=setTimeout((function(){var e;q(!1),P.trackEvent("smart_linking_generate_timeout",{is_full_content:E,selected_block:null!==(e=null==me?void 0:me.name)&&void 0!==e?e:"none",context:i}),je(E?"all":null==me?void 0:me.clientId)}),18e4),t=I,c.label=5;case 5:return c.trys.push([5,8,10,15]),[4,Se(3)];case 6:return n=c.sent(),[4,(u=n,lt(void 0,void 0,void 0,(function(){var e;return ct(this,(function(t){switch(t.label){case 0:return u=u.filter((function(e){return!F.some((function(t){return t.uid===e.uid&&t.applied}))})),e=ke.replace(/^https?:\/\//,"").replace(/\/+$/,""),u=(u=u.filter((function(t){return!t.href.includes(e)||(console.warn("PCH Smart Linking: Skipping self-reference link: ".concat(t.href)),!1)}))).filter((function(e){return!F.some((function(t){return t.href===e.href?(console.warn("PCH Smart Linking: Skipping duplicate link: ".concat(e.href)),!0):t.text===e.text&&t.offset!==e.offset&&(console.warn("PCH Smart Linking: Skipping duplicate link text: ".concat(e.text)),!0)}))})),u=(u=ge(E?ve:[me],u,{}).filter((function(e){return e.match}))).filter((function(e){if(!e.match)return!1;var t=e.match.blockLinkPosition,n=t+e.text.length;return!F.some((function(r){if(!r.match)return!1;if(e.match.blockId!==r.match.blockId)return!1;var i=r.match.blockLinkPosition,s=i+r.text.length;return t>=i&&n<=s}))})),[4,W(u)];case 1:return t.sent(),[2,u]}}))})))];case 7:if(0===c.sent().length)throw new ie((0,_.__)("No smart links were generated.","wp-parsely"),$.ParselySuggestionsApiNoData,"");return pe(!0),[3,15];case 8:return r=c.sent(),s=new ie(null!==(a=r.message)&&void 0!==a?a:"An unknown error has occurred.",null!==(l=r.code)&&void 0!==l?l:$.UnknownError),r.code&&r.code===$.ParselyAborted&&(s.message=(0,_.sprintf)(/* translators: %d: number of retry attempts, %s: attempt plural */ /* translators: %d: number of retry attempts, %s: attempt plural */ (0,_.__)("The Smart Linking process was cancelled after %1$d %2$s.","wp-parsely"),r.numRetries,(0,_._n)("attempt","attempts",r.numRetries,"wp-parsely"))),console.error(r),[4,Z(s)];case 9:return c.sent(),s.createErrorSnackbar(),[3,15];case 10:return[4,q(!1)];case 11:return c.sent(),[4,re(t)];case 12:return c.sent(),[4,ce(!1)];case 13:return c.sent(),[4,je(E?"all":null==me?void 0:me.clientId)];case 14:return c.sent(),clearTimeout(e),[7];case 15:return[2]}var u}))}))},variant:"primary",isBusy:T,disabled:T,children:M?(0,_.sprintf)(/* translators: %1$d: number of retry attempts, %2$d: maximum number of retries */ /* translators: %1$d: number of retry attempts, %2$d: maximum number of retries */ @@ -22,7 +22,7 @@ message:(0,_.sprintf)((0,_.__)('in section "%1$s"',"wp-parsely"),n.value)};if(w. message:(0,_.sprintf)((0,_.__)('by author "%1$s"',"wp-parsely"),n.value)};throw new ie((0,_.__)("No valid filter type has been specified.","wp-parsely"),$.CannotFormulateApiQuery)},t}(Re),xn=function(){return xn=Object.assign||function(e){for(var t,n=1,r=arguments.length;n0&&i[i.length-1])||6!==a[0]&&2!==a[0])){o=0;continue}if(3===a[0]&&(!i||a[1]>i[0]&&a[1]0&&f.every(Number.isInteger)?null!==(n=l("taxonomy","category",{include:f,context:"view"}))&&void 0!==n?n:void 0:null,tagRecords:o=Array.isArray(h)&&h.length>0&&h.every(Number.isInteger)?null!==(r=l("taxonomy","post_tag",{include:h,context:"view"}))&&void 0!==r?r:void 0:null,isLoading:u("getEntityRecords",["root","user",{include:[p],context:"view"}])||u("getEntityRecords",["taxonomy","category",{include:f,context:"view"}])||u("getEntityRecords",["taxonomy","post_tag",{include:h,context:"view"}]),hasResolved:(c("getEntityRecords",["root","user",{include:[p],context:"view"}])||null===i)&&(c("getEntityRecords",["taxonomy","category",{include:f,context:"view"}])||null===s)&&(c("getEntityRecords",["taxonomy","post_tag",{include:h,context:"view"}])||null===o)}}),[]);return(0,b.useEffect)((function(){var e=r.authorRecords,t=r.categoryRecords,i=r.tagRecords,s=r.isLoading;r.hasResolved&&!s&&n({authors:e,categories:t,tags:i,isReady:!0})}),[r]),t}(),c=l.authors,u=l.categories,d=l.tags,v=l.isReady;(0,b.useEffect)((function(){if(v){var e=function(e){return function(e){return!(!Array.isArray(e)||0===e.length)&&e.every((function(e){return"name"in e&&"id"in e&&"slug"in e&&"description"in e&&"link"in e}))}(e)?e.map((function(e){return e.name})):[]};a({authors:e(c),categories:e(u),tags:e(d)})}}),[c,u,d,v]);var g=(0,b.useState)(!0),x=g[0],k=g[1],S=(0,b.useState)(),j=S[0],T=S[1],L=(0,b.useState)(),E=L[0],N=L[1],C=(0,b.useState)([]),A=C[0],O=C[1],R=(0,b.useState)({type:t.RelatedPosts.FilterBy,value:t.RelatedPosts.FilterValue}),I=R[0],M=R[1],G=(0,b.useState)(void 0),H=G[0],U=G[1],q=(0,z.useDebounce)(U,1e3);(0,h.useSelect)((function(e){if("undefined"==typeof jest){var t=e("core/editor").getEditedPostContent;q(t())}else q("Jest test is running")}),[q]);var Z=function(e,r){n({RelatedPosts:xn(xn({},t.RelatedPosts),{FilterBy:e,FilterValue:r})})};return(0,b.useEffect)((function(){var e,t,n=function(e){return kn(void 0,void 0,void 0,(function(){return Sn(this,(function(t){return bn.getInstance().getRelatedPosts(r,i,I).then((function(e){O(e.posts),N(e.message),k(!1)})).catch((function(t){return kn(void 0,void 0,void 0,(function(){return Sn(this,(function(r){switch(r.label){case 0:return e>0&&t.retryFetch?[4,new Promise((function(e){return setTimeout(e,500)}))]:[3,3];case 1:return r.sent(),[4,n(e-1)];case 2:return r.sent(),[3,4];case 3:k(!1),T(t),r.label=4;case 4:return[2]}}))}))})),[2]}))}))},s=w.Author===I.type,a=w.Tag===I.type,l=w.Section===I.type,c=w.Unavailable===I.type,u=0===o.authors.length,d=0===o.tags.length,p=0===o.categories.length,f=s&&!o.authors.includes(I.value),h=a&&!o.tags.includes(I.value),v=l&&!o.categories.includes(I.value);return k(!0),c||a&&d||l&&p||s&&u?Object.values(o).every((function(e){return 0===e.length}))||M((e="",t=w.Unavailable,o.tags.length>=1?(t=w.Tag,e=o.tags[0]):o.categories.length>=1?(t=w.Section,e=o.categories[0]):o.authors.length>=1&&(t=w.Author,e=o.authors[0]),{type:t,value:e})):h?M({type:w.Tag,value:o.tags[0]}):v?M({type:w.Section,value:o.categories[0]}):f?M({type:w.Author,value:o.authors[0]}):n(1),function(){k(!1),O([]),N(""),T(void 0)}}),[r,i,I,o]),0===o.authors.length&&0===o.categories.length&&0===o.tags.length&&v?(0,p.jsx)("div",{className:"wp-parsely-related-posts",children:(0,p.jsx)("div",{className:"related-posts-body",children:(0,_.__)("Error: No author, section, or tags could be found for this post.","wp-parsely")})}):(0,p.jsxs)("div",{className:"wp-parsely-related-posts",children:[(0,p.jsx)("div",{className:"related-posts-description",children:(0,_.__)("Find top-performing related posts based on a key metric.","wp-parsely")}),(0,p.jsxs)("div",{className:"related-posts-body",children:[(0,p.jsxs)("div",{className:"related-posts-settings",children:[(0,p.jsx)(f.SelectControl,{size:"__unstable-large",onChange:function(e){var r;D(r=e,m)&&(n({RelatedPosts:xn(xn({},t.RelatedPosts),{Metric:r})}),P.trackEvent("related_posts_metric_changed",{metric:r}))},prefix:(0,p.jsx)(f.__experimentalInputControlPrefixWrapper,{children:(0,_.__)("Metric: ","wp-parsely")}),value:i,children:Object.values(m).map((function(e){return(0,p.jsx)("option",{value:e,children:V(e)},e)}))}),(0,p.jsx)(f.SelectControl,{size:"__unstable-large",value:r,prefix:(0,p.jsxs)(f.__experimentalInputControlPrefixWrapper,{children:[(0,_.__)("Period: ","wp-parsely")," "]}),onChange:function(e){return function(e){D(e,y)&&(n({RelatedPosts:xn(xn({},t.RelatedPosts),{Period:e})}),P.trackEvent("related_posts_period_changed",{period:e}))}(e)},children:Object.values(y).map((function(e){return(0,p.jsx)("option",{value:e,children:F(e)},e)}))})]}),(0,p.jsx)(ln,{label:(0,_.__)("Filter by","wp-parsely"),filter:I,onFilterTypeChange:function(e){if(D(e,w)){var t="",n=e;w.Tag===n&&(t=o.tags[0]),w.Section===n&&(t=o.categories[0]),w.Author===n&&(t=o.authors[0]),""!==t&&(Z(n,t),M({type:n,value:t}),P.trackEvent("related_posts_filter_type_changed",{filter_type:n}))}},onFilterValueChange:function(e){"string"==typeof e&&(Z(I.type,e),M(xn(xn({},I),{value:e})))},postData:o}),(0,p.jsxs)("div",{className:"related-posts-wrapper",children:[(0,p.jsx)("div",{children:(0,p.jsx)("p",{className:"related-posts-descr","data-testid":"parsely-related-posts-descr",children:w.Tag===I.type?(0,_.sprintf)(/* translators: 1: tag name, 2: period */ /* translators: 1: tag name, 2: period */ (0,_.__)("Top related posts with the “%1$s” tag in the %2$s.","wp-parsely"),I.value,F(r,!0)):w.Section===I.type?(0,_.sprintf)(/* translators: 1: section name, 2: period */ /* translators: 1: section name, 2: period */ (0,_.__)("Top related posts in the “%1$s” section in the %2$s.","wp-parsely"),I.value,F(r,!0)):w.Author===I.type?(0,_.sprintf)(/* translators: 1: author name, 2: period */ /* translators: 1: author name, 2: period */ -(0,_.__)("Top related posts by %1$s in the %2$s.","wp-parsely"),I.value,F(r,!0)):null!=E?E:""})}),j&&j.Message(),x&&(0,p.jsx)("div",{className:"related-posts-loading-message","data-testid":"parsely-related-posts-loading-message",children:(0,_.__)("Loading…","wp-parsely")}),!x&&!j&&0===A.length&&(0,p.jsx)("div",{className:"related-posts-empty","data-testid":"parsely-related-posts-empty",children:(0,_.__)("No related posts found.","wp-parsely")}),!x&&A.length>0&&(0,p.jsx)("div",{className:"related-posts-list",children:A.map((function(e){return(0,p.jsx)(hn,{metric:i,post:e,postContent:H},e.id)}))})]})]})]})},jn=(0,p.jsx)(x.SVG,{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 24 24",children:(0,p.jsx)(x.Path,{d:"m19 7-3-3-8.5 8.5-1 4 4-1L19 7Zm-7 11.5H5V20h7v-1.5Z"})}),Tn=function(){return(0,p.jsx)(f.SVG,{xmlns:"http://www.w3.org/2000/svg",width:"18",height:"18",viewBox:"0 0 18 18",fill:"none",children:(0,p.jsx)(f.Path,{fillRule:"evenodd",clipRule:"evenodd",d:"M13.5034 7.91642L9 12.0104L4.49662 7.91642L5.25337 7.08398L8.99999 10.49L12.7466 7.08398L13.5034 7.91642Z",fill:"#1E1E1E"})})},Ln={journalist:{label:(0,_.__)("Journalist","wp-parsely")},editorialWriter:{label:(0,_.__)("Editorial Writer","wp-parsely")},investigativeReporter:{label:(0,_.__)("Investigative Reporter","wp-parsely")},techAnalyst:{label:(0,_.__)("Tech Analyst","wp-parsely")},businessAnalyst:{label:(0,_.__)("Business Analyst","wp-parsely")},culturalCommentator:{label:(0,_.__)("Cultural Commentator","wp-parsely")},scienceCorrespondent:{label:(0,_.__)("Science Correspondent","wp-parsely")},politicalAnalyst:{label:(0,_.__)("Political Analyst","wp-parsely")},healthWellnessAdvocate:{label:(0,_.__)("Health and Wellness Advocate","wp-parsely")},environmentalJournalist:{label:(0,_.__)("Environmental Journalist","wp-parsely")},custom:{label:(0,_.__)("Custom Persona","wp-parsely"),icon:jn}},En=Object.keys(Ln),Nn=function(e){return"custom"===e||""===e?Ln.custom.label:Cn(e)?e:Ln[e].label},Cn=function(e){return!En.includes(e)||"custom"===e},An=function(e){var t=e.value,n=e.onChange,r=(0,b.useState)(""),i=r[0],s=r[1],o=(0,z.useDebounce)(n,500);return(0,p.jsx)("div",{className:"parsely-persona-selector-custom",children:(0,p.jsx)(f.TextControl,{value:i||t,placeholder:(0,_.__)("Enter a custom persona…","wp-parsely"),onChange:function(e){if(""===e)return n(""),void s("");e.length>32&&(e=e.slice(0,32)),o(e),s(e)}})})},On=function(e){var t=e.persona,n=e.value,r=void 0===n?(0,_.__)("Select a persona…","wp-parsely"):n,i=e.label,s=void 0===i?(0,_.__)("Persona","wp-parsely"):i,o=e.onChange,a=e.onDropdownChange,l=e.disabled,c=void 0!==l&&l,u=e.allowCustom,d=void 0!==u&&u;return(0,p.jsxs)(f.Disabled,{isDisabled:c,children:[s&&(0,p.jsx)("div",{className:"wp-parsely-dropdown-label",children:s}),(0,p.jsx)(f.DropdownMenu,{label:(0,_.__)("Persona","wp-parsely"),className:"parsely-persona-selector-dropdown"+(c?" is-disabled":""),popoverProps:{className:"wp-parsely-popover"},toggleProps:{children:(0,p.jsxs)(p.Fragment,{children:[(0,p.jsx)("div",{className:"parsely-persona-selector-label",children:Cn(t)?Ln.custom.label:r}),(0,p.jsx)(Tn,{})]})},children:function(e){var n=e.onClose;return(0,p.jsx)(f.MenuGroup,{label:(0,_.__)("Persona","wp-parsely"),children:(0,p.jsx)(p.Fragment,{children:En.map((function(e){if(!d&&"custom"===e)return null;var r=Ln[e],i=e===t||Cn(t)&&"custom"===e;return(0,p.jsxs)(f.MenuItem,{isSelected:i,className:i?"is-selected":"",role:"menuitemradio",onClick:function(){null==a||a(e),o(e),n(),"custom"===e&&setTimeout((function(){var e=document.querySelector(".parsely-persona-selector-custom input");e&&e.focus()}),0)},children:[r.icon&&(0,p.jsx)(X,{icon:r.icon}),r.label]},e)}))})})}}),d&&Cn(t)&&(0,p.jsx)(An,{onChange:function(e){o(""!==e?e:"custom")},value:"custom"===t?"":t})]})},Rn={neutral:{label:(0,_.__)("Neutral","wp-parsely")},formal:{label:(0,_.__)("Formal","wp-parsely")},humorous:{label:(0,_.__)("Humorous","wp-parsely")},confident:{label:(0,_.__)("Confident","wp-parsely")},provocative:{label:(0,_.__)("Provocative","wp-parsely")},serious:{label:(0,_.__)("Serious","wp-parsely")},inspirational:{label:(0,_.__)("Inspirational","wp-parsely")},skeptical:{label:(0,_.__)("Skeptical","wp-parsely")},conversational:{label:(0,_.__)("Conversational","wp-parsely")},analytical:{label:(0,_.__)("Analytical","wp-parsely")},custom:{label:(0,_.__)("Custom Tone","wp-parsely"),icon:jn}},In=Object.keys(Rn),Bn=function(e){return"custom"===e||""===e?Rn.custom.label:Mn(e)?e:Rn[e].label},Mn=function(e){return!In.includes(e)||"custom"===e},Dn=function(e){var t=e.value,n=e.onChange,r=(0,b.useState)(""),i=r[0],s=r[1],o=(0,z.useDebounce)(n,500);return(0,p.jsx)("div",{className:"parsely-tone-selector-custom",children:(0,p.jsx)(f.TextControl,{value:i||t,placeholder:(0,_.__)("Enter a custom tone","wp-parsely"),onChange:function(e){if(""===e)return n(""),void s("");e.length>32&&(e=e.slice(0,32)),o(e),s(e)}})})},Fn=function(e){var t=e.tone,n=e.value,r=void 0===n?(0,_.__)("Select a tone","wp-parsely"):n,i=e.label,s=void 0===i?(0,_.__)("Tone","wp-parsely"):i,o=e.onChange,a=e.onDropdownChange,l=e.disabled,c=void 0!==l&&l,u=e.allowCustom,d=void 0!==u&&u;return(0,p.jsxs)(f.Disabled,{isDisabled:c,children:[(0,p.jsx)("div",{className:"wp-parsely-dropdown-label",children:s}),(0,p.jsx)(f.DropdownMenu,{label:(0,_.__)("Tone","wp-parsely"),className:"parsely-tone-selector-dropdown"+(c?" is-disabled":""),popoverProps:{className:"wp-parsely-popover"},toggleProps:{children:(0,p.jsxs)(p.Fragment,{children:[(0,p.jsx)("div",{className:"parsely-tone-selector-label",children:Mn(t)?Rn.custom.label:r}),(0,p.jsx)(Tn,{})]})},children:function(e){var n=e.onClose;return(0,p.jsx)(f.MenuGroup,{label:(0,_.__)("Select a tone","wp-parsely"),children:(0,p.jsx)(p.Fragment,{children:In.map((function(e){if(!d&&"custom"===e)return null;var r=Rn[e],i=e===t||Mn(t)&&"custom"===e;return(0,p.jsxs)(f.MenuItem,{isSelected:i,className:i?"is-selected":"",role:"menuitemradio",onClick:function(){null==a||a(e),o(e),n(),"custom"===e&&setTimeout((function(){var e=document.querySelector(".parsely-tone-selector-custom input");e&&e.focus()}),0)},children:[r.icon&&(0,p.jsx)(X,{icon:r.icon}),r.label]},e)}))})})}}),d&&Mn(t)&&(0,p.jsx)(Dn,{onChange:function(e){o(""!==e?e:"custom")},value:"custom"===t?"":t})]})},Vn=(0,p.jsx)(x.SVG,{width:"24",height:"24",viewBox:"0 0 24 24",xmlns:"http://www.w3.org/2000/svg",children:(0,p.jsx)(x.Path,{d:"M10.97 10.159a3.382 3.382 0 0 0-2.857.955l1.724 1.723-2.836 2.913L7 17h1.25l2.913-2.837 1.723 1.723a3.38 3.38 0 0 0 .606-.825c.33-.63.446-1.343.35-2.032L17 10.695 13.305 7l-2.334 3.159Z"})}),Gn=(0,p.jsx)(x.SVG,{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 24 24",children:(0,p.jsx)(x.Path,{d:"M18.3 11.7c-.6-.6-1.4-.9-2.3-.9H6.7l2.9-3.3-1.1-1-4.5 5L8.5 16l1-1-2.7-2.7H16c.5 0 .9.2 1.3.5 1 1 1 3.4 1 4.5v.3h1.5v-.2c0-1.5 0-4.3-1.5-5.7z"})}),Hn=(0,p.jsx)(x.SVG,{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 24 24",children:(0,p.jsx)(x.Path,{fillRule:"evenodd",clipRule:"evenodd",d:"M12 5.5A2.25 2.25 0 0 0 9.878 7h4.244A2.251 2.251 0 0 0 12 5.5ZM12 4a3.751 3.751 0 0 0-3.675 3H5v1.5h1.27l.818 8.997a2.75 2.75 0 0 0 2.739 2.501h4.347a2.75 2.75 0 0 0 2.738-2.5L17.73 8.5H19V7h-3.325A3.751 3.751 0 0 0 12 4Zm4.224 4.5H7.776l.806 8.861a1.25 1.25 0 0 0 1.245 1.137h4.347a1.25 1.25 0 0 0 1.245-1.137l.805-8.861Z"})}),zn=(0,p.jsx)(x.SVG,{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 24 24",children:(0,p.jsx)(x.Path,{d:"m21.5 9.1-6.6-6.6-4.2 5.6c-1.2-.1-2.4.1-3.6.7-.1 0-.1.1-.2.1-.5.3-.9.6-1.2.9l3.7 3.7-5.7 5.7v1.1h1.1l5.7-5.7 3.7 3.7c.4-.4.7-.8.9-1.2.1-.1.1-.2.2-.3.6-1.1.8-2.4.6-3.6l5.6-4.1zm-7.3 3.5.1.9c.1.9 0 1.8-.4 2.6l-6-6c.8-.4 1.7-.5 2.6-.4l.9.1L15 4.9 19.1 9l-4.9 3.6z"})}),Un=function(){return Un=Object.assign||function(e){for(var t,n=1,r=arguments.length;n0&&i[i.length-1])||6!==a[0]&&2!==a[0])){o=0;continue}if(3===a[0]&&(!i||a[1]>i[0]&&a[1]0&&i[i.length-1])||6!==a[0]&&2!==a[0])){o=0;continue}if(3===a[0]&&(!i||a[1]>i[0]&&a[1]0&&i[i.length-1])||6!==a[0]&&2!==a[0])){o=0;continue}if(3===a[0]&&(!i||a[1]>i[0]&&a[1]0?(0,p.jsx)("span",{className:"parsely-write-titles-text",children:(0,b.createInterpolateElement)( +(0,_.__)("Top related posts by %1$s in the %2$s.","wp-parsely"),I.value,F(r,!0)):null!=E?E:""})}),j&&j.Message(),x&&(0,p.jsx)("div",{className:"related-posts-loading-message","data-testid":"parsely-related-posts-loading-message",children:(0,_.__)("Loading…","wp-parsely")}),!x&&!j&&0===A.length&&(0,p.jsx)("div",{className:"related-posts-empty","data-testid":"parsely-related-posts-empty",children:(0,_.__)("No related posts found.","wp-parsely")}),!x&&A.length>0&&(0,p.jsx)("div",{className:"related-posts-list",children:A.map((function(e){return(0,p.jsx)(hn,{metric:i,post:e,postContent:H},e.id)}))})]})]})]})},jn=(0,p.jsx)(x.SVG,{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 24 24",children:(0,p.jsx)(x.Path,{d:"m19 7-3-3-8.5 8.5-1 4 4-1L19 7Zm-7 11.5H5V20h7v-1.5Z"})}),Tn=function(){return(0,p.jsx)(f.SVG,{xmlns:"http://www.w3.org/2000/svg",width:"18",height:"18",viewBox:"0 0 18 18",fill:"none",children:(0,p.jsx)(f.Path,{fillRule:"evenodd",clipRule:"evenodd",d:"M13.5034 7.91642L9 12.0104L4.49662 7.91642L5.25337 7.08398L8.99999 10.49L12.7466 7.08398L13.5034 7.91642Z",fill:"#1E1E1E"})})},Ln={journalist:{label:(0,_.__)("Journalist","wp-parsely")},editorialWriter:{label:(0,_.__)("Editorial Writer","wp-parsely")},investigativeReporter:{label:(0,_.__)("Investigative Reporter","wp-parsely")},techAnalyst:{label:(0,_.__)("Tech Analyst","wp-parsely")},businessAnalyst:{label:(0,_.__)("Business Analyst","wp-parsely")},culturalCommentator:{label:(0,_.__)("Cultural Commentator","wp-parsely")},scienceCorrespondent:{label:(0,_.__)("Science Correspondent","wp-parsely")},politicalAnalyst:{label:(0,_.__)("Political Analyst","wp-parsely")},healthWellnessAdvocate:{label:(0,_.__)("Health and Wellness Advocate","wp-parsely")},environmentalJournalist:{label:(0,_.__)("Environmental Journalist","wp-parsely")},custom:{label:(0,_.__)("Custom Persona","wp-parsely"),icon:jn}},En=Object.keys(Ln),Nn=function(e){return"custom"===e||""===e?Ln.custom.label:Cn(e)?e:Ln[e].label},Cn=function(e){return!En.includes(e)||"custom"===e},An=function(e){var t=e.value,n=e.onChange,r=(0,b.useState)(""),i=r[0],s=r[1],o=(0,z.useDebounce)(n,500);return(0,p.jsx)("div",{className:"parsely-persona-selector-custom",children:(0,p.jsx)(f.TextControl,{value:i||t,placeholder:(0,_.__)("Enter a custom persona…","wp-parsely"),onChange:function(e){if(""===e)return n(""),void s("");e.length>32&&(e=e.slice(0,32)),o(e),s(e)}})})},On=function(e){var t=e.persona,n=e.value,r=void 0===n?(0,_.__)("Select a persona…","wp-parsely"):n,i=e.label,s=void 0===i?(0,_.__)("Persona","wp-parsely"):i,o=e.onChange,a=e.onDropdownChange,l=e.disabled,c=void 0!==l&&l,u=e.allowCustom,d=void 0!==u&&u;return(0,p.jsxs)(f.Disabled,{isDisabled:c,children:[s&&(0,p.jsx)("div",{className:"wp-parsely-dropdown-label",children:s}),(0,p.jsx)(f.DropdownMenu,{label:(0,_.__)("Persona","wp-parsely"),className:"parsely-persona-selector-dropdown"+(c?" is-disabled":""),popoverProps:{className:"wp-parsely-popover"},toggleProps:{children:(0,p.jsxs)(p.Fragment,{children:[(0,p.jsx)("div",{className:"parsely-persona-selector-label",children:Cn(t)?Ln.custom.label:r}),(0,p.jsx)(Tn,{})]})},children:function(e){var n=e.onClose;return(0,p.jsx)(f.MenuGroup,{label:(0,_.__)("Persona","wp-parsely"),children:(0,p.jsx)(p.Fragment,{children:En.map((function(e){if(!d&&"custom"===e)return null;var r=Ln[e],i=e===t||Cn(t)&&"custom"===e;return(0,p.jsxs)(f.MenuItem,{isSelected:i,className:i?"is-selected":"",role:"menuitemradio",onClick:function(){null==a||a(e),o(e),n(),"custom"===e&&setTimeout((function(){var e=document.querySelector(".parsely-persona-selector-custom input");e&&e.focus()}),0)},children:[r.icon&&(0,p.jsx)(X,{icon:r.icon}),r.label]},e)}))})})}}),d&&Cn(t)&&(0,p.jsx)(An,{onChange:function(e){o(""!==e?e:"custom")},value:"custom"===t?"":t})]})},Rn={neutral:{label:(0,_.__)("Neutral","wp-parsely")},formal:{label:(0,_.__)("Formal","wp-parsely")},humorous:{label:(0,_.__)("Humorous","wp-parsely")},confident:{label:(0,_.__)("Confident","wp-parsely")},provocative:{label:(0,_.__)("Provocative","wp-parsely")},serious:{label:(0,_.__)("Serious","wp-parsely")},inspirational:{label:(0,_.__)("Inspirational","wp-parsely")},skeptical:{label:(0,_.__)("Skeptical","wp-parsely")},conversational:{label:(0,_.__)("Conversational","wp-parsely")},analytical:{label:(0,_.__)("Analytical","wp-parsely")},custom:{label:(0,_.__)("Custom Tone","wp-parsely"),icon:jn}},In=Object.keys(Rn),Bn=function(e){return"custom"===e||""===e?Rn.custom.label:Mn(e)?e:Rn[e].label},Mn=function(e){return!In.includes(e)||"custom"===e},Dn=function(e){var t=e.value,n=e.onChange,r=(0,b.useState)(""),i=r[0],s=r[1],o=(0,z.useDebounce)(n,500);return(0,p.jsx)("div",{className:"parsely-tone-selector-custom",children:(0,p.jsx)(f.TextControl,{value:i||t,placeholder:(0,_.__)("Enter a custom tone","wp-parsely"),onChange:function(e){if(""===e)return n(""),void s("");e.length>32&&(e=e.slice(0,32)),o(e),s(e)}})})},Fn=function(e){var t=e.tone,n=e.value,r=void 0===n?(0,_.__)("Select a tone","wp-parsely"):n,i=e.label,s=void 0===i?(0,_.__)("Tone","wp-parsely"):i,o=e.onChange,a=e.onDropdownChange,l=e.disabled,c=void 0!==l&&l,u=e.allowCustom,d=void 0!==u&&u;return(0,p.jsxs)(f.Disabled,{isDisabled:c,children:[(0,p.jsx)("div",{className:"wp-parsely-dropdown-label",children:s}),(0,p.jsx)(f.DropdownMenu,{label:(0,_.__)("Tone","wp-parsely"),className:"parsely-tone-selector-dropdown"+(c?" is-disabled":""),popoverProps:{className:"wp-parsely-popover"},toggleProps:{children:(0,p.jsxs)(p.Fragment,{children:[(0,p.jsx)("div",{className:"parsely-tone-selector-label",children:Mn(t)?Rn.custom.label:r}),(0,p.jsx)(Tn,{})]})},children:function(e){var n=e.onClose;return(0,p.jsx)(f.MenuGroup,{label:(0,_.__)("Select a tone","wp-parsely"),children:(0,p.jsx)(p.Fragment,{children:In.map((function(e){if(!d&&"custom"===e)return null;var r=Rn[e],i=e===t||Mn(t)&&"custom"===e;return(0,p.jsxs)(f.MenuItem,{isSelected:i,className:i?"is-selected":"",role:"menuitemradio",onClick:function(){null==a||a(e),o(e),n(),"custom"===e&&setTimeout((function(){var e=document.querySelector(".parsely-tone-selector-custom input");e&&e.focus()}),0)},children:[r.icon&&(0,p.jsx)(X,{icon:r.icon}),r.label]},e)}))})})}}),d&&Mn(t)&&(0,p.jsx)(Dn,{onChange:function(e){o(""!==e?e:"custom")},value:"custom"===t?"":t})]})},Vn=(0,p.jsx)(x.SVG,{width:"24",height:"24",viewBox:"0 0 24 24",xmlns:"http://www.w3.org/2000/svg",children:(0,p.jsx)(x.Path,{d:"M10.97 10.159a3.382 3.382 0 0 0-2.857.955l1.724 1.723-2.836 2.913L7 17h1.25l2.913-2.837 1.723 1.723a3.38 3.38 0 0 0 .606-.825c.33-.63.446-1.343.35-2.032L17 10.695 13.305 7l-2.334 3.159Z"})}),Gn=(0,p.jsx)(x.SVG,{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 24 24",children:(0,p.jsx)(x.Path,{d:"M18.3 11.7c-.6-.6-1.4-.9-2.3-.9H6.7l2.9-3.3-1.1-1-4.5 5L8.5 16l1-1-2.7-2.7H16c.5 0 .9.2 1.3.5 1 1 1 3.4 1 4.5v.3h1.5v-.2c0-1.5 0-4.3-1.5-5.7z"})}),Hn=(0,p.jsx)(x.SVG,{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 24 24",children:(0,p.jsx)(x.Path,{fillRule:"evenodd",clipRule:"evenodd",d:"M12 5.5A2.25 2.25 0 0 0 9.878 7h4.244A2.251 2.251 0 0 0 12 5.5ZM12 4a3.751 3.751 0 0 0-3.675 3H5v1.5h1.27l.818 8.997a2.75 2.75 0 0 0 2.739 2.501h4.347a2.75 2.75 0 0 0 2.738-2.5L17.73 8.5H19V7h-3.325A3.751 3.751 0 0 0 12 4Zm4.224 4.5H7.776l.806 8.861a1.25 1.25 0 0 0 1.245 1.137h4.347a1.25 1.25 0 0 0 1.245-1.137l.805-8.861Z"})}),zn=(0,p.jsx)(x.SVG,{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 24 24",children:(0,p.jsx)(x.Path,{d:"m21.5 9.1-6.6-6.6-4.2 5.6c-1.2-.1-2.4.1-3.6.7-.1 0-.1.1-.2.1-.5.3-.9.6-1.2.9l3.7 3.7-5.7 5.7v1.1h1.1l5.7-5.7 3.7 3.7c.4-.4.7-.8.9-1.2.1-.1.1-.2.2-.3.6-1.1.8-2.4.6-3.6l5.6-4.1zm-7.3 3.5.1.9c.1.9 0 1.8-.4 2.6l-6-6c.8-.4 1.7-.5 2.6-.4l.9.1L15 4.9 19.1 9l-4.9 3.6z"})}),Un=function(){return Un=Object.assign||function(e){for(var t,n=1,r=arguments.length;n0&&i[i.length-1])||6!==a[0]&&2!==a[0])){o=0;continue}if(3===a[0]&&(!i||a[1]>i[0]&&a[1]0&&i[i.length-1])||6!==a[0]&&2!==a[0])){o=0;continue}if(3===a[0]&&(!i||a[1]>i[0]&&a[1]0&&i[i.length-1])||6!==a[0]&&2!==a[0])){o=0;continue}if(3===a[0]&&(!i||a[1]>i[0]&&a[1]0?(0,p.jsx)("span",{className:"parsely-write-titles-text",children:(0,b.createInterpolateElement)( // translators: %1$s is the tone, %2$s is the persona. // translators: %1$s is the tone, %2$s is the persona. (0,_.__)("We've generated a few titles based on the content of your post, written as a .","wp-parsely"),{tone:(0,p.jsx)("strong",{children:Bn(a)}),persona:(0,p.jsx)("strong",{children:Nn(u)})})}):(0,_.__)("Use Parse.ly AI to generate a title for your post.","wp-parsely"),(0,p.jsxs)(f.Button,{href:"https://docs.parse.ly/plugin-content-helper/#h-title-suggestions-beta",target:"_blank",variant:"link",children:[(0,_.__)("Learn more about Parse.ly AI","wp-parsely"),(0,p.jsx)(X,{icon:ee,size:18,className:"parsely-external-link-icon"})]})]}),i&&(0,p.jsx)(f.Notice,{className:"wp-parsely-content-helper-error",onRemove:function(){return s(void 0)},status:"info",children:i.Message()}),void 0!==k&&(0,p.jsx)(Jn,{title:k,type:fn.PostTitle,isOriginal:!0}),00&&(0,p.jsx)(Qn,{pinnedTitles:m,isOpen:!0}),y.length>0&&(0,p.jsx)(er,{suggestions:y,isOpen:!0,isLoading:g})]}),(0,p.jsx)(Xn,{isLoading:g,onPersonaChange:function(e){C("Persona",e),d(e)},onSettingChange:C,onToneChange:function(e){C("Tone",e),l(e)},persona:t.TitleSuggestions.Persona,tone:t.TitleSuggestions.Tone}),(0,p.jsx)("div",{className:"title-suggestions-generate",children:(0,p.jsxs)(f.Button,{variant:"primary",isBusy:g,disabled:g||"custom"===a||"custom"===u,onClick:function(){return ir(void 0,void 0,void 0,(function(){return sr(this,(function(e){switch(e.label){case 0:return s(void 0),!1!==g?[3,2]:(P.trackEvent("title_suggestions_generate_pressed",{request_more:y.length>0,total_titles:y.length,total_pinned:y.filter((function(e){return e.isPinned})).length,tone:a,persona:u}),[4,(t=fn.PostTitle,n=A,r=a,i=u,ir(void 0,void 0,void 0,(function(){var e,o,a;return sr(this,(function(l){switch(l.label){case 0:return[4,T(!0)];case 1:l.sent(),e=nr.getInstance(),l.label=2;case 2:return l.trys.push([2,5,,6]),[4,e.generateTitles(n,3,r,i)];case 3:return o=l.sent(),[4,j(t,o)];case 4:return l.sent(),[3,6];case 5:return a=l.sent(),s(a),j(t,[]),[3,6];case 6:return[4,T(!1)];case 7:return l.sent(),[2]}}))})))]);case 1:e.sent(),e.label=2;case 2:return[2]}var t,n,r,i}))}))},children:[g&&(0,_.__)("Generating Titles…","wp-parsely"),!g&&w.length>0&&(0,_.__)("Generate More","wp-parsely"),!g&&0===w.length&&(0,_.__)("Generate Titles","wp-parsely")]})})]})})},ar=function(){return ar=Object.assign||function(e){for(var t,n=1,r=arguments.length;n array('react', 'wp-api-fetch', 'wp-components', 'wp-data', 'wp-editor', 'wp-element', 'wp-hooks', 'wp-i18n', 'wp-plugins', 'wp-primitives', 'wp-url', 'wp-wordcount'), 'version' => '577d3025e0a7c2398780'); + array('react', 'wp-api-fetch', 'wp-components', 'wp-data', 'wp-editor', 'wp-element', 'wp-hooks', 'wp-i18n', 'wp-plugins', 'wp-primitives', 'wp-url', 'wp-wordcount'), 'version' => 'c02551fe09a73f993b6f'); diff --git a/build/content-helper/excerpt-generator.js b/build/content-helper/excerpt-generator.js index 82cb2f791a..26c6f9ac79 100644 --- a/build/content-helper/excerpt-generator.js +++ b/build/content-helper/excerpt-generator.js @@ -1,4 +1,4 @@ -!function(){"use strict";var e={20:function(e,t,r){var n=r(609),o=Symbol.for("react.element"),a=Symbol.for("react.fragment"),i=Object.prototype.hasOwnProperty,s=n.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED.ReactCurrentOwner,l={key:!0,ref:!0,__self:!0,__source:!0};function c(e,t,r){var n,a={},c=null,u=null;for(n in void 0!==r&&(c=""+r),void 0!==t.key&&(c=""+t.key),void 0!==t.ref&&(u=t.ref),t)i.call(t,n)&&!l.hasOwnProperty(n)&&(a[n]=t[n]);if(e&&e.defaultProps)for(n in t=e.defaultProps)void 0===a[n]&&(a[n]=t[n]);return{$$typeof:o,type:e,key:c,ref:u,props:a,_owner:s.current}}t.Fragment=a,t.jsx=c,t.jsxs=c},848:function(e,t,r){e.exports=r(20)},609:function(e){e.exports=window.React}},t={};function r(n){var o=t[n];if(void 0!==o)return o.exports;var a=t[n]={exports:{}};return e[n](a,a.exports,r),a.exports}r.n=function(e){var t=e&&e.__esModule?function(){return e.default}:function(){return e};return r.d(t,{a:t}),t},r.d=function(e,t){for(var n in t)r.o(t,n)&&!r.o(e,n)&&Object.defineProperty(e,n,{enumerable:!0,get:t[n]})},r.o=function(e,t){return Object.prototype.hasOwnProperty.call(e,t)},function(){var e,t,n,o,a,i,s,l,c,u,p,d=window.wp.data,y=window.wp.hooks,h=window.wp.plugins,f=((0,d.dispatch)("core/block-editor"),(0,d.dispatch)("core/editor"),(0,d.dispatch)("core/edit-post")),w=r(848),v=window.wp.components,g=window.wp.editor;void 0!==window.wp&&(p=null!==(t=null===(e=window.wp.editor)||void 0===e?void 0:e.PluginDocumentSettingPanel)&&void 0!==t?t:null!==(o=null===(n=window.wp.editPost)||void 0===n?void 0:n.PluginDocumentSettingPanel)&&void 0!==o?o:null===(a=window.wp.editSite)||void 0===a?void 0:a.PluginDocumentSettingPanel,null!==(s=null===(i=window.wp.editor)||void 0===i?void 0:i.PluginSidebar)&&void 0!==s||null!==(c=null===(l=window.wp.editPost)||void 0===l?void 0:l.PluginSidebar)&&void 0!==c||null===(u=window.wp.editSite)||void 0===u||u.PluginSidebar);var _,b,P=window.wp.element,m=window.wp.i18n,x=window.wp.wordcount,E=window.wp.primitives,A=(0,w.jsx)(E.SVG,{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 24 24",children:(0,w.jsx)(E.Path,{d:"M19.5 4.5h-7V6h4.44l-5.97 5.97 1.06 1.06L18 7.06v4.44h1.5v-7Zm-13 1a2 2 0 0 0-2 2v10a2 2 0 0 0 2 2h10a2 2 0 0 0 2-2v-3H17v3a.5.5 0 0 1-.5.5h-10a.5.5 0 0 1-.5-.5v-10a.5.5 0 0 1 .5-.5h3V5.5h-3Z"})}),S=function(){function e(){this._tkq=[],this.isLoaded=!1,this.isEnabled=!1,"undefined"!=typeof wpParselyTracksTelemetry&&(this.isEnabled=!0,this.loadTrackingLibrary())}return e.getInstance=function(){return window.wpParselyTelemetryInstance||Object.defineProperty(window,"wpParselyTelemetryInstance",{value:new e,writable:!1,configurable:!1,enumerable:!1}),window.wpParselyTelemetryInstance},e.prototype.loadTrackingLibrary=function(){var e=this,t=document.createElement("script");t.async=!0,t.src="//stats.wp.com/w.js",t.onload=function(){e.isLoaded=!0,e._tkq=window._tkq||[]},document.head.appendChild(t)},e.trackEvent=function(t){return r=this,n=arguments,a=function(t,r){var n;return void 0===r&&(r={}),function(e,t){var r,n,o,a,i={label:0,sent:function(){if(1&o[0])throw o[1];return o[1]},trys:[],ops:[]};return a={next:s(0),throw:s(1),return:s(2)},"function"==typeof Symbol&&(a[Symbol.iterator]=function(){return this}),a;function s(s){return function(l){return function(s){if(r)throw new TypeError("Generator is already executing.");for(;a&&(a=0,s[0]&&(i=0)),i;)try{if(r=1,n&&(o=2&s[0]?n.return:s[0]?n.throw||((o=n.return)&&o.call(n),0):n.next)&&!(o=o.call(n,s[1])).done)return o;switch(n=0,o&&(s=[2&s[0],o.value]),s[0]){case 0:case 1:o=s;break;case 4:return i.label++,{value:s[1],done:!1};case 5:i.label++,n=s[1],s=[0];continue;case 7:s=i.ops.pop(),i.trys.pop();continue;default:if(!((o=(o=i.trys).length>0&&o[o.length-1])||6!==s[0]&&2!==s[0])){i=0;continue}if(3===s[0]&&(!o||s[1]>o[0]&&s[1]=1e4&&(clearInterval(a),r("Telemetry library not loaded"))}),100);else r("Telemetry not enabled")}))},e.prototype.trackEvent=function(t,r){var n;this.isLoaded?(0!==t.indexOf(e.TRACKS_PREFIX)&&(t=e.TRACKS_PREFIX+t),this.isEventNameValid(t)?(r=this.prepareProperties(r),null===(n=this._tkq)||void 0===n||n.push(["recordEvent",t,r])):console.error("Error tracking event: Invalid event name")):console.error("Error tracking event: Telemetry not loaded")},e.prototype.isTelemetryEnabled=function(){return this.isEnabled},e.prototype.isProprietyValid=function(t){return e.PROPERTY_REGEX.test(t)},e.prototype.isEventNameValid=function(t){return e.EVENT_NAME_REGEX.test(t)},e.prototype.prepareProperties=function(e){return(e=this.sanitizeProperties(e)).parsely_version=wpParselyTracksTelemetry.version,wpParselyTracksTelemetry.user&&(e._ut=wpParselyTracksTelemetry.user.type,e._ui=wpParselyTracksTelemetry.user.id),wpParselyTracksTelemetry.vipgo_env&&(e.vipgo_env=wpParselyTracksTelemetry.vipgo_env),this.sanitizeProperties(e)},e.prototype.sanitizeProperties=function(e){var t=this,r={};return Object.keys(e).forEach((function(n){t.isProprietyValid(n)&&(r[n]=e[n])})),r},e.TRACKS_PREFIX="wpparsely_",e.EVENT_NAME_REGEX=/^(([a-z0-9]+)_){2}([a-z0-9_]+)$/,e.PROPERTY_REGEX=/^[a-z_][a-z0-9_]*$/,e}(),k=(S.trackEvent,function(e){void 0===e&&(e=null);var t="";(null==e?void 0:e.children)&&(t=e.children);var r="content-helper-error-message";return(null==e?void 0:e.className)&&(r+=" "+e.className),(0,w.jsx)("div",{className:r,"data-testid":null==e?void 0:e.testId,dangerouslySetInnerHTML:{__html:t}})}),T=(_=function(e,t){return _=Object.setPrototypeOf||{__proto__:[]}instanceof Array&&function(e,t){e.__proto__=t}||function(e,t){for(var r in t)Object.prototype.hasOwnProperty.call(t,r)&&(e[r]=t[r])},_(e,t)},function(e,t){if("function"!=typeof t&&null!==t)throw new TypeError("Class extends value "+String(t)+" is not a constructor or null");function __(){this.constructor=e}_(e,t),e.prototype=null===t?Object.create(t):(__.prototype=t.prototype,new __)});!function(e){e.AccessToFeatureDisabled="ch_access_to_feature_disabled",e.CannotFormulateApiQuery="ch_cannot_formulate_api_query",e.FetchError="fetch_error",e.HttpRequestFailed="http_request_failed",e.ParselyAborted="ch_parsely_aborted",e[e.ParselyApiForbidden=403]="ParselyApiForbidden",e.ParselyApiResponseContainsError="ch_response_contains_error",e.ParselyApiReturnedNoData="ch_parsely_api_returned_no_data",e.ParselyApiReturnedTooManyResults="ch_parsely_api_returned_too_many_results",e.PluginCredentialsNotSetMessageDetected="parsely_credentials_not_set_message_detected",e.PluginSettingsApiSecretNotSet="parsely_api_secret_not_set",e.PluginSettingsSiteIdNotSet="parsely_site_id_not_set",e.PostIsNotPublished="ch_post_not_published",e.UnknownError="ch_unknown_error",e.ParselySuggestionsApiAuthUnavailable="AUTH_UNAVAILABLE",e.ParselySuggestionsApiNoAuthentication="NO_AUTHENTICATION",e.ParselySuggestionsApiNoAuthorization="NO_AUTHORIZATION",e.ParselySuggestionsApiNoData="NO_DATA",e.ParselySuggestionsApiOpenAiError="OPENAI_ERROR",e.ParselySuggestionsApiOpenAiSchema="OPENAI_SCHEMA",e.ParselySuggestionsApiOpenAiUnavailable="OPENAI_UNAVAILABLE",e.ParselySuggestionsApiSchemaError="SCHEMA_ERROR"}(b||(b={}));var N=function(e){function t(r,n,o){void 0===o&&(o=(0,m.__)("Error: ","wp-parsely"));var a=this;r.startsWith(o)&&(o=""),(a=e.call(this,o+r)||this).hint=null,a.name=a.constructor.name,a.code=n;var i=[b.AccessToFeatureDisabled,b.ParselyApiForbidden,b.ParselyApiResponseContainsError,b.ParselyApiReturnedNoData,b.ParselyApiReturnedTooManyResults,b.PluginCredentialsNotSetMessageDetected,b.PluginSettingsApiSecretNotSet,b.PluginSettingsSiteIdNotSet,b.PostIsNotPublished,b.UnknownError,b.ParselySuggestionsApiAuthUnavailable,b.ParselySuggestionsApiNoAuthentication,b.ParselySuggestionsApiNoAuthorization,b.ParselySuggestionsApiNoData,b.ParselySuggestionsApiSchemaError];return a.retryFetch=!i.includes(a.code),Object.setPrototypeOf(a,t.prototype),a.code===b.AccessToFeatureDisabled?a.message=(0,m.__)("Access to this feature is disabled by the site's administration.","wp-parsely"):a.code===b.ParselySuggestionsApiNoAuthorization?a.message=(0,m.__)('This AI-powered feature is opt-in. To gain access, please submit a request here.',"wp-parsely"):a.code===b.ParselySuggestionsApiOpenAiError||a.code===b.ParselySuggestionsApiOpenAiUnavailable?a.message=(0,m.__)("The Parse.ly API returned an internal server error. Please retry with a different input, or try again later.","wp-parsely"):a.code===b.HttpRequestFailed&&a.message.includes("cURL error 28")?a.message=(0,m.__)("The Parse.ly API did not respond in a timely manner. Please try again later.","wp-parsely"):a.code===b.ParselySuggestionsApiSchemaError?a.message=(0,m.__)("The Parse.ly API returned a validation error. Please try again with different parameters.","wp-parsely"):a.code===b.ParselySuggestionsApiNoData?a.message=(0,m.__)("The Parse.ly API couldn't find any relevant data to fulfill the request. Please retry with a different input.","wp-parsely"):a.code===b.ParselySuggestionsApiOpenAiSchema?a.message=(0,m.__)("The Parse.ly API returned an incorrect response. Please try again later.","wp-parsely"):a.code===b.ParselySuggestionsApiAuthUnavailable&&(a.message=(0,m.__)("The Parse.ly API is currently unavailable. Please try again later.","wp-parsely")),a}return T(t,e),t.prototype.Message=function(e){return void 0===e&&(e=null),[b.PluginCredentialsNotSetMessageDetected,b.PluginSettingsSiteIdNotSet,b.PluginSettingsApiSecretNotSet].includes(this.code)?function(e){var t;return void 0===e&&(e=null),(0,w.jsx)(k,{className:null==e?void 0:e.className,testId:"empty-credentials-message",children:null!==(t=window.wpParselyEmptyCredentialsMessage)&&void 0!==t?t:(0,m.__)("Please ensure that the Site ID and API Secret given in the plugin's settings are correct.","wp-parsely")})}(e):(this.code===b.FetchError&&(this.hint=this.Hint((0,m.__)("This error can sometimes be caused by ad-blockers or browser tracking protections. Please add this site to any applicable allow lists and try again.","wp-parsely"))),this.code!==b.ParselyApiForbidden&&this.code!==b.ParselySuggestionsApiNoAuthentication||(this.hint=this.Hint((0,m.__)("Please ensure that the Site ID and API Secret given in the plugin's settings are correct.","wp-parsely"))),this.code===b.HttpRequestFailed&&(this.hint=this.Hint((0,m.__)("The Parse.ly API cannot be reached. Please verify that you are online.","wp-parsely"))),(0,w.jsx)(k,{className:null==e?void 0:e.className,testId:"error",children:"

".concat(this.message,"

").concat(this.hint?this.hint:"")}))},t.prototype.Hint=function(e){return'

'.concat((0,m.__)("Hint:","wp-parsely")," ").concat(e,"

")},t.prototype.createErrorSnackbar=function(){//.test(this.message)||(0,d.dispatch)("core/notices").createNotice("error",this.message,{type:"snackbar"})},t}(Error),j=function(e){var t=e.size,r=void 0===t?24:t,n=e.className,o=void 0===n?"wp-parsely-icon":n;return(0,w.jsxs)(v.SVG,{className:o,height:r,viewBox:"0 0 60 65",width:r,xmlns:"http://www.w3.org/2000/svg",children:[(0,w.jsx)(v.Path,{fill:"#5ba745",d:"M23.72,51.53c0-.18,0-.34-.06-.52a13.11,13.11,0,0,0-2.1-5.53A14.74,14.74,0,0,0,19.12,43c-.27-.21-.5-.11-.51.22l-.24,3.42c0,.33-.38.35-.49,0l-1.5-4.8a1.4,1.4,0,0,0-.77-.78,23.91,23.91,0,0,0-3.1-.84c-1.38-.24-3.39-.39-3.39-.39-.34,0-.45.21-.25.49l2.06,3.76c.2.27,0,.54-.29.33l-4.51-3.6a3.68,3.68,0,0,0-2.86-.48c-1,.16-2.44.46-2.44.46a.68.68,0,0,0-.39.25.73.73,0,0,0-.14.45S.41,43,.54,44a3.63,3.63,0,0,0,1.25,2.62L6.48,50c.28.2.09.49-.23.37l-4.18-.94c-.32-.12-.5,0-.4.37,0,0,.69,1.89,1.31,3.16a24,24,0,0,0,1.66,2.74,1.34,1.34,0,0,0,1,.52l5,.13c.33,0,.41.38.1.48L7.51,58c-.31.1-.34.35-.07.55a14.29,14.29,0,0,0,3.05,1.66,13.09,13.09,0,0,0,5.9.5,25.13,25.13,0,0,0,4.34-1,9.55,9.55,0,0,1-.08-1.2,9.32,9.32,0,0,1,3.07-6.91"}),(0,w.jsx)(v.Path,{fill:"#5ba745",d:"M59.7,41.53a.73.73,0,0,0-.14-.45.68.68,0,0,0-.39-.25s-1.43-.3-2.44-.46a3.64,3.64,0,0,0-2.86.48l-4.51,3.6c-.26.21-.49-.06-.29-.33l2.06-3.76c.2-.28.09-.49-.25-.49,0,0-2,.15-3.39.39a23.91,23.91,0,0,0-3.1.84,1.4,1.4,0,0,0-.77.78l-1.5,4.8c-.11.32-.48.3-.49,0l-.24-3.42c0-.33-.24-.43-.51-.22a14.74,14.74,0,0,0-2.44,2.47A13.11,13.11,0,0,0,36.34,51c0,.18,0,.34-.06.52a9.26,9.26,0,0,1,3,8.1,24.1,24.1,0,0,0,4.34,1,13.09,13.09,0,0,0,5.9-.5,14.29,14.29,0,0,0,3.05-1.66c.27-.2.24-.45-.07-.55l-3.22-1.17c-.31-.1-.23-.47.1-.48l5-.13a1.38,1.38,0,0,0,1-.52A24.6,24.6,0,0,0,57,52.92c.61-1.27,1.31-3.16,1.31-3.16.1-.33-.08-.49-.4-.37l-4.18.94c-.32.12-.51-.17-.23-.37l4.69-3.34A3.63,3.63,0,0,0,59.46,44c.13-1,.24-2.47.24-2.47"}),(0,w.jsx)(v.Path,{fill:"#5ba745",d:"M46.5,25.61c0-.53-.35-.72-.8-.43l-4.86,2.66c-.45.28-.56-.27-.23-.69l4.66-6.23a2,2,0,0,0,.28-1.68,36.51,36.51,0,0,0-2.19-4.89,34,34,0,0,0-2.81-3.94c-.33-.41-.74-.35-.91.16l-2.28,5.68c-.16.5-.6.48-.59-.05l.28-8.93a2.54,2.54,0,0,0-.66-1.64S35,4.27,33.88,3.27,30.78.69,30.78.69a1.29,1.29,0,0,0-1.54,0s-1.88,1.49-3.12,2.59-2.48,2.35-2.48,2.35A2.5,2.5,0,0,0,23,7.27l.27,8.93c0,.53-.41.55-.58.05l-2.29-5.69c-.17-.5-.57-.56-.91-.14a35.77,35.77,0,0,0-3,4.2,35.55,35.55,0,0,0-2,4.62,2,2,0,0,0,.27,1.67l4.67,6.24c.33.42.23,1-.22.69l-4.87-2.66c-.45-.29-.82-.1-.82.43a18.6,18.6,0,0,0,.83,5.07,20.16,20.16,0,0,0,5.37,7.77c3.19,3,5.93,7.8,7.45,11.08A9.6,9.6,0,0,1,30,49.09a9.31,9.31,0,0,1,2.86.45c1.52-3.28,4.26-8.11,7.44-11.09a20.46,20.46,0,0,0,5.09-7,19,19,0,0,0,1.11-5.82"}),(0,w.jsx)(v.Path,{fill:"#5ba745",d:"M36.12,58.44A6.12,6.12,0,1,1,30,52.32a6.11,6.11,0,0,1,6.12,6.12"})]})},C=window.wp.url,I=window.wp.apiFetch,O=r.n(I),R=function(){function e(){this.abortControllers=new Map}return e.prototype.cancelRequest=function(e){if(e)(t=this.abortControllers.get(e))&&(t.abort(),this.abortControllers.delete(e));else{var t,r=Array.from(this.abortControllers.keys()).pop();r&&(t=this.abortControllers.get(r))&&(t.abort(),this.abortControllers.delete(r))}},e.prototype.cancelAll=function(){this.abortControllers.forEach((function(e){return e.abort()})),this.abortControllers.clear()},e.prototype.getOrCreateController=function(e){if(e&&this.abortControllers.has(e))return{abortController:this.abortControllers.get(e),abortId:e};var t=null!=e?e:"auto-"+Date.now(),r=new AbortController;return this.abortControllers.set(t,r),{abortController:r,abortId:t}},e.prototype.fetch=function(e,t){return r=this,n=void 0,a=function(){var r,n,o,a,i,s;return function(e,t){var r,n,o,a,i={label:0,sent:function(){if(1&o[0])throw o[1];return o[1]},trys:[],ops:[]};return a={next:s(0),throw:s(1),return:s(2)},"function"==typeof Symbol&&(a[Symbol.iterator]=function(){return this}),a;function s(s){return function(l){return function(s){if(r)throw new TypeError("Generator is already executing.");for(;a&&(a=0,s[0]&&(i=0)),i;)try{if(r=1,n&&(o=2&s[0]?n.return:s[0]?n.throw||((o=n.return)&&o.call(n),0):n.next)&&!(o=o.call(n,s[1])).done)return o;switch(n=0,o&&(s=[2&s[0],o.value]),s[0]){case 0:case 1:o=s;break;case 4:return i.label++,{value:s[1],done:!1};case 5:i.label++,n=s[1],s=[0];continue;case 7:s=i.ops.pop(),i.trys.pop();continue;default:if(!((o=(o=i.trys).length>0&&o[o.length-1])||6!==s[0]&&2!==s[0])){i=0;continue}if(3===s[0]&&(!o||s[1]>o[0]&&s[1]0&&o[o.length-1])||6!==s[0]&&2!==s[0])){i=0;continue}if(3===s[0]&&(!o||s[1]>o[0]&&s[1]0&&o[o.length-1])||6!==s[0]&&2!==s[0])){i=0;continue}if(3===s[0]&&(!o||s[1]>o[0]&&s[1]0?(0,m.sprintf)( +!function(){"use strict";var e={20:function(e,t,r){var n=r(609),o=Symbol.for("react.element"),a=Symbol.for("react.fragment"),i=Object.prototype.hasOwnProperty,s=n.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED.ReactCurrentOwner,l={key:!0,ref:!0,__self:!0,__source:!0};function c(e,t,r){var n,a={},c=null,u=null;for(n in void 0!==r&&(c=""+r),void 0!==t.key&&(c=""+t.key),void 0!==t.ref&&(u=t.ref),t)i.call(t,n)&&!l.hasOwnProperty(n)&&(a[n]=t[n]);if(e&&e.defaultProps)for(n in t=e.defaultProps)void 0===a[n]&&(a[n]=t[n]);return{$$typeof:o,type:e,key:c,ref:u,props:a,_owner:s.current}}t.Fragment=a,t.jsx=c,t.jsxs=c},848:function(e,t,r){e.exports=r(20)},609:function(e){e.exports=window.React}},t={};function r(n){var o=t[n];if(void 0!==o)return o.exports;var a=t[n]={exports:{}};return e[n](a,a.exports,r),a.exports}r.n=function(e){var t=e&&e.__esModule?function(){return e.default}:function(){return e};return r.d(t,{a:t}),t},r.d=function(e,t){for(var n in t)r.o(t,n)&&!r.o(e,n)&&Object.defineProperty(e,n,{enumerable:!0,get:t[n]})},r.o=function(e,t){return Object.prototype.hasOwnProperty.call(e,t)},function(){var e,t,n,o,a,i,s,l,c,u,p,d=window.wp.data,y=window.wp.hooks,h=window.wp.plugins,f=((0,d.dispatch)("core/block-editor"),(0,d.dispatch)("core/editor"),(0,d.dispatch)("core/edit-post")),w=r(848),v=window.wp.components,g=window.wp.editor;void 0!==window.wp&&(p=null!==(t=null===(e=window.wp.editor)||void 0===e?void 0:e.PluginDocumentSettingPanel)&&void 0!==t?t:null!==(o=null===(n=window.wp.editPost)||void 0===n?void 0:n.PluginDocumentSettingPanel)&&void 0!==o?o:null===(a=window.wp.editSite)||void 0===a?void 0:a.PluginDocumentSettingPanel,null!==(s=null===(i=window.wp.editor)||void 0===i?void 0:i.PluginSidebar)&&void 0!==s||null!==(c=null===(l=window.wp.editPost)||void 0===l?void 0:l.PluginSidebar)&&void 0!==c||null===(u=window.wp.editSite)||void 0===u||u.PluginSidebar);var _,b,P=window.wp.element,m=window.wp.i18n,x=window.wp.wordcount,E=window.wp.primitives,A=(0,w.jsx)(E.SVG,{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 24 24",children:(0,w.jsx)(E.Path,{d:"M19.5 4.5h-7V6h4.44l-5.97 5.97 1.06 1.06L18 7.06v4.44h1.5v-7Zm-13 1a2 2 0 0 0-2 2v10a2 2 0 0 0 2 2h10a2 2 0 0 0 2-2v-3H17v3a.5.5 0 0 1-.5.5h-10a.5.5 0 0 1-.5-.5v-10a.5.5 0 0 1 .5-.5h3V5.5h-3Z"})}),S=function(){function e(){this._tkq=[],this.isLoaded=!1,this.isEnabled=!1,"undefined"!=typeof wpParselyTracksTelemetry&&(this.isEnabled=!0,this.loadTrackingLibrary())}return e.getInstance=function(){return window.wpParselyTelemetryInstance||Object.defineProperty(window,"wpParselyTelemetryInstance",{value:new e,writable:!1,configurable:!1,enumerable:!1}),window.wpParselyTelemetryInstance},e.prototype.loadTrackingLibrary=function(){var e=this,t=document.createElement("script");t.async=!0,t.src="//stats.wp.com/w.js",t.onload=function(){e.isLoaded=!0,e._tkq=window._tkq||[]},document.head.appendChild(t)},e.trackEvent=function(t){return r=this,n=arguments,a=function(t,r){var n;return void 0===r&&(r={}),function(e,t){var r,n,o,a,i={label:0,sent:function(){if(1&o[0])throw o[1];return o[1]},trys:[],ops:[]};return a={next:s(0),throw:s(1),return:s(2)},"function"==typeof Symbol&&(a[Symbol.iterator]=function(){return this}),a;function s(s){return function(l){return function(s){if(r)throw new TypeError("Generator is already executing.");for(;a&&(a=0,s[0]&&(i=0)),i;)try{if(r=1,n&&(o=2&s[0]?n.return:s[0]?n.throw||((o=n.return)&&o.call(n),0):n.next)&&!(o=o.call(n,s[1])).done)return o;switch(n=0,o&&(s=[2&s[0],o.value]),s[0]){case 0:case 1:o=s;break;case 4:return i.label++,{value:s[1],done:!1};case 5:i.label++,n=s[1],s=[0];continue;case 7:s=i.ops.pop(),i.trys.pop();continue;default:if(!((o=(o=i.trys).length>0&&o[o.length-1])||6!==s[0]&&2!==s[0])){i=0;continue}if(3===s[0]&&(!o||s[1]>o[0]&&s[1]=1e4&&(clearInterval(a),r("Telemetry library not loaded"))}),100);else r("Telemetry not enabled")}))},e.prototype.trackEvent=function(t,r){var n;this.isLoaded?(0!==t.indexOf(e.TRACKS_PREFIX)&&(t=e.TRACKS_PREFIX+t),this.isEventNameValid(t)?(r=this.prepareProperties(r),null===(n=this._tkq)||void 0===n||n.push(["recordEvent",t,r])):console.error("Error tracking event: Invalid event name")):console.error("Error tracking event: Telemetry not loaded")},e.prototype.isTelemetryEnabled=function(){return this.isEnabled},e.prototype.isProprietyValid=function(t){return e.PROPERTY_REGEX.test(t)},e.prototype.isEventNameValid=function(t){return e.EVENT_NAME_REGEX.test(t)},e.prototype.prepareProperties=function(e){return(e=this.sanitizeProperties(e)).parsely_version=wpParselyTracksTelemetry.version,wpParselyTracksTelemetry.user&&(e._ut=wpParselyTracksTelemetry.user.type,e._ui=wpParselyTracksTelemetry.user.id),wpParselyTracksTelemetry.vipgo_env&&(e.vipgo_env=wpParselyTracksTelemetry.vipgo_env),this.sanitizeProperties(e)},e.prototype.sanitizeProperties=function(e){var t=this,r={};return Object.keys(e).forEach((function(n){t.isProprietyValid(n)&&(r[n]=e[n])})),r},e.TRACKS_PREFIX="wpparsely_",e.EVENT_NAME_REGEX=/^(([a-z0-9]+)_){2}([a-z0-9_]+)$/,e.PROPERTY_REGEX=/^[a-z_][a-z0-9_]*$/,e}(),k=(S.trackEvent,function(e){void 0===e&&(e=null);var t="";(null==e?void 0:e.children)&&(t=e.children);var r="content-helper-error-message";return(null==e?void 0:e.className)&&(r+=" "+e.className),(0,w.jsx)("div",{className:r,"data-testid":null==e?void 0:e.testId,dangerouslySetInnerHTML:{__html:t}})}),T=(_=function(e,t){return _=Object.setPrototypeOf||{__proto__:[]}instanceof Array&&function(e,t){e.__proto__=t}||function(e,t){for(var r in t)Object.prototype.hasOwnProperty.call(t,r)&&(e[r]=t[r])},_(e,t)},function(e,t){if("function"!=typeof t&&null!==t)throw new TypeError("Class extends value "+String(t)+" is not a constructor or null");function __(){this.constructor=e}_(e,t),e.prototype=null===t?Object.create(t):(__.prototype=t.prototype,new __)});!function(e){e.AccessToFeatureDisabled="ch_access_to_feature_disabled",e.CannotFormulateApiQuery="ch_cannot_formulate_api_query",e.FetchError="fetch_error",e.HttpRequestFailed="http_request_failed",e.ParselyAborted="ch_parsely_aborted",e[e.ParselyApiForbidden=403]="ParselyApiForbidden",e.ParselyApiResponseContainsError="ch_response_contains_error",e.ParselyApiReturnedNoData="ch_parsely_api_returned_no_data",e.ParselyApiReturnedTooManyResults="ch_parsely_api_returned_too_many_results",e.PluginCredentialsNotSetMessageDetected="parsely_credentials_not_set_message_detected",e.PluginSettingsApiSecretNotSet="parsely_api_secret_not_set",e.PluginSettingsSiteIdNotSet="parsely_site_id_not_set",e.PostIsNotPublished="ch_post_not_published",e.UnknownError="ch_unknown_error",e.ParselySuggestionsApiAuthUnavailable="AUTH_UNAVAILABLE",e.ParselySuggestionsApiNoAuthentication="NO_AUTHENTICATION",e.ParselySuggestionsApiNoAuthorization="NO_AUTHORIZATION",e.ParselySuggestionsApiNoData="NO_DATA",e.ParselySuggestionsApiOpenAiError="OPENAI_ERROR",e.ParselySuggestionsApiOpenAiSchema="OPENAI_SCHEMA",e.ParselySuggestionsApiOpenAiUnavailable="OPENAI_UNAVAILABLE",e.ParselySuggestionsApiSchemaError="SCHEMA_ERROR"}(b||(b={}));var N=function(e){function t(r,n,o){void 0===o&&(o=(0,m.__)("Error: ","wp-parsely"));var a=this;r.startsWith(o)&&(o=""),(a=e.call(this,o+r)||this).hint=null,a.name=a.constructor.name,a.code=n;var i=[b.AccessToFeatureDisabled,b.ParselyApiForbidden,b.ParselyApiResponseContainsError,b.ParselyApiReturnedNoData,b.ParselyApiReturnedTooManyResults,b.PluginCredentialsNotSetMessageDetected,b.PluginSettingsApiSecretNotSet,b.PluginSettingsSiteIdNotSet,b.PostIsNotPublished,b.UnknownError,b.ParselySuggestionsApiAuthUnavailable,b.ParselySuggestionsApiNoAuthentication,b.ParselySuggestionsApiNoAuthorization,b.ParselySuggestionsApiNoData,b.ParselySuggestionsApiSchemaError];return a.retryFetch=!i.includes(a.code),Object.setPrototypeOf(a,t.prototype),a.code===b.AccessToFeatureDisabled?a.message=(0,m.__)("Access to this feature is disabled by the site's administration.","wp-parsely"):a.code===b.ParselySuggestionsApiNoAuthorization?a.message=(0,m.__)('This AI-powered feature is opt-in. To gain access, please submit a request here.',"wp-parsely"):a.code===b.ParselySuggestionsApiOpenAiError||a.code===b.ParselySuggestionsApiOpenAiUnavailable?a.message=(0,m.__)("The Parse.ly API returned an internal server error. Please retry with a different input, or try again later.","wp-parsely"):a.code===b.HttpRequestFailed&&a.message.includes("cURL error 28")?a.message=(0,m.__)("The Parse.ly API did not respond in a timely manner. Please try again later.","wp-parsely"):a.code===b.ParselySuggestionsApiSchemaError?a.message=(0,m.__)("The Parse.ly API returned a validation error. Please try again with different parameters.","wp-parsely"):a.code===b.ParselySuggestionsApiNoData?a.message=(0,m.__)("The Parse.ly API couldn't find any relevant data to fulfill the request. Please retry with a different input.","wp-parsely"):a.code===b.ParselySuggestionsApiOpenAiSchema?a.message=(0,m.__)("The Parse.ly API returned an incorrect response. Please try again later.","wp-parsely"):a.code===b.ParselySuggestionsApiAuthUnavailable&&(a.message=(0,m.__)("The Parse.ly API is currently unavailable. Please try again later.","wp-parsely")),a}return T(t,e),t.prototype.Message=function(e){return void 0===e&&(e=null),[b.PluginCredentialsNotSetMessageDetected,b.PluginSettingsSiteIdNotSet,b.PluginSettingsApiSecretNotSet].includes(this.code)?function(e){var t;return void 0===e&&(e=null),(0,w.jsx)(k,{className:null==e?void 0:e.className,testId:"empty-credentials-message",children:null!==(t=window.wpParselyEmptyCredentialsMessage)&&void 0!==t?t:(0,m.__)("Please ensure that the Site ID and API Secret given in the plugin's settings are correct.","wp-parsely")})}(e):(this.code===b.FetchError&&(this.hint=this.Hint((0,m.__)("This error can sometimes be caused by ad-blockers or browser tracking protections. Please add this site to any applicable allow lists and try again.","wp-parsely"))),this.code!==b.ParselyApiForbidden&&this.code!==b.ParselySuggestionsApiNoAuthentication||(this.hint=this.Hint((0,m.__)("Please ensure that the Site ID and API Secret given in the plugin's settings are correct.","wp-parsely"))),this.code===b.HttpRequestFailed&&(this.hint=this.Hint((0,m.__)("The Parse.ly API cannot be reached. Please verify that you are online.","wp-parsely"))),(0,w.jsx)(k,{className:null==e?void 0:e.className,testId:"error",children:"

".concat(this.message,"

").concat(this.hint?this.hint:"")}))},t.prototype.Hint=function(e){return'

'.concat((0,m.__)("Hint:","wp-parsely")," ").concat(e,"

")},t.prototype.createErrorSnackbar=function(){//.test(this.message)||(0,d.dispatch)("core/notices").createNotice("error",this.message,{type:"snackbar"})},t}(Error),j=function(e){var t=e.size,r=void 0===t?24:t,n=e.className,o=void 0===n?"wp-parsely-icon":n;return(0,w.jsxs)(v.SVG,{className:o,height:r,viewBox:"0 0 60 65",width:r,xmlns:"http://www.w3.org/2000/svg",children:[(0,w.jsx)(v.Path,{fill:"#5ba745",d:"M23.72,51.53c0-.18,0-.34-.06-.52a13.11,13.11,0,0,0-2.1-5.53A14.74,14.74,0,0,0,19.12,43c-.27-.21-.5-.11-.51.22l-.24,3.42c0,.33-.38.35-.49,0l-1.5-4.8a1.4,1.4,0,0,0-.77-.78,23.91,23.91,0,0,0-3.1-.84c-1.38-.24-3.39-.39-3.39-.39-.34,0-.45.21-.25.49l2.06,3.76c.2.27,0,.54-.29.33l-4.51-3.6a3.68,3.68,0,0,0-2.86-.48c-1,.16-2.44.46-2.44.46a.68.68,0,0,0-.39.25.73.73,0,0,0-.14.45S.41,43,.54,44a3.63,3.63,0,0,0,1.25,2.62L6.48,50c.28.2.09.49-.23.37l-4.18-.94c-.32-.12-.5,0-.4.37,0,0,.69,1.89,1.31,3.16a24,24,0,0,0,1.66,2.74,1.34,1.34,0,0,0,1,.52l5,.13c.33,0,.41.38.1.48L7.51,58c-.31.1-.34.35-.07.55a14.29,14.29,0,0,0,3.05,1.66,13.09,13.09,0,0,0,5.9.5,25.13,25.13,0,0,0,4.34-1,9.55,9.55,0,0,1-.08-1.2,9.32,9.32,0,0,1,3.07-6.91"}),(0,w.jsx)(v.Path,{fill:"#5ba745",d:"M59.7,41.53a.73.73,0,0,0-.14-.45.68.68,0,0,0-.39-.25s-1.43-.3-2.44-.46a3.64,3.64,0,0,0-2.86.48l-4.51,3.6c-.26.21-.49-.06-.29-.33l2.06-3.76c.2-.28.09-.49-.25-.49,0,0-2,.15-3.39.39a23.91,23.91,0,0,0-3.1.84,1.4,1.4,0,0,0-.77.78l-1.5,4.8c-.11.32-.48.3-.49,0l-.24-3.42c0-.33-.24-.43-.51-.22a14.74,14.74,0,0,0-2.44,2.47A13.11,13.11,0,0,0,36.34,51c0,.18,0,.34-.06.52a9.26,9.26,0,0,1,3,8.1,24.1,24.1,0,0,0,4.34,1,13.09,13.09,0,0,0,5.9-.5,14.29,14.29,0,0,0,3.05-1.66c.27-.2.24-.45-.07-.55l-3.22-1.17c-.31-.1-.23-.47.1-.48l5-.13a1.38,1.38,0,0,0,1-.52A24.6,24.6,0,0,0,57,52.92c.61-1.27,1.31-3.16,1.31-3.16.1-.33-.08-.49-.4-.37l-4.18.94c-.32.12-.51-.17-.23-.37l4.69-3.34A3.63,3.63,0,0,0,59.46,44c.13-1,.24-2.47.24-2.47"}),(0,w.jsx)(v.Path,{fill:"#5ba745",d:"M46.5,25.61c0-.53-.35-.72-.8-.43l-4.86,2.66c-.45.28-.56-.27-.23-.69l4.66-6.23a2,2,0,0,0,.28-1.68,36.51,36.51,0,0,0-2.19-4.89,34,34,0,0,0-2.81-3.94c-.33-.41-.74-.35-.91.16l-2.28,5.68c-.16.5-.6.48-.59-.05l.28-8.93a2.54,2.54,0,0,0-.66-1.64S35,4.27,33.88,3.27,30.78.69,30.78.69a1.29,1.29,0,0,0-1.54,0s-1.88,1.49-3.12,2.59-2.48,2.35-2.48,2.35A2.5,2.5,0,0,0,23,7.27l.27,8.93c0,.53-.41.55-.58.05l-2.29-5.69c-.17-.5-.57-.56-.91-.14a35.77,35.77,0,0,0-3,4.2,35.55,35.55,0,0,0-2,4.62,2,2,0,0,0,.27,1.67l4.67,6.24c.33.42.23,1-.22.69l-4.87-2.66c-.45-.29-.82-.1-.82.43a18.6,18.6,0,0,0,.83,5.07,20.16,20.16,0,0,0,5.37,7.77c3.19,3,5.93,7.8,7.45,11.08A9.6,9.6,0,0,1,30,49.09a9.31,9.31,0,0,1,2.86.45c1.52-3.28,4.26-8.11,7.44-11.09a20.46,20.46,0,0,0,5.09-7,19,19,0,0,0,1.11-5.82"}),(0,w.jsx)(v.Path,{fill:"#5ba745",d:"M36.12,58.44A6.12,6.12,0,1,1,30,52.32a6.11,6.11,0,0,1,6.12,6.12"})]})},C=window.wp.url,I=window.wp.apiFetch,O=r.n(I),R=function(){function e(){this.abortControllers=new Map}return e.prototype.cancelRequest=function(e){if(e)(t=this.abortControllers.get(e))&&(t.abort(),this.abortControllers.delete(e));else{var t,r=Array.from(this.abortControllers.keys()).pop();r&&(t=this.abortControllers.get(r))&&(t.abort(),this.abortControllers.delete(r))}},e.prototype.cancelAll=function(){this.abortControllers.forEach((function(e){return e.abort()})),this.abortControllers.clear()},e.prototype.getOrCreateController=function(e){if(e&&this.abortControllers.has(e))return{abortController:this.abortControllers.get(e),abortId:e};var t=null!=e?e:"auto-"+Date.now(),r=new AbortController;return this.abortControllers.set(t,r),{abortController:r,abortId:t}},e.prototype.fetch=function(e,t){return r=this,n=void 0,a=function(){var r,n,o,a,i,s;return function(e,t){var r,n,o,a,i={label:0,sent:function(){if(1&o[0])throw o[1];return o[1]},trys:[],ops:[]};return a={next:s(0),throw:s(1),return:s(2)},"function"==typeof Symbol&&(a[Symbol.iterator]=function(){return this}),a;function s(s){return function(l){return function(s){if(r)throw new TypeError("Generator is already executing.");for(;a&&(a=0,s[0]&&(i=0)),i;)try{if(r=1,n&&(o=2&s[0]?n.return:s[0]?n.throw||((o=n.return)&&o.call(n),0):n.next)&&!(o=o.call(n,s[1])).done)return o;switch(n=0,o&&(s=[2&s[0],o.value]),s[0]){case 0:case 1:o=s;break;case 4:return i.label++,{value:s[1],done:!1};case 5:i.label++,n=s[1],s=[0];continue;case 7:s=i.ops.pop(),i.trys.pop();continue;default:if(!((o=(o=i.trys).length>0&&o[o.length-1])||6!==s[0]&&2!==s[0])){i=0;continue}if(3===s[0]&&(!o||s[1]>o[0]&&s[1]0&&o[o.length-1])||6!==s[0]&&2!==s[0])){i=0;continue}if(3===s[0]&&(!o||s[1]>o[0]&&s[1]0&&o[o.length-1])||6!==s[0]&&2!==s[0])){i=0;continue}if(3===s[0]&&(!o||s[1]>o[0]&&s[1]0?(0,m.sprintf)( // Translators: %1$s the number of words in the excerpt. // Translators: %1$s the number of words in the excerpt. (0,m._n)("%1$s word","%1$s words",e,"wp-parsely"),e):"")}),[h.currentExcerpt,k]),(0,P.useEffect)((function(){var e=document.querySelector(".editor-post-excerpt textarea");e&&(e.scrollTop=0)}),[h.newExcerptGeneratedCount]),(0,w.jsxs)("div",{className:"editor-post-excerpt",children:[(0,w.jsxs)("div",{style:{position:"relative"},children:[t&&(0,w.jsx)("div",{className:"editor-post-excerpt__loading_animation",children:(0,w.jsx)(H,{})}),(0,w.jsx)(v.TextareaControl,{__nextHasNoMarginBottom:!0,label:(0,m.__)("Write an excerpt (optional)","wp-parsely"),className:"editor-post-excerpt__textarea",onChange:function(e){h.isUnderReview||_({excerpt:e}),f(F(F({},h),{currentExcerpt:e})),l(!0)},onKeyUp:function(){var e;if(s)l(!1);else{var t=document.querySelector(".editor-post-excerpt textarea"),r=null!==(e=null==t?void 0:t.textContent)&&void 0!==e?e:"";f(F(F({},h),{currentExcerpt:r}))}},value:t?"":h.isUnderReview?h.currentExcerpt:k,help:u||null})]}),(0,w.jsxs)(v.Button,{href:(0,m.__)("https://wordpress.org/documentation/article/page-post-settings-sidebar/#excerpt","wp-parsely"),target:"_blank",variant:"link",children:[(0,m.__)("Learn more about manual excerpts","wp-parsely"),(0,w.jsx)(v.Icon,{icon:A,size:18,className:"parsely-external-link-icon"})]}),(0,w.jsxs)("div",{className:"wp-parsely-excerpt-generator",children:[(0,w.jsxs)("div",{className:"wp-parsely-excerpt-generator-header",children:[(0,w.jsx)(j,{size:16}),(0,w.jsxs)("div",{className:"wp-parsely-excerpt-generator-header-label",children:[(0,m.__)("Generate With Parse.ly","wp-parsely"),(0,w.jsx)("span",{className:"beta-label",children:(0,m.__)("Beta","wp-parsely")})]})]}),o&&(0,w.jsx)(v.Notice,{className:"wp-parsely-excerpt-generator-error",onRemove:function(){return a(void 0)},status:"info",children:o.Message()}),(0,w.jsx)("div",{className:"wp-parsely-excerpt-generator-controls",children:h.isUnderReview?(0,w.jsxs)(w.Fragment,{children:[(0,w.jsx)(v.Button,{variant:"secondary",onClick:function(){return L(void 0,void 0,void 0,(function(){return M(this,(function(e){switch(e.label){case 0:return[4,_({excerpt:h.currentExcerpt})];case 1:return e.sent(),f(F(F({},h),{isUnderReview:!1})),S.trackEvent("excerpt_generator_accepted"),[2]}}))}))},children:(0,m.__)("Accept","wp-parsely")}),(0,w.jsx)(v.Button,{isDestructive:!0,variant:"secondary",onClick:function(){return L(void 0,void 0,void 0,(function(){return M(this,(function(e){return _({excerpt:h.oldExcerpt}),f(F(F({},h),{currentExcerpt:h.oldExcerpt,isUnderReview:!1})),S.trackEvent("excerpt_generator_discarded"),[2]}))}))},children:(0,m.__)("Discard","wp-parsely")})]}):(0,w.jsxs)(v.Button,{onClick:function(){return L(void 0,void 0,void 0,(function(){var e,t;return M(this,(function(n){switch(n.label){case 0:r(!0),a(void 0),n.label=1;case 1:return n.trys.push([1,3,4,5]),S.trackEvent("excerpt_generator_pressed"),[4,D.getInstance().generateExcerpt(C,T)];case 2:return e=n.sent(),f({currentExcerpt:e,isUnderReview:!0,newExcerptGeneratedCount:h.newExcerptGeneratedCount+1,oldExcerpt:k}),[3,5];case 3:return(t=n.sent())instanceof N?a(t):(a(new N((0,m.__)("An unknown error occurred.","wp-parsely"),b.UnknownError)),console.error(t)),[3,5];case 4:return r(!1),[7];case 5:return[2]}}))}))},variant:"primary",isBusy:t,disabled:t||!T,children:[t&&(0,m.__)("Generating Excerpt…","wp-parsely"),!t&&h.newExcerptGeneratedCount>0&&(0,m.__)("Regenerate Excerpt","wp-parsely"),!t&&0===h.newExcerptGeneratedCount&&(0,m.__)("Generate Excerpt","wp-parsely")]})}),(0,w.jsxs)(v.Button,{href:"https://docs.parse.ly/plugin-content-helper/#h-excerpt-generator-beta",target:"_blank",variant:"link",children:[(0,m.__)("Learn more about Parse.ly AI","wp-parsely"),(0,w.jsx)(v.Icon,{icon:A,size:18,className:"parsely-external-link-icon"})]})]})]})},H=function(){return(0,w.jsx)(v.Animate,{type:"loading",children:function(e){var t=e.className;return(0,w.jsx)("span",{className:t,children:(0,m.__)("Generating…","wp-parsely")})}})},q=function(){return(0,w.jsx)(g.PostTypeSupportCheck,{supportKeys:"excerpt",children:(0,w.jsx)(p,{name:"parsely-post-excerpt",title:(0,m.__)("Excerpt","wp-parsely"),children:(0,w.jsx)(G,{})})})};(0,y.addFilter)("plugins.registerPlugin","wp-parsely-excerpt-generator",(function(e,t){var r,n,o;return"wp-parsely-block-editor-sidebar"!==t||((null===(r=null===window||void 0===window?void 0:window.Jetpack_Editor_Initial_State)||void 0===r?void 0:r.available_blocks["ai-content-lens"])&&(console.log("Parse.ly: Jetpack AI is enabled and will be disabled."),(0,y.removeFilter)("blocks.registerBlockType","jetpack/ai-content-lens-features")),(0,h.registerPlugin)("wp-parsely-excerpt-generator",{render:q}),(null===(n=(0,d.dispatch)("core/editor"))||void 0===n?void 0:n.removeEditorPanel)?null===(o=(0,d.dispatch)("core/editor"))||void 0===o||o.removeEditorPanel("post-excerpt"):null==f||f.removeEditorPanel("post-excerpt")),e}),1e3)}()}(); \ No newline at end of file diff --git a/src/content-helper/editor-sidebar/smart-linking/provider.ts b/src/content-helper/editor-sidebar/smart-linking/provider.ts index 66b3215d47..4e60e8edf3 100644 --- a/src/content-helper/editor-sidebar/smart-linking/provider.ts +++ b/src/content-helper/editor-sidebar/smart-linking/provider.ts @@ -145,7 +145,7 @@ export class SmartLinkingProvider extends BaseProvider { } ), data: { url_exclusion_list: urlExclusionList, - content, + text: content, }, } ); diff --git a/src/content-helper/editor-sidebar/title-suggestions/provider.ts b/src/content-helper/editor-sidebar/title-suggestions/provider.ts index a5fb0d4a14..9b85f4767f 100644 --- a/src/content-helper/editor-sidebar/title-suggestions/provider.ts +++ b/src/content-helper/editor-sidebar/title-suggestions/provider.ts @@ -58,7 +58,7 @@ export class TitleSuggestionsProvider extends BaseProvider { persona: getPersonaLabel( persona ), } ), data: { - content, + text: content, }, } ); diff --git a/src/content-helper/excerpt-generator/provider.ts b/src/content-helper/excerpt-generator/provider.ts index 9716dcd878..2a5ebaf830 100644 --- a/src/content-helper/excerpt-generator/provider.ts +++ b/src/content-helper/excerpt-generator/provider.ts @@ -55,7 +55,7 @@ export class ExcerptGeneratorProvider extends BaseProvider { title, } ), data: { - content, + text: content, }, } ); } diff --git a/src/rest-api/content-helper/class-endpoint-excerpt-generator.php b/src/rest-api/content-helper/class-endpoint-excerpt-generator.php index d2ee594240..1adb8d544d 100644 --- a/src/rest-api/content-helper/class-endpoint-excerpt-generator.php +++ b/src/rest-api/content-helper/class-endpoint-excerpt-generator.php @@ -85,7 +85,7 @@ public function register_routes(): void { array( 'POST' ), array( $this, 'generate_excerpt' ), array( - 'content' => array( + 'text' => array( 'description' => __( 'The text to generate the excerpt from.', 'wp-parsely' ), 'type' => 'string', 'required' => true, @@ -127,7 +127,7 @@ public function generate_excerpt( WP_REST_Request $request ) { * * @var string $post_content */ - $post_content = $request->get_param( 'content' ); + $post_content = $request->get_param( 'text' ); /** * The post title to be sent to the API. diff --git a/src/rest-api/content-helper/class-endpoint-smart-linking.php b/src/rest-api/content-helper/class-endpoint-smart-linking.php index ab1a2f14b0..fe37e324ff 100644 --- a/src/rest-api/content-helper/class-endpoint-smart-linking.php +++ b/src/rest-api/content-helper/class-endpoint-smart-linking.php @@ -87,7 +87,7 @@ public function register_routes(): void { array( 'POST' ), array( $this, 'generate_smart_links' ), array( - 'content' => array( + 'text' => array( 'required' => true, 'type' => 'string', 'description' => __( 'The text to generate smart links for.', 'wp-parsely' ), @@ -229,7 +229,7 @@ public function generate_smart_links( WP_REST_Request $request ) { * * @var string $post_content */ - $post_content = $request->get_param( 'content' ); + $post_content = $request->get_param( 'text' ); /** * The maximum number of smart links to generate. diff --git a/src/rest-api/content-helper/class-endpoint-title-suggestions.php b/src/rest-api/content-helper/class-endpoint-title-suggestions.php index 8b81fc9587..df5717bbaf 100644 --- a/src/rest-api/content-helper/class-endpoint-title-suggestions.php +++ b/src/rest-api/content-helper/class-endpoint-title-suggestions.php @@ -85,7 +85,7 @@ public function register_routes(): void { array( 'POST' ), array( $this, 'generate_titles' ), array( - 'content' => array( + 'text' => array( 'description' => __( 'The content for which to generate titles.', 'wp-parsely' ), 'required' => true, 'type' => 'string', @@ -128,7 +128,7 @@ public function generate_titles( WP_REST_Request $request ) { * * @var string $post_content */ - $post_content = $request->get_param( 'content' ); + $post_content = $request->get_param( 'text' ); /** * The maximum number of titles to generate. diff --git a/tests/Integration/RestAPI/ContentHelper/EndpointExcerptGeneratorTest.php b/tests/Integration/RestAPI/ContentHelper/EndpointExcerptGeneratorTest.php index 7d38eedc1b..c959a8e2d7 100644 --- a/tests/Integration/RestAPI/ContentHelper/EndpointExcerptGeneratorTest.php +++ b/tests/Integration/RestAPI/ContentHelper/EndpointExcerptGeneratorTest.php @@ -132,7 +132,7 @@ public function test_generate_excerpt_returns_valid_response(): void { // Create a mock request. $request = new WP_REST_Request( 'POST', '/excerpt-generator/generate' ); - $request->set_param( 'content', 'Test content' ); + $request->set_param( 'text', 'Test content' ); $request->set_param( 'title', 'Test title' ); $request->set_param( 'persona', 'journalist' ); $request->set_param( 'style', 'neutral' ); @@ -178,7 +178,7 @@ public function test_generate_excerpt_returns_error_on_failure(): void { // Create a mock request. $request = new WP_REST_Request( 'POST', '/excerpt-generator/generate' ); - $request->set_param( 'content', 'Test content' ); + $request->set_param( 'text', 'Test content' ); $request->set_param( 'title', 'Test title' ); $request->set_param( 'persona', 'journalist' ); $request->set_param( 'style', 'neutral' ); diff --git a/tests/Integration/RestAPI/ContentHelper/EndpointSmartLinkingTest.php b/tests/Integration/RestAPI/ContentHelper/EndpointSmartLinkingTest.php index 3c52d72ed8..8389bd7c0c 100644 --- a/tests/Integration/RestAPI/ContentHelper/EndpointSmartLinkingTest.php +++ b/tests/Integration/RestAPI/ContentHelper/EndpointSmartLinkingTest.php @@ -148,7 +148,7 @@ public function test_generate_smart_links_returns_valid_response(): void { // Create a mock request. $request = new WP_REST_Request( 'POST', '/smart-linking/generate' ); - $request->set_param( 'content', 'Test content' ); + $request->set_param( 'text', 'Test content' ); $request->set_param( 'max_links', 2 ); $request->set_param( 'url_exclusion_list', array( 'http://excluded.com' ) ); @@ -193,7 +193,7 @@ public function test_generate_smart_links_returns_error_on_failure(): void { // Create a mock request. $request = new WP_REST_Request( 'POST', '/smart-linking/generate' ); - $request->set_param( 'content', 'Test content' ); + $request->set_param( 'text', 'Test content' ); $request->set_param( 'max_links', 2 ); $request->set_param( 'url_exclusion_list', array( 'http://excluded.com' ) ); diff --git a/tests/Integration/RestAPI/ContentHelper/EndpointTitleSuggestionsTest.php b/tests/Integration/RestAPI/ContentHelper/EndpointTitleSuggestionsTest.php index a9c96e8fae..a54e9391b3 100644 --- a/tests/Integration/RestAPI/ContentHelper/EndpointTitleSuggestionsTest.php +++ b/tests/Integration/RestAPI/ContentHelper/EndpointTitleSuggestionsTest.php @@ -131,7 +131,7 @@ public function test_generate_titles_returns_valid_response(): void { // Create a mock request. $request = new WP_REST_Request( 'POST', '/title-suggestions/generate' ); - $request->set_param( 'content', 'Test content' ); + $request->set_param( 'text', 'Test content' ); $request->set_param( 'limit', 3 ); $request->set_param( 'style', 'neutral' ); $request->set_param( 'persona', 'journalist' ); @@ -179,7 +179,7 @@ public function test_generate_titles_returns_error_on_failure(): void { // Create a mock request. $request = new WP_REST_Request( 'POST', '/title-suggestions/generate' ); - $request->set_param( 'content', 'Test content' ); + $request->set_param( 'text', 'Test content' ); $request->set_param( 'limit', 3 ); $request->set_param( 'style', 'neutral' ); $request->set_param( 'persona', 'journalist' ); From a2cefb5bf5f9da08ed21b1f5039766d13d9345da Mon Sep 17 00:00:00 2001 From: Henrique Mouta Date: Thu, 29 Aug 2024 11:54:54 +0100 Subject: [PATCH 11/49] Add `stats` endpoint --- src/RemoteAPI/class-base-endpoint-remote.php | 6 +- src/rest-api/class-base-endpoint.php | 8 +- src/rest-api/class-rest-api-controller.php | 2 + .../class-endpoint-smart-linking.php | 83 +-- src/rest-api/stats/class-endpoint-post.php | 495 ++++++++++++++++++ src/rest-api/stats/class-endpoint-posts.php | 249 +++++++++ src/rest-api/stats/class-endpoint-related.php | 112 ++++ src/rest-api/stats/class-stats-controller.php | 48 ++ src/rest-api/stats/trait-post-data.php | 145 +++++ src/rest-api/stats/trait-related-posts.php | 165 ++++++ src/rest-api/trait-use-post-id-parameter.php | 88 ++++ 11 files changed, 1332 insertions(+), 69 deletions(-) create mode 100644 src/rest-api/stats/class-endpoint-post.php create mode 100644 src/rest-api/stats/class-endpoint-posts.php create mode 100644 src/rest-api/stats/class-endpoint-related.php create mode 100644 src/rest-api/stats/class-stats-controller.php create mode 100644 src/rest-api/stats/trait-post-data.php create mode 100644 src/rest-api/stats/trait-related-posts.php create mode 100644 src/rest-api/trait-use-post-id-parameter.php diff --git a/src/RemoteAPI/class-base-endpoint-remote.php b/src/RemoteAPI/class-base-endpoint-remote.php index 334f38e0d9..e9ec20eb38 100644 --- a/src/RemoteAPI/class-base-endpoint-remote.php +++ b/src/RemoteAPI/class-base-endpoint-remote.php @@ -108,7 +108,11 @@ public function get_items( array $query, bool $associative = false ) { } if ( ! property_exists( $decoded, 'data' ) ) { - return new WP_Error( $decoded->code ?? 400, $decoded->message ?? __( 'Unable to read data from upstream API', 'wp-parsely' ) ); + return new WP_Error( + $decoded->code ?? 400, + $decoded->message ?? __( 'Unable to read data from upstream API', 'wp-parsely' ), + array( 'status' => $decoded->code ?? 400 ) + ); } if ( ! is_array( $decoded->data ) ) { diff --git a/src/rest-api/class-base-endpoint.php b/src/rest-api/class-base-endpoint.php index 3229b4bb80..2ce4f9be26 100644 --- a/src/rest-api/class-base-endpoint.php +++ b/src/rest-api/class-base-endpoint.php @@ -169,7 +169,13 @@ public function register_rest_route( string $route, array $methods, callable $ca * @return string */ public function get_full_endpoint( string $route = '' ): string { - $route = $this->get_endpoint_name() . '/' . $route; + $route = trim( $route, '/' ); + + if ( '' !== $route ) { + $route = $this->get_endpoint_name() . '/' . $route; + } else { + $route = $this->get_endpoint_name(); + } return '/' . $this->api_controller->get_full_namespace() . diff --git a/src/rest-api/class-rest-api-controller.php b/src/rest-api/class-rest-api-controller.php index e864e59fad..5ce4e76fac 100644 --- a/src/rest-api/class-rest-api-controller.php +++ b/src/rest-api/class-rest-api-controller.php @@ -11,6 +11,7 @@ namespace Parsely\REST_API; use Parsely\REST_API\Content_Helper\Content_Helper_Controller; +use Parsely\REST_API\Stats\Stats_Controller; /** * The REST API Controller. @@ -60,6 +61,7 @@ public function init(): void { // Register the controllers for each namespace. $controllers = array( new Content_Helper_Controller( $this->get_parsely() ), + new Stats_Controller( $this->get_parsely() ), ); // Initialize the controllers. diff --git a/src/rest-api/content-helper/class-endpoint-smart-linking.php b/src/rest-api/content-helper/class-endpoint-smart-linking.php index fe37e324ff..cce1c99c43 100644 --- a/src/rest-api/content-helper/class-endpoint-smart-linking.php +++ b/src/rest-api/content-helper/class-endpoint-smart-linking.php @@ -14,6 +14,7 @@ use Parsely\Models\Smart_Link; use Parsely\RemoteAPI\ContentSuggestions\Suggest_Linked_Reference_API; use Parsely\REST_API\Base_Endpoint; +use Parsely\REST_API\Use_Post_ID_Parameter_Trait; use WP_Error; use WP_Post; use WP_REST_Request; @@ -28,6 +29,7 @@ */ class Endpoint_Smart_Linking extends Base_Endpoint { use Content_Helper_Feature; + use Use_Post_ID_Parameter_Trait; /** * The Suggest Linked Reference API instance. @@ -110,40 +112,28 @@ public function register_routes(): void { * GET /smart-linking/{post_id}/get * Gets the smart links for a post. */ - $this->register_rest_route( - '(?P\d+)/get', + $this->register_rest_route_with_post_id( + '/get', array( 'GET' ), - array( $this, 'get_smart_links' ), - array( - 'post_id' => array( - 'required' => true, - 'description' => __( 'The post ID.', 'wp-parsely' ), - 'validate_callback' => array( $this, 'validate_post_id' ), - ), - ) + array( $this, 'get_smart_links' ) ); /** * POST /smart-linking/{post_id}/add * Adds a smart link to a post. */ - $this->register_rest_route( - '(?P\d+)/add', + $this->register_rest_route_with_post_id( + '/add', array( 'POST' ), array( $this, 'add_smart_link' ), array( - 'post_id' => array( - 'required' => true, - 'description' => __( 'The post ID.', 'wp-parsely' ), - 'validate_callback' => array( $this, 'validate_post_id' ), - ), - 'link' => array( + 'link' => array( 'required' => true, 'type' => 'object', 'description' => __( 'The smart link data to add.', 'wp-parsely' ), 'validate_callback' => array( $this, 'validate_smart_link_params' ), ), - 'update' => array( + 'update' => array( 'type' => 'boolean', 'description' => __( 'Whether to update the existing smart link.', 'wp-parsely' ), 'default' => false, @@ -155,23 +145,18 @@ public function register_routes(): void { * POST /smart-linking/{post_id}/add-multiple * Adds multiple smart links to a post. */ - $this->register_rest_route( - '(?P\d+)/add-multiple', + $this->register_rest_route_with_post_id( + '/add-multiple', array( 'POST' ), array( $this, 'add_multiple_smart_links' ), array( - 'post_id' => array( - 'required' => true, - 'description' => __( 'The post ID.', 'wp-parsely' ), - 'validate_callback' => array( $this, 'validate_post_id' ), - ), - 'links' => array( + 'links' => array( 'required' => true, 'type' => 'array', 'description' => __( 'The multiple smart links data to add.', 'wp-parsely' ), 'validate_callback' => array( $this, 'validate_multiple_smart_links' ), ), - 'update' => array( + 'update' => array( 'type' => 'boolean', 'description' => __( 'Whether to update the existing smart links.', 'wp-parsely' ), 'default' => false, @@ -183,17 +168,12 @@ public function register_routes(): void { * POST /smart-linking/{post_id}/set * Updates the smart links of a given post and removes the ones that are not in the request. */ - $this->register_rest_route( - '(?P\d+)/set', + $this->register_rest_route_with_post_id( + '/set', array( 'POST' ), array( $this, 'set_smart_links' ), array( - 'post_id' => array( - 'required' => true, - 'description' => __( 'The post ID.', 'wp-parsely' ), - 'validate_callback' => array( $this, 'validate_post_id' ), - ), - 'links' => array( + 'links' => array( 'required' => true, 'type' => 'array', 'description' => __( 'The smart links data to set.', 'wp-parsely' ), @@ -548,37 +528,6 @@ public function url_to_post_type( WP_REST_Request $request ): WP_REST_Response { return new WP_REST_Response( $response, 200 ); } - /** - * Validates the post ID parameter. - * - * The callback sets the post object in the request object if the parameter is valid. - * - * @since 3.16.0 - * @access private - * - * @param string $param The parameter value. - * @param WP_REST_Request $request The request object. - * @return bool Whether the parameter is valid. - */ - public function validate_post_id( string $param, WP_REST_Request $request ): bool { - if ( ! is_numeric( $param ) ) { - return false; - } - - $param = filter_var( $param, FILTER_VALIDATE_INT ); - - if ( false === $param ) { - return false; - } - - // Validate if the post ID exists. - $post = get_post( $param ); - // Set the post attribute in the request. - $request->set_param( 'post', $post ); - - return null !== $post; - } - /** * Validates the URL exclusion list parameter. * diff --git a/src/rest-api/stats/class-endpoint-post.php b/src/rest-api/stats/class-endpoint-post.php new file mode 100644 index 0000000000..c837133150 --- /dev/null +++ b/src/rest-api/stats/class-endpoint-post.php @@ -0,0 +1,495 @@ +analytics_post_detail_api = new Analytics_Post_Detail_API( $this->parsely ); + $this->referrers_post_detail_api = new Referrers_Post_Detail_API( $this->parsely ); + $this->related_posts_api = new Related_API( $this->parsely ); + } + + /** + * Returns the endpoint name. + * + * @since 3.17.0 + * + * @return string The endpoint name. + */ + public function get_endpoint_name(): string { + return 'post'; + } + + /** + * Registers the routes for the endpoint. + * + * @since 3.17.0 + */ + public function register_routes(): void { + /** + * GET /stats/post/{post_id}/details + * Returns the analytics details of a post. + */ + $this->register_rest_route_with_post_id( + '/details', + array( 'GET' ), + array( $this, 'get_post_details' ), + array_merge( + array( + 'period_start' => array( + 'description' => __( 'The start of the period.', 'wp-parsely' ), + 'type' => 'string', + 'required' => false, + ), + 'period_end' => array( + 'description' => __( 'The end of the period.', 'wp-parsely' ), + 'type' => 'string', + 'required' => false, + ), + ), + $this->get_itm_source_param_args() + ) + ); + + /** + * GET /stats/post/{post_id}/referrers + * Returns the referrers of a post. + */ + $this->register_rest_route_with_post_id( + '/referrers', + array( 'GET' ), + array( $this, 'get_post_referrers' ), + array( + 'period_start' => array( + 'description' => __( 'The start of the period.', 'wp-parsely' ), + 'type' => 'string', + 'required' => false, + ), + 'period_end' => array( + 'description' => __( 'The end of the period.', 'wp-parsely' ), + 'type' => 'string', + 'required' => false, + ), + 'total_views' => array( + 'description' => __( 'The total views of the post.', 'wp-parsely' ), + 'type' => 'string', + 'required' => false, + 'default' => '0', + ), + ) + ); + + /** + * GET /stats/post/{post_id}/related + * Returns the related posts of a post. + */ + $this->register_rest_route_with_post_id( + '/related', + array( 'GET' ), + array( $this, 'get_related_posts' ), + $this->get_related_posts_param_args() + ); + } + + /** + * API Endpoint: GET /stats/post/{post_id}/details + * + * Gets the details of a post. + * + * @since 3.17.0 + * + * @param WP_REST_Request $request The request object. + * @return WP_REST_Response|WP_Error The response object. + */ + public function get_post_details( WP_REST_Request $request ) { + /** + * The post object. + * + * @var WP_Post $post + */ + $post = $request->get_param( 'post' ); + $permalink = get_permalink( $post->ID ); + + // Set the itm_source parameter. + $this->set_itm_source_from_request( $request ); + + // Get the data from the API. + $analytics_request = $this->analytics_post_detail_api->get_items( + array( + 'url' => $permalink, + 'period_start' => $request->get_param( 'period_start' ), + 'period_end' => $request->get_param( 'period_end' ), + ) + ); + + if ( is_wp_error( $analytics_request ) ) { + return $analytics_request; + } + + $post_data = array(); + /** + * The analytics data object. + * + * @var array $analytics_request + */ + foreach ( $analytics_request as $data ) { + $post_data[] = $this->extract_post_data( $data ); + } + + $response = array( + 'params' => $request->get_params(), + 'data' => $post_data, + ); + + return new WP_REST_Response( $response, 200 ); + } + + /** + * API Endpoint: GET /stats/post/{post_id}/referrers + * + * Gets the referrers of a post. + * + * @since 3.17.0 + * + * @param WP_REST_Request $request The request object. + * @return WP_REST_Response|WP_Error The response object. + */ + public function get_post_referrers( WP_REST_Request $request ) { + /** + * The post object. + * + * @var WP_Post $post + */ + $post = $request->get_param( 'post' ); + $permalink = get_permalink( $post->ID ); + + // Set the itm_source parameter. + $this->set_itm_source_from_request( $request ); + + // Get the total views. + $total_views = $request->get_param( 'total_views' ) ?? 0; + + if ( is_string( $total_views ) ) { + $total_views = Utils::convert_to_positive_integer( $total_views ); + } + + $this->total_views = $total_views; + + // Do the analytics request. + $analytics_request = $this->referrers_post_detail_api->get_items( + array( + 'url' => $permalink, + 'period_start' => $request->get_param( 'period_start' ), + 'period_end' => $request->get_param( 'period_end' ), + ) + ); + + if ( is_wp_error( $analytics_request ) ) { + return $analytics_request; + } + + /** + * The analytics data object. + * + * @var array $analytics_request + */ + $referrers_types = $this->generate_referrer_types_data( $analytics_request ); + $direct_views = Utils::convert_to_positive_integer( + $referrers_types->direct->views ?? '0' + ); + $referrers_top = $this->generate_referrers_data( 5, $analytics_request, $direct_views ); + + $response_data = array( + 'params' => $request->get_params(), + 'data' => array( + 'top' => $referrers_top, + 'types' => $referrers_types, + ), + ); + + return new WP_REST_Response( $response_data, 200 ); + } + + /** + * API Endpoint: GET /stats/post/{post_id}/related + * + * Gets the related posts of a post. + * + * @since 3.17.0 + * + * @param WP_REST_Request $request The request object. + * @return WP_REST_Response|WP_Error The response data. + */ + public function get_related_posts( WP_REST_Request $request ) { + /** + * The post object. + * + * @var WP_Post $post + */ + $post = $request->get_param( 'post' ); + + /** + * The post permalink. + * + * @var string $permalink + */ + $permalink = get_permalink( $post->ID ); + + $related_posts = $this->get_related_posts_of_url( $request, $permalink ); + + $response_data = array( + 'params' => $request->get_params(), + 'data' => $related_posts, + ); + return new WP_REST_Response( $response_data, 200 ); + } + + /** + * Generates the referrer types data. + * + * Referrer types are: + * - `social`: Views coming from social media. + * - `search`: Views coming from search engines. + * - `other`: Views coming from other referrers, like external websites. + * - `internal`: Views coming from linking pages of the same website. + * + * Returned object properties: + * - `views`: The number of views. + * - `viewPercentage`: The number of views as a percentage, compared to the + * total views of all referrer types. + * + * @since 3.6.0 + * @since 3.17.0 Moved from the `Referrers_Post_Detail_API_Proxy` class. + * + * @param array $response The response received by the proxy. + * @return stdClass The generated data. + */ + private function generate_referrer_types_data( array $response ): stdClass { + $result = new stdClass(); + $total_referrer_views = 0; // Views from all referrer types combined. + + // Set referrer type order as it is displayed in the Parse.ly dashboard. + $referrer_type_keys = array( 'social', 'search', 'other', 'internal', 'direct' ); + foreach ( $referrer_type_keys as $key ) { + $result->$key = (object) array( 'views' => 0 ); + } + + // Set views and views totals. + foreach ( $response as $referrer_data ) { + /** + * Variable. + * + * @var int + */ + $current_views = $referrer_data->metrics->referrers_views ?? 0; + $total_referrer_views += $current_views; + + /** + * Variable. + * + * @var string + */ + $current_key = $referrer_data->type ?? ''; + if ( '' !== $current_key ) { + if ( ! isset( $result->$current_key->views ) ) { + $result->$current_key = (object) array( 'views' => 0 ); + } + + $result->$current_key->views += $current_views; + } + } + + // Add direct and total views to the object. + $result->direct->views = $this->total_views - $total_referrer_views; + $result->totals = (object) array( 'views' => $this->total_views ); + + // Remove referrer types without views. + foreach ( $referrer_type_keys as $key ) { + if ( 0 === $result->$key->views ) { + unset( $result->$key ); + } + } + + // Set percentage values and format numbers. + // @phpstan-ignore-next-line. + foreach ( $result as $key => $value ) { + // Set and format percentage values. + $result->{ $key }->viewsPercentage = $this->get_i18n_percentage( + absint( $value->views ), + $this->total_views + ); + + // Format views values. + $result->{ $key }->views = number_format_i18n( $result->{ $key }->views ); + } + + return $result; + } + + /** + * Generates the top referrers data. + * + * Returned object properties: + * - `views`: The number of views. + * - `viewPercentage`: The number of views as a percentage, compared to the + * total views of all referrer types. + * - `datasetViewsPercentage: The number of views as a percentage, compared + * to the total views of the current dataset. + * + * @since 3.6.0 + * @since 3.17.0 Moved from the `Referrers_Post_Detail_API_Proxy` class. + * + * @param int $limit The limit of returned referrers. + * @param array $response The response received by the proxy. + * @param int $direct_views The count of direct views. + * @return stdClass The generated data. + */ + private function generate_referrers_data( + int $limit, + array $response, + int $direct_views + ): stdClass { + $temp_views = array(); + $totals = 0; + $referrer_count = count( $response ); + + // Set views and views totals. + $loop_count = $referrer_count > $limit ? $limit : $referrer_count; + for ( $i = 0; $i < $loop_count; $i++ ) { + $data = $response[ $i ]; + + /** + * Variable. + * + * @var int + */ + $referrer_views = $data->metrics->referrers_views ?? 0; + $totals += $referrer_views; + if ( isset( $data->name ) ) { + $temp_views[ $data->name ] = $referrer_views; + } + } + + // If applicable, add the direct views. + if ( isset( $referrer_views ) && $direct_views >= $referrer_views ) { + $temp_views['direct'] = $direct_views; + $totals += $direct_views; + arsort( $temp_views ); + if ( count( $temp_views ) > $limit ) { + $totals -= array_pop( $temp_views ); + } + } + + // Convert temporary array to result object and add totals. + $result = new stdClass(); + foreach ( $temp_views as $key => $value ) { + $result->$key = (object) array( 'views' => $value ); + } + $result->totals = (object) array( 'views' => $totals ); + + // Set percentages values and format numbers. + // @phpstan-ignore-next-line. + foreach ( $result as $key => $value ) { + // Percentage against all referrer views, even those not included + // in the dataset due to the $limit argument. + $result->{ $key }->viewsPercentage = $this + ->get_i18n_percentage( absint( $value->views ), $this->total_views ); + + // Percentage against the current dataset that is limited due to the + // $limit argument. + $result->{ $key }->datasetViewsPercentage = $this + ->get_i18n_percentage( absint( $value->views ), $totals ); + + // Format views values. + $result->{ $key }->views = number_format_i18n( $result->{ $key }->views ); + } + + return $result; + } + + /** + * Returns the passed number compared to the passed total, in an + * internationalized percentage format. + * + * @since 3.6.0 + * @since 3.17.0 Moved from the `Referrers_Post_Detail_API_Proxy` class. + * + * @param int $number The number to be calculated as a percentage. + * @param int $total The total number to compare against. + * @return string|false The internationalized percentage or false on error. + */ + private function get_i18n_percentage( int $number, int $total ) { + if ( 0 === $total ) { + return false; + } + + return number_format_i18n( $number / $total * 100, 2 ); + } +} diff --git a/src/rest-api/stats/class-endpoint-posts.php b/src/rest-api/stats/class-endpoint-posts.php new file mode 100644 index 0000000000..cb97310771 --- /dev/null +++ b/src/rest-api/stats/class-endpoint-posts.php @@ -0,0 +1,249 @@ +analytics_posts_api = new Analytics_Posts_API( $this->parsely ); + } + + /** + * Returns the endpoint name. + * + * @since 3.17.0 + * + * @return string + */ + public function get_endpoint_name(): string { + return 'posts'; + } + + /** + * Registers the routes for the objects of the controller. + */ + public function register_routes(): void { + /** + * GET /posts + * Retrieves the top posts for the given period. + */ + $this->register_rest_route( + '/', + array( 'GET' ), + array( $this, 'get_posts' ), + array_merge( + array( + 'period_start' => array( + 'description' => 'The start of the period to query.', + 'type' => 'string', + 'required' => false, + ), + 'period_end' => array( + 'description' => 'The end of the period to query.', + 'type' => 'string', + 'required' => false, + ), + 'pub_date_start' => array( + 'description' => 'The start of the publication date range to query.', + 'type' => 'string', + 'required' => false, + ), + 'pub_date_end' => array( + 'description' => 'The end of the publication date range to query.', + 'type' => 'string', + 'required' => false, + ), + 'limit' => array( + 'description' => 'The number of posts to return.', + 'type' => 'integer', + 'required' => false, + 'default' => self::TOP_POSTS_DEFAULT_LIMIT, + ), + 'sort' => array( + 'description' => 'The sort order of the posts.', + 'type' => 'string', + 'enum' => self::SORT_METRICS, + 'default' => self::SORT_DEFAULT, + 'required' => false, + ), + 'page' => array( + 'description' => 'The page to fetch.', + 'type' => 'integer', + 'required' => false, + 'default' => 1, + ), + 'author' => array( + 'description' => 'The author to filter by.', + 'type' => 'array', + 'items' => array( + 'type' => 'string', + ), + 'required' => false, + 'maxItems' => 5, + ), + 'section' => array( + 'description' => 'The section to filter by.', + 'type' => 'string', + 'required' => false, + ), + 'tag' => array( + 'description' => 'The tag to filter by.', + 'type' => 'array', + 'items' => array( + 'type' => 'string', + ), + 'required' => false, + 'maxItems' => 5, + ), + 'segment' => array( + 'description' => 'The segment to filter by.', + 'type' => 'string', + 'required' => false, + ), + ), + $this->get_itm_source_param_args() + ) + ); + } + + /** + * API Endpoint: GET /stats/posts + * + * Retrieves the top posts for the given query parameters. + * + * @param WP_REST_Request $request The request. + * + * @return stdClass[]|WP_Error|WP_REST_Response + */ + public function get_posts( WP_REST_Request $request ) { + $params = $request->get_params(); + + // Setup the itm_source if it is provided. + $this->set_itm_source_from_request( $request ); + + // TODO: Needed before the Public API refactor. + // Convert array of authors to a string with the first element. + if ( isset( $params['author'] ) && is_array( $params['author'] ) ) { + $params['author'] = $params['author'][0]; + } + // Convert array of tags to a string with the first element. + if ( isset( $params['tag'] ) && is_array( $params['tag'] ) ) { + $params['tag'] = $params['tag'][0]; + } + // TODO END. + + // Do the analytics request. + /** + * The raw analytics data. + * + * @var stdClass[]|WP_Error $analytics_request + */ + $analytics_request = $this->analytics_posts_api->get_items( + array( + 'period_start' => $params['period_start'] ?? null, + 'period_end' => $params['period_end'] ?? null, + 'pub_date_start' => $params['pub_date_start'] ?? null, + 'pub_date_end' => $params['pub_date_end'] ?? null, + 'limit' => $params['limit'] ?? self::TOP_POSTS_DEFAULT_LIMIT, + 'sort' => $params['sort'] ?? self::SORT_DEFAULT, + 'page' => $params['page'] ?? 1, + 'author' => $params['author'] ?? null, + 'section' => $params['section'] ?? null, + 'tag' => $params['tag'] ?? null, + 'segment' => $params['segment'] ?? null, + 'itm_source' => $params['itm_source'] ?? null, + ) + ); + + if ( is_wp_error( $analytics_request ) ) { + return $analytics_request; + } + + // Process the data. + $posts = array(); + foreach ( $analytics_request as $item ) { + $posts[] = $this->extract_post_data( $item ); + } + + $response = array( + 'params' => $params, + 'data' => $posts, + ); + + return new WP_REST_Response( $response, 200 ); + } +} diff --git a/src/rest-api/stats/class-endpoint-related.php b/src/rest-api/stats/class-endpoint-related.php new file mode 100644 index 0000000000..d675e9e923 --- /dev/null +++ b/src/rest-api/stats/class-endpoint-related.php @@ -0,0 +1,112 @@ +related_posts_api = new Related_API( $this->parsely ); + } + + /** + * Returns the endpoint name. + * + * @since 3.17.0 + * + * @return string + */ + public function get_endpoint_name(): string { + return 'related'; + } + + /** + * Registers the routes for the endpoint. + * + * @since 3.17.0 + */ + public function register_routes(): void { + /** + * GET /related + * Gets related posts. + */ + $this->register_rest_route( + '/', + array( 'GET' ), + array( $this, 'get_related_posts' ), + array( + 'url' => array( + 'description' => __( 'The URL of the post.', 'wp-parsely' ), + 'type' => 'string', + 'required' => true, + ), + $this->get_related_posts_param_args(), + ) + ); + } + + /** + * API Endpoint: GET /stats/related + * + * Gets related posts for a given URL. + * + * @since 3.17.0 + * + * @param WP_REST_Request $request The request object. + * @return WP_REST_Response|WP_Error + */ + public function get_related_posts( WP_REST_Request $request ) { + $url = $request->get_param( 'url' ); + + $related_posts = $this->get_related_posts_of_url( $request, $url ); + + if ( is_wp_error( $related_posts ) ) { + return $related_posts; + } + + return new WP_REST_Response( array( 'data' => $related_posts ), 200 ); + } + + /** + * Returns whether the endpoint is available for access by the current + * user. + * + * @since 3.17.0 + * + * @param WP_REST_Request|null $request The request object. + * @return bool + */ + public function is_available_to_current_user( ?WP_REST_Request $request = null ): bool { + return true; + } +} diff --git a/src/rest-api/stats/class-stats-controller.php b/src/rest-api/stats/class-stats-controller.php new file mode 100644 index 0000000000..ecae2387be --- /dev/null +++ b/src/rest-api/stats/class-stats-controller.php @@ -0,0 +1,48 @@ +register_endpoints( $endpoints ); + } +} diff --git a/src/rest-api/stats/trait-post-data.php b/src/rest-api/stats/trait-post-data.php new file mode 100644 index 0000000000..4698d2a9a4 --- /dev/null +++ b/src/rest-api/stats/trait-post-data.php @@ -0,0 +1,145 @@ +itm_source = $source; + } + + /** + * Sets the itm_source value from the request, if it exists. + * + * @since 3.17.0 + * + * @param WP_REST_Request $request The request object. + */ + private function set_itm_source_from_request( WP_REST_Request $request ): void { + $source = $request->get_param( 'itm_source' ); + if ( null !== $source ) { + $this->set_itm_source( $source ); + } + } + + /** + * Returns the itm_source parameter arguments, to be used in the REST API + * route registration. + * + * @since 3.17.0 + * + * @return array + */ + private function get_itm_source_param_args(): array { + return array( + 'itm_source' => array( + 'description' => __( 'The source of the item.', 'wp-parsely' ), + 'type' => 'string', + 'required' => false, + 'validate_callback' => array( $this, 'validate_itm_source' ), + ), + ); + } + + /** + * Extracts the post data from the passed object. + * + * Should only be used with endpoints that return post data. + * + * @since 3.10.0 + * @since 3.17.0 Moved from the old `Base_API_Proxy` class. + * + * @param stdClass $item The object to extract the data from. + * @return array The extracted data. + */ + protected function extract_post_data( stdClass $item ): array { + $data = array(); + + if ( isset( $item->author ) ) { + $data['author'] = $item->author; + } + + if ( isset( $item->metrics->views ) ) { + $data['views'] = number_format_i18n( $item->metrics->views ); + } + + if ( isset( $item->metrics->visitors ) ) { + $data['visitors'] = number_format_i18n( $item->metrics->visitors ); + } + + // The avg_engaged metric can be in different locations depending on the + // endpoint and passed sort/url parameters. + $avg_engaged = $item->metrics->avg_engaged ?? $item->avg_engaged ?? null; + if ( null !== $avg_engaged ) { + $data['avgEngaged'] = Utils::get_formatted_duration( (float) $avg_engaged ); + } + + if ( isset( $item->pub_date ) ) { + $data['date'] = wp_date( Utils::get_date_format(), strtotime( $item->pub_date ) ); + } + + if ( isset( $item->title ) ) { + $data['title'] = $item->title; + } + + if ( isset( $item->url ) ) { + $site_id = $this->parsely->get_site_id(); + // phpcs:ignore WordPressVIPMinimum.Functions.RestrictedFunctions.url_to_postid_url_to_postid + $post_id = url_to_postid( $item->url ); // 0 if the post cannot be found. + + $post_url = Parsely::get_url_with_itm_source( $item->url, null ); + if ( Utils::parsely_is_https_supported() ) { + $post_url = str_replace( 'http://', 'https://', $post_url ); + } + + $data['rawUrl'] = $post_url; + $data['dashUrl'] = Parsely::get_dash_url( $site_id, $post_url ); + $data['id'] = Parsely::get_url_with_itm_source( $post_url, null ); // Unique. + $data['postId'] = $post_id; // Might not be unique. + $data['url'] = Parsely::get_url_with_itm_source( $post_url, $this->itm_source ); + + // Set thumbnail URL, falling back to the Parse.ly thumbnail if needed. + $thumbnail_url = get_the_post_thumbnail_url( $post_id, 'thumbnail' ); + if ( false !== $thumbnail_url ) { + $data['thumbnailUrl'] = $thumbnail_url; + } elseif ( isset( $item->thumb_url_medium ) ) { + $data['thumbnailUrl'] = $item->thumb_url_medium; + } + } + + return $data; + } +} diff --git a/src/rest-api/stats/trait-related-posts.php b/src/rest-api/stats/trait-related-posts.php new file mode 100644 index 0000000000..dadab370a3 --- /dev/null +++ b/src/rest-api/stats/trait-related-posts.php @@ -0,0 +1,165 @@ + + */ + private function get_related_posts_param_args(): array { + return array_merge( + array( + 'sort' => array( + 'description' => __( 'The sort order.', 'wp-parsely' ), + 'type' => 'string', + 'enum' => array( '_score', 'pub_date' ), + 'required' => false, + 'default' => '_score', + ), + 'limit' => array( + 'description' => __( 'The number of related posts to return.', 'wp-parsely' ), + 'type' => 'integer', + 'required' => false, + 'default' => 10, + ), + 'pub_date_start' => array( + 'description' => __( 'The start of the publication date.', 'wp-parsely' ), + 'type' => 'string', + 'required' => false, + ), + 'pub_date_end' => array( + 'description' => __( 'The end of the publication date.', 'wp-parsely' ), + 'type' => 'string', + 'required' => false, + ), + 'page' => array( + 'description' => __( 'The page number.', 'wp-parsely' ), + 'type' => 'integer', + 'required' => false, + 'default' => 1, + ), + 'section' => array( + 'description' => __( 'The section of the post.', 'wp-parsely' ), + 'type' => 'string', + 'required' => false, + ), + 'tag' => array( + 'description' => __( 'The tag of the post.', 'wp-parsely' ), + 'type' => 'string', + 'required' => false, + ), + 'author' => array( + 'description' => __( 'The author of the post.', 'wp-parsely' ), + 'type' => 'string', + 'required' => false, + ), + ), + $this->get_itm_source_param_args() + ); + } + + /** + * Get related posts for a given URL. + * + * @since 3.17.0 + * + * @param WP_REST_Request $request The request object. + * @param string $url The URL to get related posts for. + * @return array|WP_Error + */ + public function get_related_posts_of_url( WP_REST_Request $request, string $url ) { + // Set the itm_source parameter. + $this->set_itm_source_from_request( $request ); + + // Get the data from the API. + /** + * The related posts request. + * + * @var array|WP_Error $related_posts_request + */ + $related_posts_request = $this->related_posts_api->get_items( + array( + 'url' => $url, + 'sort' => $request->get_param( 'sort' ), + 'limit' => $request->get_param( 'limit' ), + 'pub_date_start' => $request->get_param( 'pub_date_start' ), + 'pub_date_end' => $request->get_param( 'pub_date_end' ), + 'page' => $request->get_param( 'page' ), + 'section' => $request->get_param( 'section' ), + 'tag' => $request->get_param( 'tag' ), + 'author' => $request->get_param( 'author' ), + ) + ); + + if ( is_wp_error( $related_posts_request ) ) { + return $related_posts_request; + } + + $itm_source = $this->itm_source; + + $related_posts = array_map( + static function ( stdClass $item ) use ( $itm_source ) { + return (object) array( + 'image_url' => $item->image_url, + 'thumb_url_medium' => $item->thumb_url_medium, + 'title' => $item->title, + 'url' => Parsely::get_url_with_itm_source( $item->url, $itm_source ), + ); + }, + $related_posts_request + ); + + return $related_posts; + } +} diff --git a/src/rest-api/trait-use-post-id-parameter.php b/src/rest-api/trait-use-post-id-parameter.php new file mode 100644 index 0000000000..4459636e90 --- /dev/null +++ b/src/rest-api/trait-use-post-id-parameter.php @@ -0,0 +1,88 @@ + $methods The HTTP methods. + * @param callable $callback The callback function. + * @param array $args The route arguments. + */ + public function register_rest_route_with_post_id( + string $route, + array $methods, + callable $callback, + array $args = array() + ): void { + // Append the post_id parameter to the route. + $route = '/(?P\d+)/' . trim( $route, '/' ); + + // Add the post_id parameter to the args. + $args = array_merge( + $args, + array( + 'post_id' => array( + 'description' => __( 'The ID of the post.', 'wp-parsely' ), + 'type' => 'integer', + 'required' => true, + 'validate_callback' => array( $this, 'validate_post_id' ), + ), + ) + ); + + // Register the route. + $this->register_rest_route( $route, $methods, $callback, $args ); + } + + /** + * Validates the post ID parameter. + * + * The callback sets the post object in the request object if the parameter is valid. + * + * @since 3.16.0 + * @access private + * + * @param string $param The parameter value. + * @param WP_REST_Request $request The request object. + * @return bool Whether the parameter is valid. + */ + public function validate_post_id( string $param, WP_REST_Request $request ): bool { + if ( ! is_numeric( $param ) ) { + return false; + } + + $param = filter_var( $param, FILTER_VALIDATE_INT ); + + if ( false === $param ) { + return false; + } + + // Validate if the post ID exists. + $post = get_post( $param ); + // Set the post attribute in the request. + $request->set_param( 'post', $post ); + + return null !== $post; + } +} From f1bb531532ad1f5c22275f2567482ff9933c49d6 Mon Sep 17 00:00:00 2001 From: Henrique Mouta Date: Thu, 29 Aug 2024 11:56:38 +0100 Subject: [PATCH 12/49] Update the UI to use the new API endpoints --- build/blocks/recommendations/edit.asset.php | 2 +- build/blocks/recommendations/edit.js | 2 +- build/blocks/recommendations/view.asset.php | 2 +- build/blocks/recommendations/view.js | 2 +- .../content-helper/dashboard-widget.asset.php | 2 +- build/content-helper/dashboard-widget.js | 2 +- build/content-helper/editor-sidebar.asset.php | 2 +- build/content-helper/editor-sidebar.js | 8 ++--- .../parsely-recommendations-fetcher.tsx | 2 +- .../dashboard-widget/provider.ts | 2 +- .../performance-stats/provider.ts | 36 +++++++++---------- .../editor-sidebar/related-posts/provider.ts | 2 +- 12 files changed, 31 insertions(+), 33 deletions(-) diff --git a/build/blocks/recommendations/edit.asset.php b/build/blocks/recommendations/edit.asset.php index 0f36c73fce..ce1f1eb731 100644 --- a/build/blocks/recommendations/edit.asset.php +++ b/build/blocks/recommendations/edit.asset.php @@ -1 +1 @@ - array('react', 'wp-api-fetch', 'wp-block-editor', 'wp-blocks', 'wp-components', 'wp-compose', 'wp-element', 'wp-i18n', 'wp-url'), 'version' => '393b4c8be66f3bde527e'); + array('react', 'wp-api-fetch', 'wp-block-editor', 'wp-blocks', 'wp-components', 'wp-compose', 'wp-element', 'wp-i18n', 'wp-url'), 'version' => 'f1121e53d4c8acf98db5'); diff --git a/build/blocks/recommendations/edit.js b/build/blocks/recommendations/edit.js index 06593e46cf..c923405cf3 100644 --- a/build/blocks/recommendations/edit.js +++ b/build/blocks/recommendations/edit.js @@ -1 +1 @@ -!function(){"use strict";var e,n={271:function(e,n,r){var t,o,a=r(848),i=window.wp.blockEditor,l=window.wp.blocks,s=window.wp.i18n,c=window.wp.components,u=JSON.parse('{"UU":"wp-parsely/recommendations","uK":{"imagestyle":{"type":"string","default":"original"},"limit":{"type":"number","default":3},"openlinksinnewtab":{"type":"boolean","default":false},"showimages":{"type":"boolean","default":true},"sort":{"type":"string","default":"score"},"title":{"type":"string","default":"Related Content"}}}'),d=window.wp.element;(o=t||(t={}))[o.Error=0]="Error",o[o.Loaded=1]="Loaded",o[o.Recommendations=2]="Recommendations";var p=function(){return p=Object.assign||function(e){for(var n,r=1,t=arguments.length;r0&&o[o.length-1])||6!==l[0]&&2!==l[0])){i=0;continue}if(3===l[0]&&(!o||l[1]>o[0]&&l[1]=a)&&Object.keys(t.O).every((function(e){return t.O[e](r[s])}))?r.splice(s--,1):(l=!1,a0&&e[u-1][2]>a;u--)e[u]=e[u-1];e[u]=[r,o,a]},t.n=function(e){var n=e&&e.__esModule?function(){return e.default}:function(){return e};return t.d(n,{a:n}),n},t.d=function(e,n){for(var r in n)t.o(n,r)&&!t.o(e,r)&&Object.defineProperty(e,r,{enumerable:!0,get:n[r]})},t.o=function(e,n){return Object.prototype.hasOwnProperty.call(e,n)},function(){var e={335:0,203:0};t.O.j=function(n){return 0===e[n]};var n=function(n,r){var o,a,i=r[0],l=r[1],s=r[2],c=0;if(i.some((function(n){return 0!==e[n]}))){for(o in l)t.o(l,o)&&(t.m[o]=l[o]);if(s)var u=s(t)}for(n&&n(r);c0&&o[o.length-1])||6!==l[0]&&2!==l[0])){i=0;continue}if(3===l[0]&&(!o||l[1]>o[0]&&l[1]=a)&&Object.keys(t.O).every((function(e){return t.O[e](r[s])}))?r.splice(s--,1):(l=!1,a0&&e[u-1][2]>a;u--)e[u]=e[u-1];e[u]=[r,o,a]},t.n=function(e){var n=e&&e.__esModule?function(){return e.default}:function(){return e};return t.d(n,{a:n}),n},t.d=function(e,n){for(var r in n)t.o(n,r)&&!t.o(e,r)&&Object.defineProperty(e,r,{enumerable:!0,get:n[r]})},t.o=function(e,n){return Object.prototype.hasOwnProperty.call(e,n)},function(){var e={335:0,203:0};t.O.j=function(n){return 0===e[n]};var n=function(n,r){var o,a,i=r[0],l=r[1],s=r[2],c=0;if(i.some((function(n){return 0!==e[n]}))){for(o in l)t.o(l,o)&&(t.m[o]=l[o]);if(s)var u=s(t)}for(n&&n(r);c array('react', 'wp-api-fetch', 'wp-components', 'wp-compose', 'wp-dom-ready', 'wp-element', 'wp-i18n', 'wp-url'), 'version' => '827c1c0c5b711b046baf'); + array('react', 'wp-api-fetch', 'wp-components', 'wp-compose', 'wp-dom-ready', 'wp-element', 'wp-i18n', 'wp-url'), 'version' => 'af6eb0946975f32d53b1'); diff --git a/build/blocks/recommendations/view.js b/build/blocks/recommendations/view.js index ac32818be9..d8f4b45779 100644 --- a/build/blocks/recommendations/view.js +++ b/build/blocks/recommendations/view.js @@ -1 +1 @@ -!function(){"use strict";var e={20:function(e,r,n){var t=n(609),o=Symbol.for("react.element"),a=Symbol.for("react.fragment"),i=Object.prototype.hasOwnProperty,s=t.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED.ReactCurrentOwner,l={key:!0,ref:!0,__self:!0,__source:!0};function u(e,r,n){var t,a={},u=null,c=null;for(t in void 0!==n&&(u=""+n),void 0!==r.key&&(u=""+r.key),void 0!==r.ref&&(c=r.ref),r)i.call(r,t)&&!l.hasOwnProperty(t)&&(a[t]=r[t]);if(e&&e.defaultProps)for(t in r=e.defaultProps)void 0===a[t]&&(a[t]=r[t]);return{$$typeof:o,type:e,key:u,ref:c,props:a,_owner:s.current}}r.Fragment=a,r.jsx=u,r.jsxs=u},848:function(e,r,n){e.exports=n(20)},609:function(e){e.exports=window.React}},r={};function n(t){var o=r[t];if(void 0!==o)return o.exports;var a=r[t]={exports:{}};return e[t](a,a.exports,n),a.exports}n.n=function(e){var r=e&&e.__esModule?function(){return e.default}:function(){return e};return n.d(r,{a:r}),r},n.d=function(e,r){for(var t in r)n.o(r,t)&&!n.o(e,t)&&Object.defineProperty(e,t,{enumerable:!0,get:r[t]})},n.o=function(e,r){return Object.prototype.hasOwnProperty.call(e,r)},function(){var e,r,t=n(848),o=n(609),a=window.wp.domReady,i=n.n(a),s=window.wp.element,l=window.wp.i18n;(r=e||(e={}))[r.Error=0]="Error",r[r.Loaded=1]="Loaded",r[r.Recommendations=2]="Recommendations";var u=function(){return u=Object.assign||function(e){for(var r,n=1,t=arguments.length;n0&&o[o.length-1])||6!==s[0]&&2!==s[0])){i=0;continue}if(3===s[0]&&(!o||s[1]>o[0]&&s[1]0&&o[o.length-1])||6!==s[0]&&2!==s[0])){i=0;continue}if(3===s[0]&&(!o||s[1]>o[0]&&s[1] array('react', 'wp-api-fetch', 'wp-components', 'wp-data', 'wp-element', 'wp-i18n', 'wp-url'), 'version' => 'a741adf6df6b723b2f3d'); + array('react', 'wp-api-fetch', 'wp-components', 'wp-data', 'wp-element', 'wp-i18n', 'wp-url'), 'version' => 'd852566173ef137708b0'); diff --git a/build/content-helper/dashboard-widget.js b/build/content-helper/dashboard-widget.js index 50392fc4be..7b17d4192c 100644 --- a/build/content-helper/dashboard-widget.js +++ b/build/content-helper/dashboard-widget.js @@ -1 +1 @@ -!function(){"use strict";var e={20:function(e,t,r){var n=r(609),a=Symbol.for("react.element"),s=Symbol.for("react.fragment"),o=Object.prototype.hasOwnProperty,i=n.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED.ReactCurrentOwner,l={key:!0,ref:!0,__self:!0,__source:!0};function c(e,t,r){var n,s={},c=null,u=null;for(n in void 0!==r&&(c=""+r),void 0!==t.key&&(c=""+t.key),void 0!==t.ref&&(u=t.ref),t)o.call(t,n)&&!l.hasOwnProperty(n)&&(s[n]=t[n]);if(e&&e.defaultProps)for(n in t=e.defaultProps)void 0===s[n]&&(s[n]=t[n]);return{$$typeof:a,type:e,key:c,ref:u,props:s,_owner:i.current}}t.Fragment=s,t.jsx=c,t.jsxs=c},848:function(e,t,r){e.exports=r(20)},609:function(e){e.exports=window.React}},t={};function r(n){var a=t[n];if(void 0!==a)return a.exports;var s=t[n]={exports:{}};return e[n](s,s.exports,r),s.exports}r.n=function(e){var t=e&&e.__esModule?function(){return e.default}:function(){return e};return r.d(t,{a:t}),t},r.d=function(e,t){for(var n in t)r.o(t,n)&&!r.o(e,n)&&Object.defineProperty(e,n,{enumerable:!0,get:t[n]})},r.o=function(e,t){return Object.prototype.hasOwnProperty.call(e,t)},function(){var e,t,n,a=r(848),s=window.wp.element,o=window.wp.i18n,i=function(e){void 0===e&&(e=null);var t="";(null==e?void 0:e.children)&&(t=e.children);var r="content-helper-error-message";return(null==e?void 0:e.className)&&(r+=" "+e.className),(0,a.jsx)("div",{className:r,"data-testid":null==e?void 0:e.testId,dangerouslySetInnerHTML:{__html:t}})},l=function(e){var t;return void 0===e&&(e=null),(0,a.jsx)(i,{className:null==e?void 0:e.className,testId:"empty-credentials-message",children:null!==(t=window.wpParselyEmptyCredentialsMessage)&&void 0!==t?t:(0,o.__)("Please ensure that the Site ID and API Secret given in the plugin's settings are correct.","wp-parsely")})},c=function(){return c=Object.assign||function(e){for(var t,r=1,n=arguments.length;r0&&a[a.length-1])||6!==i[0]&&2!==i[0])){o=0;continue}if(3===i[0]&&(!a||i[1]>a[0]&&i[1]=1e4&&(clearInterval(s),r("Telemetry library not loaded"))}),100);else r("Telemetry not enabled")}))},e.prototype.trackEvent=function(t,r){var n;this.isLoaded?(0!==t.indexOf(e.TRACKS_PREFIX)&&(t=e.TRACKS_PREFIX+t),this.isEventNameValid(t)?(r=this.prepareProperties(r),null===(n=this._tkq)||void 0===n||n.push(["recordEvent",t,r])):console.error("Error tracking event: Invalid event name")):console.error("Error tracking event: Telemetry not loaded")},e.prototype.isTelemetryEnabled=function(){return this.isEnabled},e.prototype.isProprietyValid=function(t){return e.PROPERTY_REGEX.test(t)},e.prototype.isEventNameValid=function(t){return e.EVENT_NAME_REGEX.test(t)},e.prototype.prepareProperties=function(e){return(e=this.sanitizeProperties(e)).parsely_version=wpParselyTracksTelemetry.version,wpParselyTracksTelemetry.user&&(e._ut=wpParselyTracksTelemetry.user.type,e._ui=wpParselyTracksTelemetry.user.id),wpParselyTracksTelemetry.vipgo_env&&(e.vipgo_env=wpParselyTracksTelemetry.vipgo_env),this.sanitizeProperties(e)},e.prototype.sanitizeProperties=function(e){var t=this,r={};return Object.keys(e).forEach((function(n){t.isProprietyValid(n)&&(r[n]=e[n])})),r},e.TRACKS_PREFIX="wpparsely_",e.EVENT_NAME_REGEX=/^(([a-z0-9]+)_){2}([a-z0-9_]+)$/,e.PROPERTY_REGEX=/^[a-z_][a-z0-9_]*$/,e}(),h=(d.trackEvent,function(e){var t=e.defaultValue,r=e.items,n=e.onChange;return(0,a.jsx)("select",{onChange:n,value:t,children:r.map((function(e){return(0,a.jsx)("option",{value:e[0],children:e[1]},e[0])}))})}),f=window.wp.data,y=function(){return y=Object.assign||function(e){for(var t,r=1,n=arguments.length;rhere.',"wp-parsely"):s.code===j.ParselySuggestionsApiOpenAiError||s.code===j.ParselySuggestionsApiOpenAiUnavailable?s.message=(0,o.__)("The Parse.ly API returned an internal server error. Please retry with a different input, or try again later.","wp-parsely"):s.code===j.HttpRequestFailed&&s.message.includes("cURL error 28")?s.message=(0,o.__)("The Parse.ly API did not respond in a timely manner. Please try again later.","wp-parsely"):s.code===j.ParselySuggestionsApiSchemaError?s.message=(0,o.__)("The Parse.ly API returned a validation error. Please try again with different parameters.","wp-parsely"):s.code===j.ParselySuggestionsApiNoData?s.message=(0,o.__)("The Parse.ly API couldn't find any relevant data to fulfill the request. Please retry with a different input.","wp-parsely"):s.code===j.ParselySuggestionsApiOpenAiSchema?s.message=(0,o.__)("The Parse.ly API returned an incorrect response. Please try again later.","wp-parsely"):s.code===j.ParselySuggestionsApiAuthUnavailable&&(s.message=(0,o.__)("The Parse.ly API is currently unavailable. Please try again later.","wp-parsely")),s}return C(t,e),t.prototype.Message=function(e){return void 0===e&&(e=null),[j.PluginCredentialsNotSetMessageDetected,j.PluginSettingsSiteIdNotSet,j.PluginSettingsApiSecretNotSet].includes(this.code)?l(e):(this.code===j.FetchError&&(this.hint=this.Hint((0,o.__)("This error can sometimes be caused by ad-blockers or browser tracking protections. Please add this site to any applicable allow lists and try again.","wp-parsely"))),this.code!==j.ParselyApiForbidden&&this.code!==j.ParselySuggestionsApiNoAuthentication||(this.hint=this.Hint((0,o.__)("Please ensure that the Site ID and API Secret given in the plugin's settings are correct.","wp-parsely"))),this.code===j.HttpRequestFailed&&(this.hint=this.Hint((0,o.__)("The Parse.ly API cannot be reached. Please verify that you are online.","wp-parsely"))),(0,a.jsx)(i,{className:null==e?void 0:e.className,testId:"error",children:"

".concat(this.message,"

").concat(this.hint?this.hint:"")}))},t.prototype.Hint=function(e){return'

'.concat((0,o.__)("Hint:","wp-parsely")," ").concat(e,"

")},t.prototype.createErrorSnackbar=function(){//.test(this.message)||(0,f.dispatch)("core/notices").createNotice("error",this.message,{type:"snackbar"})},t}(Error),O=function(){function e(){this.abortControllers=new Map}return e.prototype.cancelRequest=function(e){if(e)(t=this.abortControllers.get(e))&&(t.abort(),this.abortControllers.delete(e));else{var t,r=Array.from(this.abortControllers.keys()).pop();r&&(t=this.abortControllers.get(r))&&(t.abort(),this.abortControllers.delete(r))}},e.prototype.cancelAll=function(){this.abortControllers.forEach((function(e){return e.abort()})),this.abortControllers.clear()},e.prototype.getOrCreateController=function(e){if(e&&this.abortControllers.has(e))return{abortController:this.abortControllers.get(e),abortId:e};var t=null!=e?e:"auto-"+Date.now(),r=new AbortController;return this.abortControllers.set(t,r),{abortController:r,abortId:t}},e.prototype.fetch=function(e,t){return r=this,n=void 0,s=function(){var r,n,a,s,i,l;return function(e,t){var r,n,a,s,o={label:0,sent:function(){if(1&a[0])throw a[1];return a[1]},trys:[],ops:[]};return s={next:i(0),throw:i(1),return:i(2)},"function"==typeof Symbol&&(s[Symbol.iterator]=function(){return this}),s;function i(i){return function(l){return function(i){if(r)throw new TypeError("Generator is already executing.");for(;s&&(s=0,i[0]&&(o=0)),o;)try{if(r=1,n&&(a=2&i[0]?n.return:i[0]?n.throw||((a=n.return)&&a.call(n),0):n.next)&&!(a=a.call(n,i[1])).done)return a;switch(n=0,a&&(i=[2&i[0],a.value]),i[0]){case 0:case 1:a=i;break;case 4:return o.label++,{value:i[1],done:!1};case 5:o.label++,n=i[1],i=[0];continue;case 7:i=o.ops.pop(),o.trys.pop();continue;default:if(!((a=(a=o.trys).length>0&&a[a.length-1])||6!==i[0]&&2!==i[0])){o=0;continue}if(3===i[0]&&(!a||i[1]>a[0]&&i[1]0&&a[a.length-1])||6!==i[0]&&2!==i[0])){o=0;continue}if(3===i[0]&&(!a||i[1]>a[0]&&i[1]=c){var u=t;(a=n/c)%1>1/i&&(u=a>10?1:2),u=parseFloat(a.toFixed(2))===parseFloat(a.toFixed(0))?0:u,s=a.toFixed(u),o=l}i=c})),s+r+o}function z(e){var t=e.metric,r=e.post,n=e.avgEngagedIcon,s=e.viewsIcon;return"views"===t?(0,a.jsxs)("span",{className:"parsely-post-metric-data",children:[(0,a.jsx)("span",{className:"screen-reader-text",children:(0,o.__)("Number of Views","wp-parsely")}),s,$(r.views.toString())]}):"avg_engaged"===t?(0,a.jsxs)("span",{className:"parsely-post-metric-data",children:[(0,a.jsx)("span",{className:"screen-reader-text",children:(0,o.__)("Average Time","wp-parsely")}),n,r.avgEngaged]}):(0,a.jsx)("span",{className:"parsely-post-metric-data",children:"-"})}function X(e){var t,r=e.metric,n=e.post;return(0,a.jsx)("li",{className:"parsely-top-post",children:(0,a.jsxs)("div",{className:"parsely-top-post-content",children:[(0,a.jsx)(B,{post:n}),(0,a.jsxs)("div",{className:"parsely-top-post-data",children:[(0,a.jsx)(z,{metric:r,post:n}),(0,a.jsx)(Z,{post:n}),(0,a.jsxs)("a",{className:"parsely-top-post-icon-link",href:n.url,target:"_blank",rel:"noreferrer",children:[(0,a.jsx)("span",{className:"screen-reader-text",children:(0,o.__)("View Post (opens in new tab)","wp-parsely")}),(0,a.jsx)(q,{})]}),0!==n.postId&&(0,a.jsxs)("a",{className:"parsely-top-post-icon-link",href:(t=n.postId,"/wp-admin/post.php?post=".concat(t,"&action=edit")),target:"_blank",rel:"noreferrer",children:[(0,a.jsx)("span",{className:"screen-reader-text",children:(0,o.__)("Edit Post (opens in new tab)","wp-parsely")}),(0,a.jsx)(G,{})]}),(0,a.jsxs)("div",{className:"parsely-top-post-metadata",children:[(0,a.jsxs)("span",{className:"parsely-top-post-date",children:[(0,a.jsx)("span",{className:"screen-reader-text",children:(0,o.__)("Date","wp-parsely")}),M(new Date(n.date))]}),(0,a.jsxs)("span",{className:"parsely-top-post-author",children:[(0,a.jsx)("span",{className:"screen-reader-text",children:(0,o.__)("Author","wp-parsely")}),n.author]})]})]})]})},n.id)}function B(e){var t=e.post;return t.thumbnailUrl?(0,a.jsxs)("div",{className:"parsely-top-post-thumbnail",children:[(0,a.jsx)("span",{className:"screen-reader-text",children:(0,o.__)("Thumbnail","wp-parsely")}),(0,a.jsx)("img",{src:t.thumbnailUrl,alt:(0,o.__)("Post thumbnail","wp-parsely")})]}):(0,a.jsx)("div",{className:"parsely-top-post-thumbnail",children:(0,a.jsx)("span",{className:"screen-reader-text",children:(0,o.__)("Post thumbnail not available","wp-parsely")})})}function Z(e){var t=e.post;return(0,a.jsxs)("a",{className:"parsely-top-post-title",href:t.dashUrl,target:"_blank",rel:"noreferrer",children:[(0,a.jsx)("span",{className:"screen-reader-text",children:(0,o.__)("View in Parse.ly (opens in new tab)","wp-parsely")}),t.title]})}var W=function(){return W=Object.assign||function(e){for(var t,r=1,n=arguments.length;r0&&a[a.length-1])||6!==i[0]&&2!==i[0])){o=0;continue}if(3===i[0]&&(!a||i[1]>a[0]&&i[1]0&&e.retryFetch?[4,new Promise((function(e){return setTimeout(e,500)}))]:[3,3];case 1:return r.sent(),[4,t(n-1)];case 2:return r.sent(),[3,4];case 3:f(!1),v(e),r.label=4;case 4:return[2]}}))}))})),[2]}))}))};return f(!0),t(1),function(){f(!1),m([]),v(void 0)}}),[i,S]);var j=function(e,t){d.trackEvent("dash_widget_filter_changed",W({filter:e},t))},N=(0,a.jsxs)("div",{className:"parsely-top-posts-filters",children:[(0,a.jsx)(h,{defaultValue:i.Period,items:Object.values(e).map((function(e){return[e,A(e)]})),onChange:function(t){x(t.target.value,e)&&(l({Period:t.target.value}),j("period",{period:t.target.value}),T(1))}}),(0,a.jsx)(h,{defaultValue:i.Metric,items:Object.values(t).map((function(e){return[e,E(e)]})),onChange:function(e){x(e.target.value,t)&&(l({Metric:e.target.value}),j("metric",{metric:e.target.value}),T(1))}})]}),C=(0,a.jsxs)("div",{className:"parsely-top-posts-navigation",children:[(0,a.jsx)("button",{className:"parsely-top-posts-navigation-prev",disabled:S<=1,"aria-label":(0,o.__)("Previous page","wp-parsely"),onClick:function(){T(S-1),d.trackEvent("dash_widget_navigation",{navigation:"previous",to_page:S-1})},children:(0,o.__)("<< Previous","wp-parsely")}),(0,o.sprintf)(/* translators: 1: Current page */ /* translators: 1: Current page */(0,o.__)("Page %1$d","wp-parsely"),S),(0,a.jsx)("button",{className:"parsely-top-posts-navigation-next",disabled:!u&&_.length<5,"aria-label":(0,o.__)("Next page","wp-parsely"),onClick:function(){T(S+1),d.trackEvent("dash_widget_navigation",{navigation:"next",to_page:S+1})},children:(0,o.__)("Next >>","wp-parsely")})]});if(g)return(0,a.jsxs)(a.Fragment,{children:[N,g.Message(),S>1&&C]});var k=(0,a.jsx)("div",{className:"parsely-spinner-wrapper",children:(0,a.jsx)(p.Spinner,{})});return(0,a.jsxs)(a.Fragment,{children:[N,u?k:(0,a.jsx)("ol",{className:"parsely-top-posts",style:{counterReset:"item "+5*(S-1)},children:_.map((function(e){return(0,a.jsx)(X,{metric:i.Metric,post:e},e.id)}))}),(_.length>=5||S>1)&&C]})}var J=function(r){var n;try{n=JSON.parse(r)}catch(r){return{Metric:t.Views,Period:e.Days7}}return x(null==n?void 0:n.Metric,t)||(n.Metric=t.Views),x(null==n?void 0:n.Period,e)||(n.Period=e.Days7),n};window.addEventListener("load",(function(){var e=document.querySelector("#wp-parsely-dashboard-widget > .inside");if(null!==e){var t=(0,a.jsx)(S,{endpoint:"dashboard-widget-settings",defaultSettings:J(window.wpParselyContentHelperSettings),children:(0,a.jsx)(u,{children:(0,a.jsx)(Q,{})})});s.createRoot?(0,s.createRoot)(e).render(t):(0,s.render)(t,e)}}),!1)}()}(); \ No newline at end of file +!function(){"use strict";var e={20:function(e,t,r){var n=r(609),a=Symbol.for("react.element"),s=Symbol.for("react.fragment"),o=Object.prototype.hasOwnProperty,i=n.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED.ReactCurrentOwner,l={key:!0,ref:!0,__self:!0,__source:!0};function c(e,t,r){var n,s={},c=null,u=null;for(n in void 0!==r&&(c=""+r),void 0!==t.key&&(c=""+t.key),void 0!==t.ref&&(u=t.ref),t)o.call(t,n)&&!l.hasOwnProperty(n)&&(s[n]=t[n]);if(e&&e.defaultProps)for(n in t=e.defaultProps)void 0===s[n]&&(s[n]=t[n]);return{$$typeof:a,type:e,key:c,ref:u,props:s,_owner:i.current}}t.Fragment=s,t.jsx=c,t.jsxs=c},848:function(e,t,r){e.exports=r(20)},609:function(e){e.exports=window.React}},t={};function r(n){var a=t[n];if(void 0!==a)return a.exports;var s=t[n]={exports:{}};return e[n](s,s.exports,r),s.exports}r.n=function(e){var t=e&&e.__esModule?function(){return e.default}:function(){return e};return r.d(t,{a:t}),t},r.d=function(e,t){for(var n in t)r.o(t,n)&&!r.o(e,n)&&Object.defineProperty(e,n,{enumerable:!0,get:t[n]})},r.o=function(e,t){return Object.prototype.hasOwnProperty.call(e,t)},function(){var e,t,n,a=r(848),s=window.wp.element,o=window.wp.i18n,i=function(e){void 0===e&&(e=null);var t="";(null==e?void 0:e.children)&&(t=e.children);var r="content-helper-error-message";return(null==e?void 0:e.className)&&(r+=" "+e.className),(0,a.jsx)("div",{className:r,"data-testid":null==e?void 0:e.testId,dangerouslySetInnerHTML:{__html:t}})},l=function(e){var t;return void 0===e&&(e=null),(0,a.jsx)(i,{className:null==e?void 0:e.className,testId:"empty-credentials-message",children:null!==(t=window.wpParselyEmptyCredentialsMessage)&&void 0!==t?t:(0,o.__)("Please ensure that the Site ID and API Secret given in the plugin's settings are correct.","wp-parsely")})},c=function(){return c=Object.assign||function(e){for(var t,r=1,n=arguments.length;r0&&a[a.length-1])||6!==i[0]&&2!==i[0])){o=0;continue}if(3===i[0]&&(!a||i[1]>a[0]&&i[1]=1e4&&(clearInterval(s),r("Telemetry library not loaded"))}),100);else r("Telemetry not enabled")}))},e.prototype.trackEvent=function(t,r){var n;this.isLoaded?(0!==t.indexOf(e.TRACKS_PREFIX)&&(t=e.TRACKS_PREFIX+t),this.isEventNameValid(t)?(r=this.prepareProperties(r),null===(n=this._tkq)||void 0===n||n.push(["recordEvent",t,r])):console.error("Error tracking event: Invalid event name")):console.error("Error tracking event: Telemetry not loaded")},e.prototype.isTelemetryEnabled=function(){return this.isEnabled},e.prototype.isProprietyValid=function(t){return e.PROPERTY_REGEX.test(t)},e.prototype.isEventNameValid=function(t){return e.EVENT_NAME_REGEX.test(t)},e.prototype.prepareProperties=function(e){return(e=this.sanitizeProperties(e)).parsely_version=wpParselyTracksTelemetry.version,wpParselyTracksTelemetry.user&&(e._ut=wpParselyTracksTelemetry.user.type,e._ui=wpParselyTracksTelemetry.user.id),wpParselyTracksTelemetry.vipgo_env&&(e.vipgo_env=wpParselyTracksTelemetry.vipgo_env),this.sanitizeProperties(e)},e.prototype.sanitizeProperties=function(e){var t=this,r={};return Object.keys(e).forEach((function(n){t.isProprietyValid(n)&&(r[n]=e[n])})),r},e.TRACKS_PREFIX="wpparsely_",e.EVENT_NAME_REGEX=/^(([a-z0-9]+)_){2}([a-z0-9_]+)$/,e.PROPERTY_REGEX=/^[a-z_][a-z0-9_]*$/,e}(),h=(d.trackEvent,function(e){var t=e.defaultValue,r=e.items,n=e.onChange;return(0,a.jsx)("select",{onChange:n,value:t,children:r.map((function(e){return(0,a.jsx)("option",{value:e[0],children:e[1]},e[0])}))})}),f=window.wp.data,y=function(){return y=Object.assign||function(e){for(var t,r=1,n=arguments.length;rhere.',"wp-parsely"):s.code===j.ParselySuggestionsApiOpenAiError||s.code===j.ParselySuggestionsApiOpenAiUnavailable?s.message=(0,o.__)("The Parse.ly API returned an internal server error. Please retry with a different input, or try again later.","wp-parsely"):s.code===j.HttpRequestFailed&&s.message.includes("cURL error 28")?s.message=(0,o.__)("The Parse.ly API did not respond in a timely manner. Please try again later.","wp-parsely"):s.code===j.ParselySuggestionsApiSchemaError?s.message=(0,o.__)("The Parse.ly API returned a validation error. Please try again with different parameters.","wp-parsely"):s.code===j.ParselySuggestionsApiNoData?s.message=(0,o.__)("The Parse.ly API couldn't find any relevant data to fulfill the request. Please retry with a different input.","wp-parsely"):s.code===j.ParselySuggestionsApiOpenAiSchema?s.message=(0,o.__)("The Parse.ly API returned an incorrect response. Please try again later.","wp-parsely"):s.code===j.ParselySuggestionsApiAuthUnavailable&&(s.message=(0,o.__)("The Parse.ly API is currently unavailable. Please try again later.","wp-parsely")),s}return C(t,e),t.prototype.Message=function(e){return void 0===e&&(e=null),[j.PluginCredentialsNotSetMessageDetected,j.PluginSettingsSiteIdNotSet,j.PluginSettingsApiSecretNotSet].includes(this.code)?l(e):(this.code===j.FetchError&&(this.hint=this.Hint((0,o.__)("This error can sometimes be caused by ad-blockers or browser tracking protections. Please add this site to any applicable allow lists and try again.","wp-parsely"))),this.code!==j.ParselyApiForbidden&&this.code!==j.ParselySuggestionsApiNoAuthentication||(this.hint=this.Hint((0,o.__)("Please ensure that the Site ID and API Secret given in the plugin's settings are correct.","wp-parsely"))),this.code===j.HttpRequestFailed&&(this.hint=this.Hint((0,o.__)("The Parse.ly API cannot be reached. Please verify that you are online.","wp-parsely"))),(0,a.jsx)(i,{className:null==e?void 0:e.className,testId:"error",children:"

".concat(this.message,"

").concat(this.hint?this.hint:"")}))},t.prototype.Hint=function(e){return'

'.concat((0,o.__)("Hint:","wp-parsely")," ").concat(e,"

")},t.prototype.createErrorSnackbar=function(){//.test(this.message)||(0,f.dispatch)("core/notices").createNotice("error",this.message,{type:"snackbar"})},t}(Error),O=function(){function e(){this.abortControllers=new Map}return e.prototype.cancelRequest=function(e){if(e)(t=this.abortControllers.get(e))&&(t.abort(),this.abortControllers.delete(e));else{var t,r=Array.from(this.abortControllers.keys()).pop();r&&(t=this.abortControllers.get(r))&&(t.abort(),this.abortControllers.delete(r))}},e.prototype.cancelAll=function(){this.abortControllers.forEach((function(e){return e.abort()})),this.abortControllers.clear()},e.prototype.getOrCreateController=function(e){if(e&&this.abortControllers.has(e))return{abortController:this.abortControllers.get(e),abortId:e};var t=null!=e?e:"auto-"+Date.now(),r=new AbortController;return this.abortControllers.set(t,r),{abortController:r,abortId:t}},e.prototype.fetch=function(e,t){return r=this,n=void 0,s=function(){var r,n,a,s,i,l;return function(e,t){var r,n,a,s,o={label:0,sent:function(){if(1&a[0])throw a[1];return a[1]},trys:[],ops:[]};return s={next:i(0),throw:i(1),return:i(2)},"function"==typeof Symbol&&(s[Symbol.iterator]=function(){return this}),s;function i(i){return function(l){return function(i){if(r)throw new TypeError("Generator is already executing.");for(;s&&(s=0,i[0]&&(o=0)),o;)try{if(r=1,n&&(a=2&i[0]?n.return:i[0]?n.throw||((a=n.return)&&a.call(n),0):n.next)&&!(a=a.call(n,i[1])).done)return a;switch(n=0,a&&(i=[2&i[0],a.value]),i[0]){case 0:case 1:a=i;break;case 4:return o.label++,{value:i[1],done:!1};case 5:o.label++,n=i[1],i=[0];continue;case 7:i=o.ops.pop(),o.trys.pop();continue;default:if(!((a=(a=o.trys).length>0&&a[a.length-1])||6!==i[0]&&2!==i[0])){o=0;continue}if(3===i[0]&&(!a||i[1]>a[0]&&i[1]0&&a[a.length-1])||6!==i[0]&&2!==i[0])){o=0;continue}if(3===i[0]&&(!a||i[1]>a[0]&&i[1]=c){var u=t;(a=n/c)%1>1/i&&(u=a>10?1:2),u=parseFloat(a.toFixed(2))===parseFloat(a.toFixed(0))?0:u,s=a.toFixed(u),o=l}i=c})),s+r+o}function z(e){var t=e.metric,r=e.post,n=e.avgEngagedIcon,s=e.viewsIcon;return"views"===t?(0,a.jsxs)("span",{className:"parsely-post-metric-data",children:[(0,a.jsx)("span",{className:"screen-reader-text",children:(0,o.__)("Number of Views","wp-parsely")}),s,$(r.views.toString())]}):"avg_engaged"===t?(0,a.jsxs)("span",{className:"parsely-post-metric-data",children:[(0,a.jsx)("span",{className:"screen-reader-text",children:(0,o.__)("Average Time","wp-parsely")}),n,r.avgEngaged]}):(0,a.jsx)("span",{className:"parsely-post-metric-data",children:"-"})}function X(e){var t,r=e.metric,n=e.post;return(0,a.jsx)("li",{className:"parsely-top-post",children:(0,a.jsxs)("div",{className:"parsely-top-post-content",children:[(0,a.jsx)(B,{post:n}),(0,a.jsxs)("div",{className:"parsely-top-post-data",children:[(0,a.jsx)(z,{metric:r,post:n}),(0,a.jsx)(Z,{post:n}),(0,a.jsxs)("a",{className:"parsely-top-post-icon-link",href:n.url,target:"_blank",rel:"noreferrer",children:[(0,a.jsx)("span",{className:"screen-reader-text",children:(0,o.__)("View Post (opens in new tab)","wp-parsely")}),(0,a.jsx)(q,{})]}),0!==n.postId&&(0,a.jsxs)("a",{className:"parsely-top-post-icon-link",href:(t=n.postId,"/wp-admin/post.php?post=".concat(t,"&action=edit")),target:"_blank",rel:"noreferrer",children:[(0,a.jsx)("span",{className:"screen-reader-text",children:(0,o.__)("Edit Post (opens in new tab)","wp-parsely")}),(0,a.jsx)(G,{})]}),(0,a.jsxs)("div",{className:"parsely-top-post-metadata",children:[(0,a.jsxs)("span",{className:"parsely-top-post-date",children:[(0,a.jsx)("span",{className:"screen-reader-text",children:(0,o.__)("Date","wp-parsely")}),M(new Date(n.date))]}),(0,a.jsxs)("span",{className:"parsely-top-post-author",children:[(0,a.jsx)("span",{className:"screen-reader-text",children:(0,o.__)("Author","wp-parsely")}),n.author]})]})]})]})},n.id)}function B(e){var t=e.post;return t.thumbnailUrl?(0,a.jsxs)("div",{className:"parsely-top-post-thumbnail",children:[(0,a.jsx)("span",{className:"screen-reader-text",children:(0,o.__)("Thumbnail","wp-parsely")}),(0,a.jsx)("img",{src:t.thumbnailUrl,alt:(0,o.__)("Post thumbnail","wp-parsely")})]}):(0,a.jsx)("div",{className:"parsely-top-post-thumbnail",children:(0,a.jsx)("span",{className:"screen-reader-text",children:(0,o.__)("Post thumbnail not available","wp-parsely")})})}function Z(e){var t=e.post;return(0,a.jsxs)("a",{className:"parsely-top-post-title",href:t.dashUrl,target:"_blank",rel:"noreferrer",children:[(0,a.jsx)("span",{className:"screen-reader-text",children:(0,o.__)("View in Parse.ly (opens in new tab)","wp-parsely")}),t.title]})}var W=function(){return W=Object.assign||function(e){for(var t,r=1,n=arguments.length;r0&&a[a.length-1])||6!==i[0]&&2!==i[0])){o=0;continue}if(3===i[0]&&(!a||i[1]>a[0]&&i[1]0&&e.retryFetch?[4,new Promise((function(e){return setTimeout(e,500)}))]:[3,3];case 1:return r.sent(),[4,t(n-1)];case 2:return r.sent(),[3,4];case 3:f(!1),v(e),r.label=4;case 4:return[2]}}))}))})),[2]}))}))};return f(!0),t(1),function(){f(!1),m([]),v(void 0)}}),[i,S]);var j=function(e,t){d.trackEvent("dash_widget_filter_changed",W({filter:e},t))},N=(0,a.jsxs)("div",{className:"parsely-top-posts-filters",children:[(0,a.jsx)(h,{defaultValue:i.Period,items:Object.values(e).map((function(e){return[e,A(e)]})),onChange:function(t){x(t.target.value,e)&&(l({Period:t.target.value}),j("period",{period:t.target.value}),T(1))}}),(0,a.jsx)(h,{defaultValue:i.Metric,items:Object.values(t).map((function(e){return[e,E(e)]})),onChange:function(e){x(e.target.value,t)&&(l({Metric:e.target.value}),j("metric",{metric:e.target.value}),T(1))}})]}),C=(0,a.jsxs)("div",{className:"parsely-top-posts-navigation",children:[(0,a.jsx)("button",{className:"parsely-top-posts-navigation-prev",disabled:S<=1,"aria-label":(0,o.__)("Previous page","wp-parsely"),onClick:function(){T(S-1),d.trackEvent("dash_widget_navigation",{navigation:"previous",to_page:S-1})},children:(0,o.__)("<< Previous","wp-parsely")}),(0,o.sprintf)(/* translators: 1: Current page */ /* translators: 1: Current page */(0,o.__)("Page %1$d","wp-parsely"),S),(0,a.jsx)("button",{className:"parsely-top-posts-navigation-next",disabled:!u&&_.length<5,"aria-label":(0,o.__)("Next page","wp-parsely"),onClick:function(){T(S+1),d.trackEvent("dash_widget_navigation",{navigation:"next",to_page:S+1})},children:(0,o.__)("Next >>","wp-parsely")})]});if(g)return(0,a.jsxs)(a.Fragment,{children:[N,g.Message(),S>1&&C]});var k=(0,a.jsx)("div",{className:"parsely-spinner-wrapper",children:(0,a.jsx)(p.Spinner,{})});return(0,a.jsxs)(a.Fragment,{children:[N,u?k:(0,a.jsx)("ol",{className:"parsely-top-posts",style:{counterReset:"item "+5*(S-1)},children:_.map((function(e){return(0,a.jsx)(X,{metric:i.Metric,post:e},e.id)}))}),(_.length>=5||S>1)&&C]})}var J=function(r){var n;try{n=JSON.parse(r)}catch(r){return{Metric:t.Views,Period:e.Days7}}return x(null==n?void 0:n.Metric,t)||(n.Metric=t.Views),x(null==n?void 0:n.Period,e)||(n.Period=e.Days7),n};window.addEventListener("load",(function(){var e=document.querySelector("#wp-parsely-dashboard-widget > .inside");if(null!==e){var t=(0,a.jsx)(S,{endpoint:"dashboard-widget-settings",defaultSettings:J(window.wpParselyContentHelperSettings),children:(0,a.jsx)(u,{children:(0,a.jsx)(Q,{})})});s.createRoot?(0,s.createRoot)(e).render(t):(0,s.render)(t,e)}}),!1)}()}(); \ No newline at end of file diff --git a/build/content-helper/editor-sidebar.asset.php b/build/content-helper/editor-sidebar.asset.php index 0a96a20139..a791b1c322 100644 --- a/build/content-helper/editor-sidebar.asset.php +++ b/build/content-helper/editor-sidebar.asset.php @@ -1 +1 @@ - array('react', 'wp-api-fetch', 'wp-block-editor', 'wp-blocks', 'wp-components', 'wp-compose', 'wp-core-data', 'wp-data', 'wp-dom-ready', 'wp-editor', 'wp-element', 'wp-hooks', 'wp-i18n', 'wp-plugins', 'wp-primitives', 'wp-url'), 'version' => 'ceb704108c34274732ed'); + array('react', 'wp-api-fetch', 'wp-block-editor', 'wp-blocks', 'wp-components', 'wp-compose', 'wp-core-data', 'wp-data', 'wp-dom-ready', 'wp-editor', 'wp-element', 'wp-hooks', 'wp-i18n', 'wp-plugins', 'wp-primitives', 'wp-url'), 'version' => 'eaad1ca5764f3fbdb9e6'); diff --git a/build/content-helper/editor-sidebar.js b/build/content-helper/editor-sidebar.js index 3036042520..697b66067d 100644 --- a/build/content-helper/editor-sidebar.js +++ b/build/content-helper/editor-sidebar.js @@ -9,11 +9,11 @@ /* translators: %s: percentage value */,{ /* translators: %s: percentage value */ text:"".concat(c(t)," - ").concat((0,_.sprintf)((0,_.__)("%s%%","wp-parsely"),n.viewsPercentage)),delay:150,children:(0,p.jsx)("div",{"aria-label":r,className:"bar-fill "+t,style:{width:n.viewsPercentage+"%"}})},t)}))}),(0,p.jsx)("div",{className:"percentage-bar-labels",children:Object.entries(t.referrers.types).map((function(e){var t=e[0],n=e[1];return(0,p.jsxs)("div",{className:"single-label "+t,children:[(0,p.jsx)("div",{className:"label-color "+t}),(0,p.jsx)("div",{className:"label-text",children:c(t)}),(0,p.jsx)("div",{className:"label-value",children:Lt(n.views)})]},t)}))})]})]})},Nt=(0,p.jsx)(x.SVG,{viewBox:"0 0 24 24",xmlns:"http://www.w3.org/2000/svg",children:(0,p.jsx)(x.Path,{d:"M3.99961 13C4.67043 13.3354 4.6703 13.3357 4.67017 13.3359L4.67298 13.3305C4.67621 13.3242 4.68184 13.3135 4.68988 13.2985C4.70595 13.2686 4.7316 13.2218 4.76695 13.1608C4.8377 13.0385 4.94692 12.8592 5.09541 12.6419C5.39312 12.2062 5.84436 11.624 6.45435 11.0431C7.67308 9.88241 9.49719 8.75 11.9996 8.75C14.502 8.75 16.3261 9.88241 17.5449 11.0431C18.1549 11.624 18.6061 12.2062 18.9038 12.6419C19.0523 12.8592 19.1615 13.0385 19.2323 13.1608C19.2676 13.2218 19.2933 13.2686 19.3093 13.2985C19.3174 13.3135 19.323 13.3242 19.3262 13.3305L19.3291 13.3359C19.3289 13.3357 19.3288 13.3354 19.9996 13C20.6704 12.6646 20.6703 12.6643 20.6701 12.664L20.6697 12.6632L20.6688 12.6614L20.6662 12.6563L20.6583 12.6408C20.6517 12.6282 20.6427 12.6108 20.631 12.5892C20.6078 12.5459 20.5744 12.4852 20.5306 12.4096C20.4432 12.2584 20.3141 12.0471 20.1423 11.7956C19.7994 11.2938 19.2819 10.626 18.5794 9.9569C17.1731 8.61759 14.9972 7.25 11.9996 7.25C9.00203 7.25 6.82614 8.61759 5.41987 9.9569C4.71736 10.626 4.19984 11.2938 3.85694 11.7956C3.68511 12.0471 3.55605 12.2584 3.4686 12.4096C3.42484 12.4852 3.39142 12.5459 3.36818 12.5892C3.35656 12.6108 3.34748 12.6282 3.34092 12.6408L3.33297 12.6563L3.33041 12.6614L3.32948 12.6632L3.32911 12.664C3.32894 12.6643 3.32879 12.6646 3.99961 13ZM11.9996 16C13.9326 16 15.4996 14.433 15.4996 12.5C15.4996 10.567 13.9326 9 11.9996 9C10.0666 9 8.49961 10.567 8.49961 12.5C8.49961 14.433 10.0666 16 11.9996 16Z"})}),Ct=(0,p.jsx)(x.SVG,{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 24 24",children:(0,p.jsx)(x.Path,{d:"M15.5 9.5a1 1 0 100-2 1 1 0 000 2zm0 1.5a2.5 2.5 0 100-5 2.5 2.5 0 000 5zm-2.25 6v-2a2.75 2.75 0 00-2.75-2.75h-4A2.75 2.75 0 003.75 15v2h1.5v-2c0-.69.56-1.25 1.25-1.25h4c.69 0 1.25.56 1.25 1.25v2h1.5zm7-2v2h-1.5v-2c0-.69-.56-1.25-1.25-1.25H15v-1.5h2.5A2.75 2.75 0 0120.25 15zM9.5 8.5a1 1 0 11-2 0 1 1 0 012 0zm1.5 0a2.5 2.5 0 11-5 0 2.5 2.5 0 015 0z",fillRule:"evenodd"})}),At=(0,p.jsx)(x.SVG,{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 24 24",children:(0,p.jsx)(x.Path,{d:"M12 4V2.2L9 4.8l3 2.5V5.5c3.6 0 6.5 2.9 6.5 6.5 0 2.9-1.9 5.3-4.5 6.2v.2l-.1-.2c-.4.1-.7.2-1.1.2l.2 1.5c.3 0 .6-.1 1-.2 3.5-.9 6-4 6-7.7 0-4.4-3.6-8-8-8zm-7.9 7l1.5.2c.1-1.2.5-2.3 1.2-3.2l-1.1-.9C4.8 8.2 4.3 9.6 4.1 11zm1.5 1.8l-1.5.2c.1.7.3 1.4.5 2 .3.7.6 1.3 1 1.8l1.2-.8c-.3-.5-.6-1-.8-1.5s-.4-1.1-.4-1.7zm1.5 5.5c1.1.9 2.4 1.4 3.8 1.6l.2-1.5c-1.1-.1-2.2-.5-3.1-1.2l-.9 1.1z"})}),Ot=(0,p.jsx)(x.SVG,{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 24 24",children:(0,p.jsx)(x.Path,{d:"M11 13h2v-2h-2v2zm-6 0h2v-2H5v2zm12-2v2h2v-2h-2z"})}),Rt=function(){return Rt=Object.assign||function(e){for(var t,n=1,r=arguments.length;n0&&i[i.length-1])||6!==a[0]&&2!==a[0])){o=0;continue}if(3===a[0]&&(!i||a[1]>i[0]&&a[1]1?[2,Promise.reject(new ie((0,_.sprintf)(/* translators: URL of the published post */ /* translators: URL of the published post */ -(0,_.__)("Multiple results were returned for the post %s by the Parse.ly API.","wp-parsely"),t),$.ParselyApiReturnedTooManyResults))]:[2,n[0]]}}))}))},t.prototype.fetchReferrerDataFromWpEndpoint=function(e,t,n){return Zt(this,void 0,void 0,(function(){return $t(this,(function(r){switch(r.label){case 0:return[4,this.fetch({path:(0,Oe.addQueryArgs)("/wp-parsely/v1/referrers/post/detail",qt(qt({},zt(e)),{itm_source:this.itmSource,total_views:n,url:t}))})];case 1:return[2,r.sent()]}}))}))},t}(Re),Kt=function(){return Kt=Object.assign||function(e){for(var t,n=1,r=arguments.length;n0&&i[i.length-1])||6!==a[0]&&2!==a[0])){o=0;continue}if(3===a[0]&&(!i||a[1]>i[0]&&a[1]0&&e.retryFetch?[4,new Promise((function(e){return setTimeout(e,500)}))]:[3,3];case 1:return t.sent(),[4,n(r-1)];case 2:return t.sent(),[3,4];case 3:a(e),i(!1),t.label=4;case 4:return[2]}}))}))})),[2]}))}))};return i(!0),n(1),function(){a(void 0)}}),[t]),(0,p.jsxs)("div",{className:"wp-parsely-performance-panel",children:[(0,p.jsx)(Tt,{title:(0,_.__)("Performance Stats","wp-parsely"),icon:jt,dropdownChildren:function(e){var t=e.onClose;return(0,p.jsx)(tn,{onClose:t})},children:(0,p.jsx)("div",{className:"panel-settings",children:(0,p.jsx)(f.SelectControl,{size:"__unstable-large",value:h.PerformanceStats.Period,prefix:(0,p.jsx)(f.__experimentalInputControlPrefixWrapper,{children:(0,_.__)("Period: ","wp-parsely")}),onChange:function(e){D(e,y)&&(v({PerformanceStats:Kt(Kt({},h.PerformanceStats),{Period:e})}),P.trackEvent("editor_sidebar_performance_period_changed",{period:e}))},children:Object.values(y).map((function(e){return(0,p.jsx)("option",{value:e,children:F(e)},e)}))})})}),o?o.Message():(0,p.jsxs)(p.Fragment,{children:[en(h,"overview")&&(0,p.jsx)(Gt,{data:c,isLoading:r}),en(h,"categories")&&(0,p.jsx)(Et,{data:c,isLoading:r}),en(h,"referrers")&&(0,p.jsx)(Ht,{data:c,isLoading:r})]}),window.wpParselyPostUrl&&(0,p.jsx)(f.Button,{className:"wp-parsely-view-post",variant:"primary",onClick:function(){P.trackEvent("editor_sidebar_view_post_pressed")},href:window.wpParselyPostUrl,rel:"noopener",target:"_blank",children:(0,_.__)("View this in Parse.ly","wp-parsely")})]})},rn=function(e){var t=e.period;return(0,p.jsx)(f.Panel,{children:(0,p.jsx)(J,{children:(0,p.jsx)(nn,{period:t})})})},sn=function(e,t){var n={};for(var r in e)Object.prototype.hasOwnProperty.call(e,r)&&t.indexOf(r)<0&&(n[r]=e[r]);if(null!=e&&"function"==typeof Object.getOwnPropertySymbols){var i=0;for(r=Object.getOwnPropertySymbols(e);i=1&&(0,p.jsx)(f.__experimentalToggleGroupControlOption,{value:w.Tag,label:(0,_.__)("Tag","wp-parsely")}),r.categories.length>=1&&(0,p.jsx)(f.__experimentalToggleGroupControlOption,{value:w.Section,label:(0,_.__)("Section","wp-parsely")}),r.authors.length>=1&&(0,p.jsx)(f.__experimentalToggleGroupControlOption,{value:w.Author,label:(0,_.__)("Author","wp-parsely")})]})})},an=function(e){var t=e.filter,n=e.label,r=e.postData,i=sn(e,["filter","label","postData"]);return(0,p.jsx)("div",{className:"related-posts-filter-values",children:(0,p.jsx)(f.ComboboxControl,{__next40pxDefaultSize:!0,allowReset:!0,label:n,onChange:function(e){return i.onFilterValueChange(e)},options:w.Tag===t.type?r.tags.map((function(e){return{value:e,label:e}})):w.Section===t.type?r.categories.map((function(e){return{value:e,label:e}})):w.Author===t.type?r.authors.map((function(e){return{value:e,label:e}})):[],value:t.value})})},ln=function(e){var t=e.filter,n=e.postData,r=e.label,i=sn(e,["filter","postData","label"]),s=function(){return n.authors.length>0&&n.categories.length>0||n.authors.length>0&&n.tags.length>0||n.tags.length>0&&n.categories.length>0},o=function(){return w.Tag===t.type&&n.tags.length>1||w.Section===t.type&&n.categories.length>1||w.Author===t.type&&n.authors.length>1};return s()||o()?(0,p.jsxs)("div",{className:"related-posts-filter-settings",children:[s()&&(0,p.jsx)(on,{filter:t,label:r,onFilterTypeChange:i.onFilterTypeChange,postData:n}),o()&&(0,p.jsx)(an,{filter:t,label:s()?void 0:r,onFilterValueChange:i.onFilterValueChange,postData:n})]}):null},cn=(0,p.jsx)(x.SVG,{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 24 24",children:(0,p.jsx)(x.Path,{d:"M10 17.389H8.444A5.194 5.194 0 1 1 8.444 7H10v1.5H8.444a3.694 3.694 0 0 0 0 7.389H10v1.5ZM14 7h1.556a5.194 5.194 0 0 1 0 10.39H14v-1.5h1.556a3.694 3.694 0 0 0 0-7.39H14V7Zm-4.5 6h5v-1.5h-5V13Z"})}),un=(0,p.jsx)(x.SVG,{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 24 24",children:(0,p.jsx)(x.Path,{fillRule:"evenodd",clipRule:"evenodd",d:"M5.625 5.5h9.75c.069 0 .125.056.125.125v9.75a.125.125 0 0 1-.125.125h-9.75a.125.125 0 0 1-.125-.125v-9.75c0-.069.056-.125.125-.125ZM4 5.625C4 4.728 4.728 4 5.625 4h9.75C16.273 4 17 4.728 17 5.625v9.75c0 .898-.727 1.625-1.625 1.625h-9.75A1.625 1.625 0 0 1 4 15.375v-9.75Zm14.5 11.656v-9H20v9C20 18.8 18.77 20 17.251 20H6.25v-1.5h11.001c.69 0 1.249-.528 1.249-1.219Z"})}),dn=function(){return(0,p.jsx)(f.SVG,{xmlns:"http://www.w3.org/2000/svg",width:"1",height:"40",viewBox:"0 0 1 40",fill:"none",children:(0,p.jsx)(f.Rect,{width:"1",height:"40",fill:"#cccccc"})})};function pn(e){var t=e.metric,n=e.post,r=e.avgEngagedIcon,i=e.viewsIcon;return"views"===t?(0,p.jsxs)("span",{className:"parsely-post-metric-data",children:[(0,p.jsx)("span",{className:"screen-reader-text",children:(0,_.__)("Number of Views","wp-parsely")}),i,Lt(n.views.toString())]}):"avg_engaged"===t?(0,p.jsxs)("span",{className:"parsely-post-metric-data",children:[(0,p.jsx)("span",{className:"screen-reader-text",children:(0,_.__)("Average Time","wp-parsely")}),r,n.avgEngaged]}):(0,p.jsx)("span",{className:"parsely-post-metric-data",children:"-"})}var fn,hn=function(e){var t,n,r=e.metric,i=e.post,s=e.postContent,o=(0,h.useDispatch)("core/notices").createNotice,a=s&&(t=s,n=q(i.rawUrl),new RegExp("]*href=[\"'](http://|https://)?.*".concat(n,".*[\"'][^>]*>"),"i").test(t));return(0,p.jsxs)("div",{className:"related-post-single","data-testid":"related-post-single",children:[(0,p.jsx)("div",{className:"related-post-title",children:(0,p.jsxs)("a",{href:i.url,target:"_blank",rel:"noreferrer",children:[(0,p.jsx)("span",{className:"screen-reader-text",children:(0,_.__)("View on website (opens new tab)","wp-parsely")}),i.title]})}),(0,p.jsx)("div",{className:"related-post-actions",children:(0,p.jsxs)("div",{className:"related-post-info",children:[(0,p.jsxs)("div",{children:[(0,p.jsx)("div",{className:"related-post-metric",children:(0,p.jsx)(pn,{metric:r,post:i,viewsIcon:(0,p.jsx)(X,{icon:Nt}),avgEngagedIcon:(0,p.jsx)(f.Dashicon,{icon:"clock",size:24})})}),a&&(0,p.jsx)("div",{className:"related-post-linked",children:(0,p.jsx)(f.Tooltip,{text:(0,_.__)("This post is linked in the content","wp-parsely"),children:(0,p.jsx)(X,{icon:cn,size:24})})})]}),(0,p.jsx)(dn,{}),(0,p.jsxs)("div",{children:[(0,p.jsx)(f.Button,{icon:un,iconSize:24,onClick:function(){navigator.clipboard.writeText(i.rawUrl).then((function(){o("success",(0,_.__)("URL copied to clipboard","wp-parsely"),{type:"snackbar"})}))},label:(0,_.__)("Copy URL to clipboard","wp-parsely")}),(0,p.jsx)(f.Button,{icon:(0,p.jsx)(T,{}),iconSize:18,href:i.dashUrl,target:"_blank",label:(0,_.__)("View in Parse.ly","wp-parsely")})]})]})})]})},vn=window.wp.coreData,gn=function(){var e=function(t,n){return e=Object.setPrototypeOf||{__proto__:[]}instanceof Array&&function(e,t){e.__proto__=t}||function(e,t){for(var n in t)Object.prototype.hasOwnProperty.call(t,n)&&(e[n]=t[n])},e(t,n)};return function(t,n){if("function"!=typeof n&&null!==n)throw new TypeError("Class extends value "+String(n)+" is not a constructor or null");function __(){this.constructor=t}e(t,n),t.prototype=null===n?Object.create(n):(__.prototype=n.prototype,new __)}}(),yn=function(){return yn=Object.assign||function(e){for(var t,n=1,r=arguments.length;n0&&i[i.length-1])||6!==a[0]&&2!==a[0])){o=0;continue}if(3===a[0]&&(!i||a[1]>i[0]&&a[1]0&&i[i.length-1])||6!==a[0]&&2!==a[0])){o=0;continue}if(3===a[0]&&(!i||a[1]>i[0]&&a[1]1?[2,Promise.reject(new ie((0,_.sprintf)(/* translators: URL of the published post */ /* translators: URL of the published post */ +(0,_.__)("Multiple results were returned for the post %d by the Parse.ly API.","wp-parsely"),t),$.ParselyApiReturnedTooManyResults))]:[2,n[0]]}}))}))},t.prototype.fetchReferrerDataFromWpEndpoint=function(e,t,n){return Zt(this,void 0,void 0,(function(){return $t(this,(function(r){switch(r.label){case 0:return[4,this.fetch({path:(0,Oe.addQueryArgs)("/wp-parsely/v2/stats/post/".concat(t,"/referrers"),qt(qt({},zt(e)),{itm_source:this.itmSource,total_views:n}))})];case 1:return[2,r.sent()]}}))}))},t}(Re),Kt=function(){return Kt=Object.assign||function(e){for(var t,n=1,r=arguments.length;n0&&i[i.length-1])||6!==a[0]&&2!==a[0])){o=0;continue}if(3===a[0]&&(!i||a[1]>i[0]&&a[1]0&&e.retryFetch?[4,new Promise((function(e){return setTimeout(e,500)}))]:[3,3];case 1:return t.sent(),[4,n(r-1)];case 2:return t.sent(),[3,4];case 3:a(e),i(!1),t.label=4;case 4:return[2]}}))}))})),[2]}))}))};return i(!0),n(1),function(){a(void 0)}}),[t]),(0,p.jsxs)("div",{className:"wp-parsely-performance-panel",children:[(0,p.jsx)(Tt,{title:(0,_.__)("Performance Stats","wp-parsely"),icon:jt,dropdownChildren:function(e){var t=e.onClose;return(0,p.jsx)(tn,{onClose:t})},children:(0,p.jsx)("div",{className:"panel-settings",children:(0,p.jsx)(f.SelectControl,{size:"__unstable-large",value:h.PerformanceStats.Period,prefix:(0,p.jsx)(f.__experimentalInputControlPrefixWrapper,{children:(0,_.__)("Period: ","wp-parsely")}),onChange:function(e){D(e,y)&&(v({PerformanceStats:Kt(Kt({},h.PerformanceStats),{Period:e})}),P.trackEvent("editor_sidebar_performance_period_changed",{period:e}))},children:Object.values(y).map((function(e){return(0,p.jsx)("option",{value:e,children:F(e)},e)}))})})}),o?o.Message():(0,p.jsxs)(p.Fragment,{children:[en(h,"overview")&&(0,p.jsx)(Gt,{data:c,isLoading:r}),en(h,"categories")&&(0,p.jsx)(Et,{data:c,isLoading:r}),en(h,"referrers")&&(0,p.jsx)(Ht,{data:c,isLoading:r})]}),window.wpParselyPostUrl&&(0,p.jsx)(f.Button,{className:"wp-parsely-view-post",variant:"primary",onClick:function(){P.trackEvent("editor_sidebar_view_post_pressed")},href:window.wpParselyPostUrl,rel:"noopener",target:"_blank",children:(0,_.__)("View this in Parse.ly","wp-parsely")})]})},rn=function(e){var t=e.period;return(0,p.jsx)(f.Panel,{children:(0,p.jsx)(J,{children:(0,p.jsx)(nn,{period:t})})})},sn=function(e,t){var n={};for(var r in e)Object.prototype.hasOwnProperty.call(e,r)&&t.indexOf(r)<0&&(n[r]=e[r]);if(null!=e&&"function"==typeof Object.getOwnPropertySymbols){var i=0;for(r=Object.getOwnPropertySymbols(e);i=1&&(0,p.jsx)(f.__experimentalToggleGroupControlOption,{value:w.Tag,label:(0,_.__)("Tag","wp-parsely")}),r.categories.length>=1&&(0,p.jsx)(f.__experimentalToggleGroupControlOption,{value:w.Section,label:(0,_.__)("Section","wp-parsely")}),r.authors.length>=1&&(0,p.jsx)(f.__experimentalToggleGroupControlOption,{value:w.Author,label:(0,_.__)("Author","wp-parsely")})]})})},an=function(e){var t=e.filter,n=e.label,r=e.postData,i=sn(e,["filter","label","postData"]);return(0,p.jsx)("div",{className:"related-posts-filter-values",children:(0,p.jsx)(f.ComboboxControl,{__next40pxDefaultSize:!0,allowReset:!0,label:n,onChange:function(e){return i.onFilterValueChange(e)},options:w.Tag===t.type?r.tags.map((function(e){return{value:e,label:e}})):w.Section===t.type?r.categories.map((function(e){return{value:e,label:e}})):w.Author===t.type?r.authors.map((function(e){return{value:e,label:e}})):[],value:t.value})})},ln=function(e){var t=e.filter,n=e.postData,r=e.label,i=sn(e,["filter","postData","label"]),s=function(){return n.authors.length>0&&n.categories.length>0||n.authors.length>0&&n.tags.length>0||n.tags.length>0&&n.categories.length>0},o=function(){return w.Tag===t.type&&n.tags.length>1||w.Section===t.type&&n.categories.length>1||w.Author===t.type&&n.authors.length>1};return s()||o()?(0,p.jsxs)("div",{className:"related-posts-filter-settings",children:[s()&&(0,p.jsx)(on,{filter:t,label:r,onFilterTypeChange:i.onFilterTypeChange,postData:n}),o()&&(0,p.jsx)(an,{filter:t,label:s()?void 0:r,onFilterValueChange:i.onFilterValueChange,postData:n})]}):null},cn=(0,p.jsx)(x.SVG,{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 24 24",children:(0,p.jsx)(x.Path,{d:"M10 17.389H8.444A5.194 5.194 0 1 1 8.444 7H10v1.5H8.444a3.694 3.694 0 0 0 0 7.389H10v1.5ZM14 7h1.556a5.194 5.194 0 0 1 0 10.39H14v-1.5h1.556a3.694 3.694 0 0 0 0-7.39H14V7Zm-4.5 6h5v-1.5h-5V13Z"})}),un=(0,p.jsx)(x.SVG,{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 24 24",children:(0,p.jsx)(x.Path,{fillRule:"evenodd",clipRule:"evenodd",d:"M5.625 5.5h9.75c.069 0 .125.056.125.125v9.75a.125.125 0 0 1-.125.125h-9.75a.125.125 0 0 1-.125-.125v-9.75c0-.069.056-.125.125-.125ZM4 5.625C4 4.728 4.728 4 5.625 4h9.75C16.273 4 17 4.728 17 5.625v9.75c0 .898-.727 1.625-1.625 1.625h-9.75A1.625 1.625 0 0 1 4 15.375v-9.75Zm14.5 11.656v-9H20v9C20 18.8 18.77 20 17.251 20H6.25v-1.5h11.001c.69 0 1.249-.528 1.249-1.219Z"})}),dn=function(){return(0,p.jsx)(f.SVG,{xmlns:"http://www.w3.org/2000/svg",width:"1",height:"40",viewBox:"0 0 1 40",fill:"none",children:(0,p.jsx)(f.Rect,{width:"1",height:"40",fill:"#cccccc"})})};function pn(e){var t=e.metric,n=e.post,r=e.avgEngagedIcon,i=e.viewsIcon;return"views"===t?(0,p.jsxs)("span",{className:"parsely-post-metric-data",children:[(0,p.jsx)("span",{className:"screen-reader-text",children:(0,_.__)("Number of Views","wp-parsely")}),i,Lt(n.views.toString())]}):"avg_engaged"===t?(0,p.jsxs)("span",{className:"parsely-post-metric-data",children:[(0,p.jsx)("span",{className:"screen-reader-text",children:(0,_.__)("Average Time","wp-parsely")}),r,n.avgEngaged]}):(0,p.jsx)("span",{className:"parsely-post-metric-data",children:"-"})}var fn,hn=function(e){var t,n,r=e.metric,i=e.post,s=e.postContent,o=(0,h.useDispatch)("core/notices").createNotice,a=s&&(t=s,n=q(i.rawUrl),new RegExp("]*href=[\"'](http://|https://)?.*".concat(n,".*[\"'][^>]*>"),"i").test(t));return(0,p.jsxs)("div",{className:"related-post-single","data-testid":"related-post-single",children:[(0,p.jsx)("div",{className:"related-post-title",children:(0,p.jsxs)("a",{href:i.url,target:"_blank",rel:"noreferrer",children:[(0,p.jsx)("span",{className:"screen-reader-text",children:(0,_.__)("View on website (opens new tab)","wp-parsely")}),i.title]})}),(0,p.jsx)("div",{className:"related-post-actions",children:(0,p.jsxs)("div",{className:"related-post-info",children:[(0,p.jsxs)("div",{children:[(0,p.jsx)("div",{className:"related-post-metric",children:(0,p.jsx)(pn,{metric:r,post:i,viewsIcon:(0,p.jsx)(X,{icon:Nt}),avgEngagedIcon:(0,p.jsx)(f.Dashicon,{icon:"clock",size:24})})}),a&&(0,p.jsx)("div",{className:"related-post-linked",children:(0,p.jsx)(f.Tooltip,{text:(0,_.__)("This post is linked in the content","wp-parsely"),children:(0,p.jsx)(X,{icon:cn,size:24})})})]}),(0,p.jsx)(dn,{}),(0,p.jsxs)("div",{children:[(0,p.jsx)(f.Button,{icon:un,iconSize:24,onClick:function(){navigator.clipboard.writeText(i.rawUrl).then((function(){o("success",(0,_.__)("URL copied to clipboard","wp-parsely"),{type:"snackbar"})}))},label:(0,_.__)("Copy URL to clipboard","wp-parsely")}),(0,p.jsx)(f.Button,{icon:(0,p.jsx)(T,{}),iconSize:18,href:i.dashUrl,target:"_blank",label:(0,_.__)("View in Parse.ly","wp-parsely")})]})]})})]})},vn=window.wp.coreData,gn=function(){var e=function(t,n){return e=Object.setPrototypeOf||{__proto__:[]}instanceof Array&&function(e,t){e.__proto__=t}||function(e,t){for(var n in t)Object.prototype.hasOwnProperty.call(t,n)&&(e[n]=t[n])},e(t,n)};return function(t,n){if("function"!=typeof n&&null!==n)throw new TypeError("Class extends value "+String(n)+" is not a constructor or null");function __(){this.constructor=t}e(t,n),t.prototype=null===n?Object.create(n):(__.prototype=n.prototype,new __)}}(),yn=function(){return yn=Object.assign||function(e){for(var t,n=1,r=arguments.length;n0&&i[i.length-1])||6!==a[0]&&2!==a[0])){o=0;continue}if(3===a[0]&&(!i||a[1]>i[0]&&a[1]>( { - path: addQueryArgs( '/wp-parsely/v1/related', { query } ), + path: addQueryArgs( '/wp-parsely/v2/stats/related', query ), } ); } catch ( wpError ) { error = wpError; diff --git a/src/content-helper/dashboard-widget/provider.ts b/src/content-helper/dashboard-widget/provider.ts index 23bc4673e0..0338e490ac 100644 --- a/src/content-helper/dashboard-widget/provider.ts +++ b/src/content-helper/dashboard-widget/provider.ts @@ -83,7 +83,7 @@ export class DashboardWidgetProvider extends BaseProvider { settings: TopPostsSettings, page: number ): Promise { const response = this.fetch( { - path: addQueryArgs( '/wp-parsely/v1/stats/posts/', { + path: addQueryArgs( '/wp-parsely/v2/stats/posts/', { limit: TOP_POSTS_DEFAULT_LIMIT, ...getApiPeriodParams( settings.Period ), sort: settings.Metric, diff --git a/src/content-helper/editor-sidebar/performance-stats/provider.ts b/src/content-helper/editor-sidebar/performance-stats/provider.ts index 5bc524cd65..98ad0681b8 100644 --- a/src/content-helper/editor-sidebar/performance-stats/provider.ts +++ b/src/content-helper/editor-sidebar/performance-stats/provider.ts @@ -68,13 +68,13 @@ export class PerformanceStatsProvider extends BaseProvider { ); } - // Get post URL. - const postUrl = editor.getPermalink(); + // Get post ID. + const postID = editor.getCurrentPostId(); - if ( null === postUrl ) { + if ( null === postID ) { return Promise.reject( new ContentHelperError( __( - "The post's URL returned null.", + "The post's ID returned null.", 'wp-parsely' ), ContentHelperErrorCode.PostIsNotPublished ) ); @@ -84,10 +84,10 @@ export class PerformanceStatsProvider extends BaseProvider { let performanceData, referrerData; try { performanceData = await this.fetchPerformanceDataFromWpEndpoint( - period, postUrl + period, postID ); referrerData = await this.fetchReferrerDataFromWpEndpoint( - period, postUrl, performanceData.views + period, postID, performanceData.views ); } catch ( contentHelperError ) { return Promise.reject( contentHelperError ); @@ -100,20 +100,19 @@ export class PerformanceStatsProvider extends BaseProvider { * Fetches the performance data for the current post from the WordPress REST * API. * - * @param {Period} period The period for which to fetch data. - * @param {string} postUrl + * @param {Period} period The period for which to fetch data. + * @param {number} postId The post's ID. * * @return {Promise } The current post's details. */ private async fetchPerformanceDataFromWpEndpoint( - period: Period, postUrl: string + period: Period, postId: number ): Promise { const response = await this.fetch( { path: addQueryArgs( - '/wp-parsely/v1/stats/post/detail', { + `/wp-parsely/v2/stats/post/${ postId }/details`, { ...getApiPeriodParams( period ), itm_source: this.itmSource, - url: postUrl, } ), } ); @@ -122,8 +121,8 @@ export class PerformanceStatsProvider extends BaseProvider { return Promise.reject( new ContentHelperError( sprintf( /* translators: URL of the published post */ - __( 'The post %s has 0 views, or the Parse.ly API returned no data.', - 'wp-parsely' ), postUrl + __( 'The post %d has 0 views, or the Parse.ly API returned no data.', + 'wp-parsely' ), postId ), ContentHelperErrorCode.ParselyApiReturnedNoData, '' ) ); } @@ -133,8 +132,8 @@ export class PerformanceStatsProvider extends BaseProvider { return Promise.reject( new ContentHelperError( sprintf( /* translators: URL of the published post */ - __( 'Multiple results were returned for the post %s by the Parse.ly API.', - 'wp-parsely' ), postUrl + __( 'Multiple results were returned for the post %d by the Parse.ly API.', + 'wp-parsely' ), postId ), ContentHelperErrorCode.ParselyApiReturnedTooManyResults ) ); } @@ -146,21 +145,20 @@ export class PerformanceStatsProvider extends BaseProvider { * Fetches referrer data for the current post from the WordPress REST API. * * @param {Period} period The period for which to fetch data. - * @param {string} postUrl The post's URL. + * @param {number} postId The post's ID. * @param {string} totalViews Total post views (including direct views). * * @return {Promise} The post's referrer data. */ private async fetchReferrerDataFromWpEndpoint( - period: Period, postUrl: string, totalViews: string + period: Period, postId: string, totalViews: string ): Promise { const response = await this.fetch( { path: addQueryArgs( - '/wp-parsely/v1/referrers/post/detail', { + `/wp-parsely/v2/stats/post/${ postId }/referrers`, { ...getApiPeriodParams( period ), itm_source: this.itmSource, total_views: totalViews, // Needed to calculate direct views. - url: postUrl, } ), } ); diff --git a/src/content-helper/editor-sidebar/related-posts/provider.ts b/src/content-helper/editor-sidebar/related-posts/provider.ts index 6345f4f193..6051c4ca84 100644 --- a/src/content-helper/editor-sidebar/related-posts/provider.ts +++ b/src/content-helper/editor-sidebar/related-posts/provider.ts @@ -146,7 +146,7 @@ export class RelatedPostsProvider extends BaseProvider { */ private async fetchRelatedPostsFromWpEndpoint( query: RelatedPostsApiQuery ): Promise { const response = this.fetch( { - path: addQueryArgs( '/wp-parsely/v1/stats/posts', { + path: addQueryArgs( '/wp-parsely/v2/stats/posts', { ...query.query, itm_source: 'wp-parsely-content-helper', } ), From fb3941af8b41dc713c22952c526661d0029748cd Mon Sep 17 00:00:00 2001 From: Henrique Mouta Date: Thu, 29 Aug 2024 13:08:26 +0100 Subject: [PATCH 13/49] Delete the old endpoints --- .../class-analytics-post-detail-api-proxy.php | 48 --- .../class-analytics-posts-api-proxy.php | 48 --- .../class-referrers-post-detail-api-proxy.php | 259 --------------- src/Endpoints/class-related-api-proxy.php | 61 ---- .../Proxy/AnalyticsPostsProxyEndpointTest.php | 264 --------------- .../ReferrersPostDetailProxyEndpointTest.php | 305 ------------------ .../Proxy/RelatedProxyEndpointTest.php | 162 ---------- .../StatsPostDetailProxyEndpointTest.php | 206 ------------ wp-parsely.php | 27 +- 9 files changed, 1 insertion(+), 1379 deletions(-) delete mode 100644 src/Endpoints/class-analytics-post-detail-api-proxy.php delete mode 100644 src/Endpoints/class-analytics-posts-api-proxy.php delete mode 100644 src/Endpoints/class-referrers-post-detail-api-proxy.php delete mode 100644 src/Endpoints/class-related-api-proxy.php delete mode 100644 tests/Integration/Endpoints/Proxy/AnalyticsPostsProxyEndpointTest.php delete mode 100644 tests/Integration/Endpoints/Proxy/ReferrersPostDetailProxyEndpointTest.php delete mode 100644 tests/Integration/Endpoints/Proxy/RelatedProxyEndpointTest.php delete mode 100644 tests/Integration/Endpoints/Proxy/StatsPostDetailProxyEndpointTest.php diff --git a/src/Endpoints/class-analytics-post-detail-api-proxy.php b/src/Endpoints/class-analytics-post-detail-api-proxy.php deleted file mode 100644 index 787ae0e50e..0000000000 --- a/src/Endpoints/class-analytics-post-detail-api-proxy.php +++ /dev/null @@ -1,48 +0,0 @@ -register_endpoint( '/stats/post/detail' ); - } - - /** - * Cached "proxy" to the Parse.ly `/analytics/post/detail` API endpoint. - * - * @param WP_REST_Request $request The request object. - * @return stdClass|WP_Error stdClass containing the data or a WP_Error object on failure. - */ - public function get_items( WP_REST_Request $request ) { - return $this->get_data( $request ); - } - - /** - * Generates the final data from the passed response. - * - * @param array $response The response received by the proxy. - * @return array The generated data. - */ - protected function generate_data( $response ): array { - return $this->generate_post_data( $response ); - } -} diff --git a/src/Endpoints/class-analytics-posts-api-proxy.php b/src/Endpoints/class-analytics-posts-api-proxy.php deleted file mode 100644 index 3ee862caf8..0000000000 --- a/src/Endpoints/class-analytics-posts-api-proxy.php +++ /dev/null @@ -1,48 +0,0 @@ -register_endpoint( '/stats/posts' ); - } - - /** - * Cached "proxy" to the Parse.ly `/analytics/posts` API endpoint. - * - * @param WP_REST_Request $request The request object. - * @return stdClass|WP_Error stdClass containing the data or a WP_Error object on failure. - */ - public function get_items( WP_REST_Request $request ) { - return $this->get_data( $request ); - } - - /** - * Generates the final data from the passed response. - * - * @param array $response The response received by the proxy. - * @return array The generated data. - */ - protected function generate_data( $response ): array { - return $this->generate_post_data( $response ); - } -} diff --git a/src/Endpoints/class-referrers-post-detail-api-proxy.php b/src/Endpoints/class-referrers-post-detail-api-proxy.php deleted file mode 100644 index 812a9ca81f..0000000000 --- a/src/Endpoints/class-referrers-post-detail-api-proxy.php +++ /dev/null @@ -1,259 +0,0 @@ -register_endpoint( '/referrers/post/detail' ); - } - - /** - * Cached "proxy" to the Parse.ly `/referrers/post/detail` API endpoint. - * - * @since 3.6.0 - * - * @param WP_REST_Request $request The request object. - * @return stdClass|WP_Error stdClass containing the data or a WP_Error object on failure. - */ - public function get_items( WP_REST_Request $request ) { - $total_views = $request->get_param( 'total_views' ) ?? '0'; - - if ( ! is_string( $total_views ) ) { - $total_views = '0'; - } - - $this->total_views = Utils::convert_to_positive_integer( $total_views ); - $request->offsetUnset( 'total_views' ); // Remove param from request. - return $this->get_data( $request ); - } - - /** - * Generates the final data from the passed response. - * - * @since 3.6.0 - * - * @param array $response The response received by the proxy. - * @return array The generated data. - */ - protected function generate_data( $response ): array { - $referrers_types = $this->generate_referrer_types_data( $response ); - $direct_views = Utils::convert_to_positive_integer( - $referrers_types->direct->views ?? '0' - ); - $referrers_top = $this->generate_referrers_data( 5, $response, $direct_views ); - - return array( - 'top' => $referrers_top, - 'types' => $referrers_types, - ); - } - - /** - * Generates the referrer types data. - * - * Referrer types are: - * - `social`: Views coming from social media. - * - `search`: Views coming from search engines. - * - `other`: Views coming from other referrers, like external websites. - * - `internal`: Views coming from linking pages of the same website. - * - * Returned object properties: - * - `views`: The number of views. - * - `viewPercentage`: The number of views as a percentage, compared to the - * total views of all referrer types. - * - * @since 3.6.0 - * - * @param array $response The response received by the proxy. - * @return stdClass The generated data. - */ - private function generate_referrer_types_data( array $response ): stdClass { - $result = new stdClass(); - $total_referrer_views = 0; // Views from all referrer types combined. - - // Set referrer type order as it is displayed in the Parse.ly dashboard. - $referrer_type_keys = array( 'social', 'search', 'other', 'internal', 'direct' ); - foreach ( $referrer_type_keys as $key ) { - $result->$key = (object) array( 'views' => 0 ); - } - - // Set views and views totals. - foreach ( $response as $referrer_data ) { - /** - * Variable. - * - * @var int - */ - $current_views = $referrer_data->metrics->referrers_views ?? 0; - $total_referrer_views += $current_views; - - /** - * Variable. - * - * @var string - */ - $current_key = $referrer_data->type ?? ''; - if ( '' !== $current_key ) { - if ( ! isset( $result->$current_key->views ) ) { - $result->$current_key = (object) array( 'views' => 0 ); - } - - $result->$current_key->views += $current_views; - } - } - - // Add direct and total views to the object. - $result->direct->views = $this->total_views - $total_referrer_views; - $result->totals = (object) array( 'views' => $this->total_views ); - - // Remove referrer types without views. - foreach ( $referrer_type_keys as $key ) { - if ( 0 === $result->$key->views ) { - unset( $result->$key ); - } - } - - // Set percentage values and format numbers. - // @phpstan-ignore-next-line. - foreach ( $result as $key => $value ) { - // Set and format percentage values. - $result->{ $key }->viewsPercentage = $this->get_i18n_percentage( - absint( $value->views ), - $this->total_views - ); - - // Format views values. - $result->{ $key }->views = number_format_i18n( $result->{ $key }->views ); - } - - return $result; - } - - /** - * Generates the top referrers data. - * - * Returned object properties: - * - `views`: The number of views. - * - `viewPercentage`: The number of views as a percentage, compared to the - * total views of all referrer types. - * - `datasetViewsPercentage: The number of views as a percentage, compared - * to the total views of the current dataset. - * - * @since 3.6.0 - * - * @param int $limit The limit of returned referrers. - * @param array $response The response received by the proxy. - * @param int $direct_views The count of direct views. - * @return stdClass The generated data. - */ - private function generate_referrers_data( - int $limit, - array $response, - int $direct_views - ): stdClass { - $temp_views = array(); - $totals = 0; - $referrer_count = count( $response ); - - // Set views and views totals. - $loop_count = $referrer_count > $limit ? $limit : $referrer_count; - for ( $i = 0; $i < $loop_count; $i++ ) { - $data = $response[ $i ]; - - /** - * Variable. - * - * @var int - */ - $referrer_views = $data->metrics->referrers_views ?? 0; - $totals += $referrer_views; - if ( isset( $data->name ) ) { - $temp_views[ $data->name ] = $referrer_views; - } - } - - // If applicable, add the direct views. - if ( isset( $referrer_views ) && $direct_views >= $referrer_views ) { - $temp_views['direct'] = $direct_views; - $totals += $direct_views; - arsort( $temp_views ); - if ( count( $temp_views ) > $limit ) { - $totals -= array_pop( $temp_views ); - } - } - - // Convert temporary array to result object and add totals. - $result = new stdClass(); - foreach ( $temp_views as $key => $value ) { - $result->$key = (object) array( 'views' => $value ); - } - $result->totals = (object) array( 'views' => $totals ); - - // Set percentages values and format numbers. - // @phpstan-ignore-next-line. - foreach ( $result as $key => $value ) { - // Percentage against all referrer views, even those not included - // in the dataset due to the $limit argument. - $result->{ $key }->viewsPercentage = $this - ->get_i18n_percentage( absint( $value->views ), $this->total_views ); - - // Percentage against the current dataset that is limited due to the - // $limit argument. - $result->{ $key }->datasetViewsPercentage = $this - ->get_i18n_percentage( absint( $value->views ), $totals ); - - // Format views values. - $result->{ $key }->views = number_format_i18n( $result->{ $key }->views ); - } - - return $result; - } - - /** - * Returns the passed number compared to the passed total, in an - * internationalized percentage format. - * - * @since 3.6.0 - * - * @param int $number The number to be calculated as a percentage. - * @param int $total The total number to compare against. - * @return string|false The internationalized percentage or false on error. - */ - private function get_i18n_percentage( int $number, int $total ) { - if ( 0 === $total ) { - return false; - } - - return number_format_i18n( $number / $total * 100, 2 ); - } -} diff --git a/src/Endpoints/class-related-api-proxy.php b/src/Endpoints/class-related-api-proxy.php deleted file mode 100644 index 9835363625..0000000000 --- a/src/Endpoints/class-related-api-proxy.php +++ /dev/null @@ -1,61 +0,0 @@ -register_endpoint( '/related' ); - } - - /** - * Cached "proxy" to the Parse.ly `/related` API endpoint. - * - * @param WP_REST_Request $request The request object. - * @return stdClass|WP_Error stdClass containing the data or a WP_Error object on failure. - */ - public function get_items( WP_REST_Request $request ) { - return $this->get_data( $request, false, 'query' ); - } - - /** - * Generates the final data from the passed response. - * - * @param array $response The response received by the proxy. - * @return array The generated data. - */ - protected function generate_data( $response ): array { - $itm_source = $this->itm_source; - - return array_map( - static function ( stdClass $item ) use ( $itm_source ) { - return (object) array( - 'image_url' => $item->image_url, - 'thumb_url_medium' => $item->thumb_url_medium, - 'title' => $item->title, - 'url' => Parsely::get_url_with_itm_source( $item->url, $itm_source ), - ); - }, - $response - ); - } -} diff --git a/tests/Integration/Endpoints/Proxy/AnalyticsPostsProxyEndpointTest.php b/tests/Integration/Endpoints/Proxy/AnalyticsPostsProxyEndpointTest.php deleted file mode 100644 index b0a7c366bf..0000000000 --- a/tests/Integration/Endpoints/Proxy/AnalyticsPostsProxyEndpointTest.php +++ /dev/null @@ -1,264 +0,0 @@ -dispatch( - new WP_REST_Request( 'GET', self::$route ) - ); - /** - * Variable. - * - * @var WP_Error - */ - $error = $response->as_error(); - - self::assertSame( 401, $response->get_status() ); - self::assertSame( 'rest_forbidden', $error->get_error_code() ); - self::assertSame( - 'Sorry, you are not allowed to do that.', - $error->get_error_message() - ); - } - - /** - * Verifies that calling `GET /wp-parsely/v1/stats/posts` returns an - * error and does not perform a remote call when the Site ID is not populated - * in site options. - * - * @covers \Parsely\Endpoints\Analytics_Posts_API_Proxy::get_items - * @uses \Parsely\Endpoints\Analytics_Posts_API_Proxy::__construct - * @uses \Parsely\Endpoints\Analytics_Posts_API_Proxy::is_available_to_current_user - * @uses \Parsely\Endpoints\Analytics_Posts_API_Proxy::run - * @uses \Parsely\Parsely::site_id_is_missing - * @uses \Parsely\Parsely::site_id_is_set - * @uses \Parsely\Parsely::get_options - * @uses \Parsely\Endpoints\Base_Endpoint::__construct - * @uses \Parsely\Endpoints\Base_API_Proxy::get_data - * @uses \Parsely\Endpoints\Base_API_Proxy::register_endpoint - */ - public function test_get_items_fails_when_site_id_is_not_set(): void { - $this->set_current_user_to_admin(); - parent::run_test_get_items_fails_without_site_id_set(); - } - - /** - * Verifies that calling `GET /wp-parsely/v1/stats/posts` returns an - * error and does not perform a remote call when the API Secret is not - * populated in site options. - * - * @covers \Parsely\Endpoints\Analytics_Posts_API_Proxy::get_items - * @uses \Parsely\Endpoints\Analytics_Posts_API_Proxy::__construct - * @uses \Parsely\Endpoints\Analytics_Posts_API_Proxy::is_available_to_current_user - * @uses \Parsely\Endpoints\Analytics_Posts_API_Proxy::run - * @uses \Parsely\Parsely::site_id_is_missing - * @uses \Parsely\Parsely::site_id_is_set - * @uses \Parsely\Parsely::api_secret_is_set - * @uses \Parsely\Parsely::get_options - * @uses \Parsely\Endpoints\Base_Endpoint::__construct - * @uses \Parsely\Endpoints\Base_API_Proxy::get_data - * @uses \Parsely\Endpoints\Base_API_Proxy::register_endpoint - */ - public function test_get_items_fails_when_api_secret_is_not_set(): void { - $this->set_current_user_to_admin(); - parent::run_test_get_items_fails_without_api_secret_set(); - } - - /** - * Verifies default user capability filter. - * - * @covers \Parsely\Endpoints\Analytics_Posts_API_Proxy::is_available_to_current_user - * @uses \Parsely\Endpoints\Base_Endpoint::__construct - * @uses \Parsely\RemoteAPI\Analytics_Posts_API::is_available_to_current_user - */ - public function test_user_is_allowed_to_make_proxy_api_call_if_default_user_capability_is_changed(): void { - parent::run_test_user_is_allowed_to_make_proxy_api_call_if_default_user_capability_is_changed(); - } - - /** - * Verifies endpoint specific user capability filter. - * - * @covers \Parsely\Endpoints\Analytics_Posts_API_Proxy::is_available_to_current_user - * @uses \Parsely\Endpoints\Base_Endpoint::__construct - * @uses \Parsely\RemoteAPI\Analytics_Posts_API::is_available_to_current_user - */ - public function test_user_is_allowed_to_make_proxy_api_call_if_endpoint_specific_user_capability_is_changed(): void { - parent::run_test_user_is_allowed_to_make_proxy_api_call_if_endpoint_specific_user_capability_is_changed( 'analytics_posts' ); - } - - /** - * Verifies that calls to `GET /wp-parsely/v1/stats/posts` return - * results in the expected format. - * - * @covers \Parsely\Endpoints\Analytics_Posts_API_Proxy::get_items - * @uses \Parsely\Endpoints\Analytics_Posts_API_Proxy::__construct - * @uses \Parsely\Endpoints\Analytics_Posts_API_Proxy::generate_data - * @uses \Parsely\Endpoints\Analytics_Posts_API_Proxy::is_available_to_current_user - * @uses \Parsely\Endpoints\Analytics_Posts_API_Proxy::run - * @uses \Parsely\Endpoints\Base_API_Proxy::get_data - * @uses \Parsely\Endpoints\Base_API_Proxy::register_endpoint - * @uses \Parsely\Parsely::site_id_is_missing - * @uses \Parsely\Parsely::site_id_is_set - * @uses \Parsely\Parsely::api_secret_is_set - * @uses \Parsely\Parsely::get_site_id - * @uses \Parsely\Parsely::get_api_secret - * @uses \Parsely\Parsely::get_options - * @uses \Parsely\Endpoints\Base_Endpoint::__construct - * @uses \Parsely\RemoteAPI\Base_Endpoint_Remote::get_api_url - * @uses \Parsely\RemoteAPI\Base_Endpoint_Remote::get_items - * @uses \Parsely\RemoteAPI\Base_Endpoint_Remote::get_request_options - */ - public function test_get_items(): void { - $this->set_current_user_to_admin(); - TestCase::set_options( - array( - 'apikey' => 'example.com', - 'api_secret' => 'test', - ) - ); - - $dispatched = 0; - $date_format = Utils::get_date_format(); - - add_filter( - 'pre_http_request', - function () use ( &$dispatched ): array { - $dispatched++; - return array( - 'body' => '{"data":[ - { - "author": "Aakash Shah", - "metrics": {"views": 142}, - "pub_date": "2020-04-06T13:30:58", - "thumb_url_medium": "https://images.parsely.com/XCmTXuOf8yVbUYTxj2abQ4RSDkM=/85x85/smart/https%3A//blog.parse.ly/wp-content/uploads/2021/06/Web-Analytics-Tool.png%3Fw%3D150%26h%3D150%26crop%3D1", - "title": "9 Types of Web Analytics Tools \u2014 And How to Know Which Ones You Really Need", - "url": "https://blog.parse.ly/web-analytics-software-tools/?itm_source=parsely-api" - }, - { - "author": "Stephanie Schwartz and Andrew Butler", - "metrics": {"views": 40}, - "pub_date": "2021-04-30T20:30:24", - "thumb_url_medium": "https://images.parsely.com/ap3YSufqxnLpz6zzQshoks3snXI=/85x85/smart/https%3A//blog.parse.ly/wp-content/uploads/2021/05/pexels-brett-jordan-998501-1024x768-2.jpeg%3Fw%3D150%26h%3D150%26crop%3D1", - "title": "5 Tagging Best Practices For Getting the Most Out of Your Content Strategy", - "url": "https://blog.parse.ly/5-tagging-best-practices-content-strategy/?itm_source=parsely-api" - } - ]}', - ); - } - ); - - $rest_request = new WP_REST_Request( 'GET', '/wp-parsely/v1/stats/posts' ); - $rest_request->set_param( 'itm_source', 'wp-parsely-content-helper' ); - - $response = rest_get_server()->dispatch( $rest_request ); - - self::assertSame( 1, $dispatched ); - self::assertSame( 200, $response->get_status() ); - self::assertEquals( - (object) array( - 'data' => array( - (object) array( - 'author' => 'Aakash Shah', - 'date' => wp_date( $date_format, strtotime( '2020-04-06T13:30:58' ) ), - 'id' => 'https://blog.parse.ly/web-analytics-software-tools/', - 'dashUrl' => PARSELY::DASHBOARD_BASE_URL . '/example.com/find?url=https%3A%2F%2Fblog.parse.ly%2Fweb-analytics-software-tools%2F', - 'thumbnailUrl' => 'https://images.parsely.com/XCmTXuOf8yVbUYTxj2abQ4RSDkM=/85x85/smart/https%3A//blog.parse.ly/wp-content/uploads/2021/06/Web-Analytics-Tool.png%3Fw%3D150%26h%3D150%26crop%3D1', - 'title' => '9 Types of Web Analytics Tools — And How to Know Which Ones You Really Need', - 'url' => 'https://blog.parse.ly/web-analytics-software-tools/?itm_source=wp-parsely-content-helper', - 'views' => '142', - 'postId' => 0, - 'rawUrl' => 'https://blog.parse.ly/web-analytics-software-tools/', - ), - (object) array( - 'author' => 'Stephanie Schwartz and Andrew Butler', - 'date' => wp_date( $date_format, strtotime( '2021-04-30T20:30:24' ) ), - 'id' => 'https://blog.parse.ly/5-tagging-best-practices-content-strategy/', - 'dashUrl' => PARSELY::DASHBOARD_BASE_URL . '/example.com/find?url=https%3A%2F%2Fblog.parse.ly%2F5-tagging-best-practices-content-strategy%2F', - 'thumbnailUrl' => 'https://images.parsely.com/ap3YSufqxnLpz6zzQshoks3snXI=/85x85/smart/https%3A//blog.parse.ly/wp-content/uploads/2021/05/pexels-brett-jordan-998501-1024x768-2.jpeg%3Fw%3D150%26h%3D150%26crop%3D1', - 'title' => '5 Tagging Best Practices For Getting the Most Out of Your Content Strategy', - 'url' => 'https://blog.parse.ly/5-tagging-best-practices-content-strategy/?itm_source=wp-parsely-content-helper', - 'views' => '40', - 'postId' => 0, - 'rawUrl' => 'https://blog.parse.ly/5-tagging-best-practices-content-strategy/', - ), - ), - ), - $response->get_data() - ); - } -} diff --git a/tests/Integration/Endpoints/Proxy/ReferrersPostDetailProxyEndpointTest.php b/tests/Integration/Endpoints/Proxy/ReferrersPostDetailProxyEndpointTest.php deleted file mode 100644 index 38ec499a46..0000000000 --- a/tests/Integration/Endpoints/Proxy/ReferrersPostDetailProxyEndpointTest.php +++ /dev/null @@ -1,305 +0,0 @@ -set_current_user_to_admin(); - parent::run_test_get_items_fails_without_site_id_set(); - } - - /** - * Verifies that calling `GET /wp-parsely/v1/referrers/post/detail` returns - * an error and does not perform a remote call when the Site ID is not - * populated in site options. - * - * @covers \Parsely\Endpoints\Referrers_Post_Detail_API_Proxy::get_items - * @uses \Parsely\Endpoints\Base_API_Proxy::get_data - * @uses \Parsely\Endpoints\Base_API_Proxy::register_endpoint - * @uses \Parsely\Endpoints\Referrers_Post_Detail_API_Proxy::__construct - * @uses \Parsely\Endpoints\Referrers_Post_Detail_API_Proxy::is_available_to_current_user - * @uses \Parsely\Endpoints\Referrers_Post_Detail_API_Proxy::run - * @uses \Parsely\Parsely::site_id_is_missing - * @uses \Parsely\Parsely::site_id_is_set - * @uses \Parsely\Parsely::api_secret_is_set - * @uses \Parsely\Parsely::get_options - * @uses \Parsely\Endpoints\Base_Endpoint::__construct - */ - public function test_get_items_fails_when_api_secret_is_not_set(): void { - $this->set_current_user_to_admin(); - parent::run_test_get_items_fails_without_api_secret_set(); - } - - /** - * Verifies default user capability filter. - * - * @covers \Parsely\Endpoints\Referrers_Post_Detail_API_Proxy::is_available_to_current_user - * @uses \Parsely\Endpoints\Base_Endpoint::__construct - * @uses \Parsely\RemoteAPI\Referrers_Post_Detail_API::is_available_to_current_user - */ - public function test_user_is_allowed_to_make_proxy_api_call_if_default_user_capability_is_changed(): void { - parent::run_test_user_is_allowed_to_make_proxy_api_call_if_default_user_capability_is_changed(); - } - - /** - * Verifies endpoint specific user capability filter. - * - * @covers \Parsely\Endpoints\Referrers_Post_Detail_API_Proxy::is_available_to_current_user - * @uses \Parsely\Endpoints\Base_Endpoint::__construct - * @uses \Parsely\RemoteAPI\Referrers_Post_Detail_API::is_available_to_current_user - */ - public function test_user_is_allowed_to_make_proxy_api_call_if_endpoint_specific_user_capability_is_changed(): void { - parent::run_test_user_is_allowed_to_make_proxy_api_call_if_endpoint_specific_user_capability_is_changed(); - } - - /** - * Verifies that calls to `GET /wp-parsely/v1/referrers/post/detail` return - * results in the expected format. - * - * @covers \Parsely\Endpoints\Referrers_Post_Detail_API_Proxy::get_items - * @uses \Parsely\Endpoints\Base_API_Proxy::get_data - * @uses \Parsely\Endpoints\Base_API_Proxy::register_endpoint - * @uses \Parsely\Endpoints\Referrers_Post_Detail_API_Proxy::__construct - * @uses \Parsely\Endpoints\Referrers_Post_Detail_API_Proxy::generate_data - * @uses \Parsely\Endpoints\Referrers_Post_Detail_API_Proxy::is_available_to_current_user - * @uses \Parsely\Endpoints\Referrers_Post_Detail_API_Proxy::run - * @uses \Parsely\Parsely::site_id_is_missing - * @uses \Parsely\Parsely::site_id_is_set - * @uses \Parsely\Parsely::api_secret_is_set - * @uses \Parsely\Parsely::get_site_id - * @uses \Parsely\Parsely::get_options - * @uses \Parsely\Endpoints\Base_Endpoint::__construct - * @uses \Parsely\RemoteAPI\Base_Endpoint_Remote::get_api_url - * @uses \Parsely\RemoteAPI\Base_Endpoint_Remote::get_items - * @uses \Parsely\RemoteAPI\Base_Endpoint_Remote::get_request_options - */ - public function test_get_items(): void { - $this->set_current_user_to_admin(); - TestCase::set_options( - array( - 'apikey' => 'example.com', - 'api_secret' => 'test', - ) - ); - - $dispatched = 0; - - add_filter( - 'pre_http_request', - function () use ( &$dispatched ): array { - $dispatched++; - return array( - 'body' => '{"data":[ - { - "metrics": {"referrers_views": 1500}, - "name": "google", - "type": "search" - }, - { - "metrics": {"referrers_views": 100}, - "name": "blog.parse.ly", - "type": "internal" - }, - { - "metrics": {"referrers_views": 50}, - "name": "bing", - "type": "search" - }, - { - "metrics": {"referrers_views": 30}, - "name": "facebook.com", - "type": "social" - }, - { - "metrics": {"referrers_views": 10}, - "name": "okt.to", - "type": "other" - }, - { - "metrics": {"referrers_views": 10}, - "name": "yandex", - "type": "search" - }, - { - "metrics": {"referrers_views": 10}, - "name": "parse.ly", - "type": "internal" - }, - { - "metrics": {"referrers_views": 10}, - "name": "yahoo!", - "type": "search" - }, - { - "metrics": {"referrers_views": 5}, - "name": "site1.com", - "type": "other" - }, - { - "metrics": {"referrers_views": 5}, - "name": "link.site2.com", - "type": "other" - } - ]}', - ); - } - ); - - $expected_top = (object) array( - 'direct' => (object) array( - 'views' => '770', - 'viewsPercentage' => '30.80', - 'datasetViewsPercentage' => '31.43', - ), - 'google' => (object) array( - 'views' => '1,500', - 'viewsPercentage' => '60.00', - 'datasetViewsPercentage' => '61.22', - ), - 'blog.parse.ly' => (object) array( - 'views' => '100', - 'viewsPercentage' => '4.00', - 'datasetViewsPercentage' => '4.08', - ), - 'bing' => (object) array( - 'views' => '50', - 'viewsPercentage' => '2.00', - 'datasetViewsPercentage' => '2.04', - ), - 'facebook.com' => (object) array( - 'views' => '30', - 'viewsPercentage' => '1.20', - 'datasetViewsPercentage' => '1.22', - ), - 'totals' => (object) array( - 'views' => '2,450', - 'viewsPercentage' => '98.00', - 'datasetViewsPercentage' => '100.00', - ), - ); - - $expected_types = (object) array( - 'social' => (object) array( - 'views' => '30', - 'viewsPercentage' => '1.20', - ), - 'search' => (object) array( - 'views' => '1,570', - 'viewsPercentage' => '62.80', - ), - 'other' => (object) array( - 'views' => '20', - 'viewsPercentage' => '0.80', - ), - 'internal' => (object) array( - 'views' => '110', - 'viewsPercentage' => '4.40', - ), - 'direct' => (object) array( - 'views' => '770', - 'viewsPercentage' => '30.80', - ), - 'totals' => (object) array( - 'views' => '2,500', - 'viewsPercentage' => '100.00', - ), - ); - - $request = new WP_REST_Request( 'GET', self::$route ); - $request->set_param( 'total_views', '2,500' ); - - $response = rest_get_server()->dispatch( $request ); - - self::assertSame( 1, $dispatched ); - self::assertSame( 200, $response->get_status() ); - self::assertEquals( - (object) array( - 'data' => array( - 'top' => $expected_top, - 'types' => $expected_types, - ), - ), - $response->get_data() - ); - } -} diff --git a/tests/Integration/Endpoints/Proxy/RelatedProxyEndpointTest.php b/tests/Integration/Endpoints/Proxy/RelatedProxyEndpointTest.php deleted file mode 100644 index 7584fa597d..0000000000 --- a/tests/Integration/Endpoints/Proxy/RelatedProxyEndpointTest.php +++ /dev/null @@ -1,162 +0,0 @@ - 'example.com' ) ); - - $dispatched = 0; - - add_filter( - 'pre_http_request', - function () use ( &$dispatched ): array { - $dispatched++; - return array( - 'body' => '{"data":[ - { - "image_url":"https:\/\/example.com\/img.png", - "thumb_url_medium":"https:\/\/example.com\/thumb.png", - "title":"something", - "url":"https:\/\/example.com" - }, - { - "image_url":"https:\/\/example.com\/img2.png", - "thumb_url_medium":"https:\/\/example.com\/thumb2.png", - "title":"something2", - "url":"https:\/\/example.com\/2" - } - ]}', - ); - } - ); - - $response = rest_get_server()->dispatch( new WP_REST_Request( 'GET', '/wp-parsely/v1/related' ) ); - - self::assertSame( 1, $dispatched ); - self::assertSame( 200, $response->get_status() ); - self::assertEquals( - (object) array( - 'data' => array( - (object) array( - 'image_url' => 'https://example.com/img.png', - 'thumb_url_medium' => 'https://example.com/thumb.png', - 'title' => 'something', - 'url' => 'https://example.com', - ), - (object) array( - 'image_url' => 'https://example.com/img2.png', - 'thumb_url_medium' => 'https://example.com/thumb2.png', - 'title' => 'something2', - 'url' => 'https://example.com/2', - ), - ), - ), - $response->get_data() - ); - } -} diff --git a/tests/Integration/Endpoints/Proxy/StatsPostDetailProxyEndpointTest.php b/tests/Integration/Endpoints/Proxy/StatsPostDetailProxyEndpointTest.php deleted file mode 100644 index c442d098a2..0000000000 --- a/tests/Integration/Endpoints/Proxy/StatsPostDetailProxyEndpointTest.php +++ /dev/null @@ -1,206 +0,0 @@ -set_current_user_to_admin(); - parent::run_test_get_items_fails_without_site_id_set(); - } - - /** - * Verifies that calling `GET /wp-parsely/v1/analytics/post/detail` returns - * an error and does not perform a remote call when the Site ID is not - * populated in site options. - * - * @covers \Parsely\Endpoints\Analytics_Post_Detail_API_Proxy::get_items - * @uses \Parsely\Endpoints\Base_API_Proxy::get_data - * @uses \Parsely\Endpoints\Base_API_Proxy::register_endpoint - * @uses \Parsely\Endpoints\Analytics_Post_Detail_API_Proxy::__construct - * @uses \Parsely\Endpoints\Analytics_Post_Detail_API_Proxy::is_available_to_current_user - * @uses \Parsely\Endpoints\Analytics_Post_Detail_API_Proxy::run - * @uses \Parsely\Parsely::site_id_is_missing - * @uses \Parsely\Parsely::site_id_is_set - * @uses \Parsely\Parsely::api_secret_is_set - * @uses \Parsely\Parsely::get_options - * @uses \Parsely\Endpoints\Base_Endpoint::__construct - */ - public function test_get_items_fails_when_api_secret_is_not_set(): void { - $this->set_current_user_to_admin(); - parent::run_test_get_items_fails_without_api_secret_set(); - } - - /** - * Verifies default user capability filter. - * - * @covers \Parsely\Endpoints\Analytics_Post_Detail_API_Proxy::is_available_to_current_user - * @uses \Parsely\Endpoints\Base_Endpoint::__construct - */ - public function test_user_is_allowed_to_make_proxy_api_call_if_default_user_capability_is_changed(): void { - parent::run_test_user_is_allowed_to_make_proxy_api_call_if_default_user_capability_is_changed(); - } - - /** - * Verifies endpoint specific user capability filter. - * - * @covers \Parsely\Endpoints\Analytics_Post_Detail_API_Proxy::is_available_to_current_user - * @uses \Parsely\Endpoints\Base_Endpoint::__construct - */ - public function test_user_is_allowed_to_make_proxy_api_call_if_endpoint_specific_user_capability_is_changed(): void { - parent::run_test_user_is_allowed_to_make_proxy_api_call_if_endpoint_specific_user_capability_is_changed( 'analytics_post_detail' ); - } - - /** - * Verifies that calls to `GET /wp-parsely/v1/analytics/post/detail` return - * results in the expected format. - * - * @covers \Parsely\Endpoints\Analytics_Post_Detail_API_Proxy::get_items - * @uses \Parsely\Endpoints\Base_API_Proxy::get_data - * @uses \Parsely\Endpoints\Base_API_Proxy::register_endpoint - * @uses \Parsely\Endpoints\Analytics_Post_Detail_API_Proxy::__construct - * @uses \Parsely\Endpoints\Analytics_Post_Detail_API_Proxy::generate_data - * @uses \Parsely\Endpoints\Analytics_Post_Detail_API_Proxy::is_available_to_current_user - * @uses \Parsely\Endpoints\Analytics_Post_Detail_API_Proxy::run - * @uses \Parsely\Parsely::site_id_is_missing - * @uses \Parsely\Parsely::site_id_is_set - * @uses \Parsely\Parsely::api_secret_is_set - * @uses \Parsely\Parsely::get_site_id - * @uses \Parsely\Parsely::get_options - * @uses \Parsely\Endpoints\Base_Endpoint::__construct - * @uses \Parsely\RemoteAPI\Base_Endpoint_Remote::get_api_url - * @uses \Parsely\RemoteAPI\Base_Endpoint_Remote::get_items - * @uses \Parsely\RemoteAPI\Base_Endpoint_Remote::get_request_options - */ - public function test_get_items(): void { - $this->set_current_user_to_admin(); - TestCase::set_options( - array( - 'apikey' => 'example.com', - 'api_secret' => 'test', - ) - ); - - $dispatched = 0; - - add_filter( - 'pre_http_request', - function () use ( &$dispatched ): array { - $dispatched++; - return array( - 'body' => ' - {"data":[{ - "avg_engaged": 1.911, - "metrics": { - "views": 2158, - "visitors": 1537 - }, - "url": "https://example.com" - }]} - ', - ); - } - ); - - $response = rest_get_server()->dispatch( new WP_REST_Request( 'GET', '/wp-parsely/v1/stats/post/detail' ) ); - - self::assertSame( 1, $dispatched ); - self::assertSame( 200, $response->get_status() ); - self::assertEquals( - (object) array( - 'data' => array( - (object) array( - 'avgEngaged' => '1:55', - 'dashUrl' => Parsely::DASHBOARD_BASE_URL . '/example.com/find?url=https%3A%2F%2Fexample.com', - 'id' => 'https://example.com', - 'postId' => 0, - 'url' => 'https://example.com', - 'views' => '2,158', - 'visitors' => '1,537', - 'rawUrl' => 'https://example.com', - ), - ), - ), - $response->get_data() - ); - } -} diff --git a/wp-parsely.php b/wp-parsely.php index ae8e313315..cf5012bef6 100644 --- a/wp-parsely.php +++ b/wp-parsely.php @@ -132,37 +132,12 @@ function parsely_wp_admin_early_register(): void { * @since 3.2.0 */ function parsely_rest_api_init(): void { - $wp_cache = new WordPress_Cache(); - $rest = new Rest_Metadata( $GLOBALS['parsely'] ); + $rest = new Rest_Metadata( $GLOBALS['parsely'] ); $rest->run(); // Content Helper settings endpoints. ( new Dashboard_Widget_Settings_Endpoint( $GLOBALS['parsely'] ) )->run(); ( new Editor_Sidebar_Settings_Endpoint( $GLOBALS['parsely'] ) )->run(); - - parsely_run_rest_api_endpoint( - Related_API::class, - Related_API_Proxy::class, - $wp_cache - ); - - parsely_run_rest_api_endpoint( - Analytics_Posts_API::class, - Analytics_Posts_API_Proxy::class, - $wp_cache - ); - - parsely_run_rest_api_endpoint( - Analytics_Post_Detail_API::class, - Analytics_Post_Detail_API_Proxy::class, - $wp_cache - ); - - parsely_run_rest_api_endpoint( - Referrers_Post_Detail_API::class, - Referrers_Post_Detail_API_Proxy::class, - $wp_cache - ); } add_action( 'init', __NAMESPACE__ . '\\init_recommendations_block' ); From 5d785ddcb46b9796583a12d22650b37959d8c839 Mon Sep 17 00:00:00 2001 From: Henrique Mouta Date: Thu, 29 Aug 2024 13:08:50 +0100 Subject: [PATCH 14/49] Add tests --- src/rest-api/stats/class-endpoint-related.php | 6 +- src/rest-api/stats/trait-post-data.php | 7 +- .../Integration/RestAPI/BaseEndpointTest.php | 12 + .../ContentHelperControllerTest.php | 27 +- .../ContentHelperFeatureTestTrait.php | 164 ++--- .../EndpointExcerptGeneratorTest.php | 23 +- .../EndpointSmartLinkingTest.php | 44 +- .../EndpointTitleSuggestionsTest.php | 11 +- .../RestAPI/Stats/EndpointPostTest.php | 674 ++++++++++++++++++ .../RestAPI/Stats/EndpointPostsTest.php | 347 +++++++++ .../RestAPI/Stats/EndpointRelatedTest.php | 316 ++++++++ .../RestAPI/Stats/StatsControllerTest.php | 60 ++ 12 files changed, 1552 insertions(+), 139 deletions(-) create mode 100644 tests/Integration/RestAPI/Stats/EndpointPostTest.php create mode 100644 tests/Integration/RestAPI/Stats/EndpointPostsTest.php create mode 100644 tests/Integration/RestAPI/Stats/EndpointRelatedTest.php create mode 100644 tests/Integration/RestAPI/Stats/StatsControllerTest.php diff --git a/src/rest-api/stats/class-endpoint-related.php b/src/rest-api/stats/class-endpoint-related.php index d675e9e923..d636957b8f 100644 --- a/src/rest-api/stats/class-endpoint-related.php +++ b/src/rest-api/stats/class-endpoint-related.php @@ -104,9 +104,9 @@ public function get_related_posts( WP_REST_Request $request ) { * @since 3.17.0 * * @param WP_REST_Request|null $request The request object. - * @return bool + * @return bool|WP_Error */ - public function is_available_to_current_user( ?WP_REST_Request $request = null ): bool { - return true; + public function is_available_to_current_user( ?WP_REST_Request $request = null ) { + return $this->validate_site_id_and_secret( false ); } } diff --git a/src/rest-api/stats/trait-post-data.php b/src/rest-api/stats/trait-post-data.php index 4698d2a9a4..0ccb0a087e 100644 --- a/src/rest-api/stats/trait-post-data.php +++ b/src/rest-api/stats/trait-post-data.php @@ -66,10 +66,9 @@ private function set_itm_source_from_request( WP_REST_Request $request ): void { private function get_itm_source_param_args(): array { return array( 'itm_source' => array( - 'description' => __( 'The source of the item.', 'wp-parsely' ), - 'type' => 'string', - 'required' => false, - 'validate_callback' => array( $this, 'validate_itm_source' ), + 'description' => __( 'The source of the item.', 'wp-parsely' ), + 'type' => 'string', + 'required' => false, ), ); } diff --git a/tests/Integration/RestAPI/BaseEndpointTest.php b/tests/Integration/RestAPI/BaseEndpointTest.php index 4b0b015053..0f6ba01ecc 100644 --- a/tests/Integration/RestAPI/BaseEndpointTest.php +++ b/tests/Integration/RestAPI/BaseEndpointTest.php @@ -238,11 +238,19 @@ public function test_route_is_registered(): void { * @uses \Parsely\REST_API\Base_API_Controller::__construct * @uses \Parsely\REST_API\Base_API_Controller::get_full_namespace * @uses \Parsely\REST_API\Base_API_Controller::prefix_route + * @uses \Parsely\REST_API\Base_API_Controller::get_parsely + * @uses \Parsely\REST_API\REST_API_Controller::get_namespace + * @uses \Parsely\REST_API\REST_API_Controller::get_version + * @uses \Parsely\REST_API\Base_API_Controller::get_route_prefix * @uses \Parsely\REST_API\Base_Endpoint::__construct * @uses \Parsely\REST_API\Base_Endpoint::get_endpoint_name * @uses \Parsely\REST_API\Base_Endpoint::is_available_to_current_user * @uses \Parsely\REST_API\Base_Endpoint::register_rest_route * @uses \Parsely\REST_API\Base_Endpoint::validate_site_id_and_secret + * @uses \Parsely\REST_API\Base_Endpoint::apply_capability_filters + * @uses \Parsely\REST_API\Base_Endpoint::get_default_access_capability + * @uses \Parsely\REST_API\Content_Helper\Content_Helper_Controller::get_route_prefix + * @uses \Parsely\REST_API\Stats\Stats_Controller::get_route_prefix * @uses \Parsely\Utils\Utils::convert_endpoint_to_filter_key */ public function test_endpoint_is_registered_based_on_filter(): void { @@ -285,6 +293,7 @@ public function test_endpoint_is_registered_based_on_filter(): void { * @covers \Parsely\REST_API\Base_Endpoint::is_available_to_current_user * @covers \Parsely\REST_API\Base_Endpoint::validate_site_id_and_secret * @uses \Parsely\REST_API\Base_API_Controller::__construct + * @uses \Parsely\REST_API\Base_API_Controller::get_parsely * @uses \Parsely\Parsely::__construct * @uses \Parsely\Parsely::allow_parsely_remote_requests * @uses \Parsely\Parsely::are_credentials_managed @@ -324,6 +333,7 @@ public function test_is_available_to_current_user_returns_error_site_id_not_set( * @covers \Parsely\REST_API\Base_Endpoint::is_available_to_current_user * @covers \Parsely\REST_API\Base_Endpoint::validate_site_id_and_secret * @uses \Parsely\REST_API\Base_API_Controller::__construct + * @uses \Parsely\REST_API\Base_API_Controller::get_parsely * @uses \Parsely\Parsely::__construct * @uses \Parsely\Parsely::allow_parsely_remote_requests * @uses \Parsely\Parsely::api_secret_is_set @@ -361,6 +371,7 @@ public function test_is_available_to_current_user_returns_error_api_secret_not_s * * @covers \Parsely\REST_API\Base_Endpoint::apply_capability_filters * @uses \Parsely\REST_API\Base_API_Controller::__construct + * @uses \Parsely\REST_API\Base_API_Controller::get_parsely * @uses \Parsely\Utils\Utils::convert_endpoint_to_filter_key * @uses \Parsely\Parsely::__construct * @uses \Parsely\Parsely::allow_parsely_remote_requests @@ -387,6 +398,7 @@ function () { * * @covers \Parsely\REST_API\Base_Endpoint::validate_site_id_and_secret * @uses \Parsely\REST_API\Base_API_Controller::__construct + * @uses \Parsely\REST_API\Base_API_Controller::get_parsely * @uses \Parsely\Parsely::__construct * @uses \Parsely\Parsely::allow_parsely_remote_requests * @uses \Parsely\Parsely::api_secret_is_set diff --git a/tests/Integration/RestAPI/ContentHelper/ContentHelperControllerTest.php b/tests/Integration/RestAPI/ContentHelper/ContentHelperControllerTest.php index fcc563d076..27c9e3b19d 100644 --- a/tests/Integration/RestAPI/ContentHelper/ContentHelperControllerTest.php +++ b/tests/Integration/RestAPI/ContentHelper/ContentHelperControllerTest.php @@ -55,6 +55,8 @@ public function setUp(): void { * * @covers \Parsely\REST_API\Content_Helper\Content_Helper_Controller::__construct * @uses \Parsely\REST_API\Content_Helper\Content_Helper_Controller::get_full_namespace + * @uses \Parsely\REST_API\REST_API_Controller::get_namespace + * @uses \Parsely\REST_API\REST_API_Controller::get_version */ public function test_constructor_sets_up_namespace_and_version(): void { self::assertEquals( 'wp-parsely/v2', $this->content_helper_controller->get_full_namespace() ); @@ -77,16 +79,21 @@ public function test_route_prefix(): void { * @since 3.17.0 * * @covers \Parsely\REST_API\Content_Helper\Content_Helper_Controller::init - * @uses \Parsely\REST_API\Content_Helper\Content_Helper_Controller::register_endpoints - * @uses Parsely\Endpoints\Base_Endpoint::__construct - * @uses Parsely\REST_API\Base_API_Controller::__construct - * @uses Parsely\REST_API\Base_API_Controller::register_endpoint - * @uses Parsely\REST_API\Base_Endpoint::__construct - * @uses Parsely\REST_API\Base_Endpoint::init - * @uses Parsely\REST_API\Content_Helper\Excerpt_Generator_Endpoint::__construct - * @uses Parsely\REST_API\Content_Helper\Smart_Linking_Endpoint::__construct - * @uses Parsely\REST_API\Content_Helper\Title_Suggestions_Endpoint::__construct - * @uses Parsely\Utils\Utils::convert_endpoint_to_filter_key + * @uses \Parsely\Endpoints\Base_Endpoint::__construct + * @uses \Parsely\REST_API\Base_API_Controller::__construct + * @uses \Parsely\REST_API\Base_API_Controller::get_endpoints + * @uses \Parsely\REST_API\Base_API_Controller::get_parsely + * @uses \Parsely\REST_API\Base_API_Controller::register_endpoint + * @uses \Parsely\REST_API\Base_API_Controller::register_endpoints + * @uses \Parsely\REST_API\Base_Endpoint::__construct + * @uses \Parsely\REST_API\Base_Endpoint::init + * @uses \Parsely\REST_API\Content_Helper\Endpoint_Excerpt_Generator::__construct + * @uses \Parsely\REST_API\Content_Helper\Endpoint_Excerpt_Generator::get_endpoint_name + * @uses \Parsely\REST_API\Content_Helper\Endpoint_Smart_Linking::__construct + * @uses \Parsely\REST_API\Content_Helper\Endpoint_Smart_Linking::get_endpoint_name + * @uses \Parsely\REST_API\Content_Helper\Endpoint_Title_Suggestions::__construct + * @uses \Parsely\REST_API\Content_Helper\Endpoint_Title_Suggestions::get_endpoint_name + * @uses \Parsely\Utils\Utils::convert_endpoint_to_filter_key */ public function test_init_registers_endpoints(): void { $this->content_helper_controller->init(); diff --git a/tests/Integration/RestAPI/ContentHelper/ContentHelperFeatureTestTrait.php b/tests/Integration/RestAPI/ContentHelper/ContentHelperFeatureTestTrait.php index 47615307f8..d28768d807 100644 --- a/tests/Integration/RestAPI/ContentHelper/ContentHelperFeatureTestTrait.php +++ b/tests/Integration/RestAPI/ContentHelper/ContentHelperFeatureTestTrait.php @@ -30,27 +30,23 @@ trait ContentHelperFeatureTestTrait { * @since 3.17.0 * * @covers \Parsely\REST_API\Content_Helper\Content_Helper_Feature::is_available_to_current_user - * @uses Parsely\Endpoints\Base_Endpoint::__construct - * @uses Parsely\Parsely::__construct - * @uses Parsely\Parsely::allow_parsely_remote_requests - * @uses Parsely\Parsely::api_secret_is_set - * @uses Parsely\Parsely::are_credentials_managed - * @uses Parsely\Parsely::get_managed_credentials - * @uses Parsely\Parsely::get_options - * @uses Parsely\Parsely::set_default_content_helper_settings_values - * @uses Parsely\Parsely::set_default_full_metadata_in_non_posts - * @uses Parsely\Parsely::set_managed_options - * @uses Parsely\Parsely::site_id_is_set - * @uses Parsely\Permissions::build_pch_permissions_settings_array - * @uses Parsely\Permissions::current_user_can_use_pch_feature - * @uses Parsely\Permissions::get_user_roles_with_edit_posts_cap - * @uses Parsely\REST_API\Base_API_Controller::__construct - * @uses Parsely\REST_API\Base_Endpoint::__construct - * @uses Parsely\REST_API\Base_Endpoint::init - * @uses Parsely\REST_API\Base_Endpoint::apply_capability_filters - * @uses Parsely\REST_API\Base_Endpoint::is_available_to_current_user - * @uses Parsely\REST_API\Base_Endpoint::validate_apikey_and_secret - * @uses Parsely\Utils\Utils::convert_endpoint_to_filter_key + * @uses \Parsely\Endpoints\Base_Endpoint::__construct + * @uses \Parsely\Parsely::api_secret_is_set + * @uses \Parsely\Parsely::get_managed_credentials + * @uses \Parsely\Parsely::get_options + * @uses \Parsely\Parsely::set_default_full_metadata_in_non_posts + * @uses \Parsely\Parsely::site_id_is_set + * @uses \Parsely\Permissions::current_user_can_use_pch_feature + * @uses \Parsely\Permissions::get_user_roles_with_edit_posts_cap + * @uses \Parsely\REST_API\Base_API_Controller::__construct + * @uses \Parsely\REST_API\Base_API_Controller::get_parsely + * @uses \Parsely\REST_API\Base_Endpoint::__construct + * @uses \Parsely\REST_API\Base_Endpoint::apply_capability_filters + * @uses \Parsely\REST_API\Base_Endpoint::get_default_access_capability + * @uses \Parsely\REST_API\Base_Endpoint::init + * @uses \Parsely\REST_API\Base_Endpoint::is_available_to_current_user + * @uses \Parsely\REST_API\Base_Endpoint::validate_site_id_and_secret + * @uses \Parsely\Utils\Utils::convert_endpoint_to_filter_key */ public function test_is_available_to_current_user_returns_true_if_feature_enabled(): void { $this->enable_feature(); @@ -66,27 +62,24 @@ public function test_is_available_to_current_user_returns_true_if_feature_enable * @since 3.17.0 * * @covers \Parsely\REST_API\Content_Helper\Content_Helper_Feature::is_available_to_current_user - * @uses Parsely\Endpoints\Base_Endpoint::__construct - * @uses Parsely\Parsely::__construct - * @uses Parsely\Parsely::allow_parsely_remote_requests - * @uses Parsely\Parsely::api_secret_is_set - * @uses Parsely\Parsely::are_credentials_managed - * @uses Parsely\Parsely::get_managed_credentials - * @uses Parsely\Parsely::get_options - * @uses Parsely\Parsely::set_default_content_helper_settings_values - * @uses Parsely\Parsely::set_default_full_metadata_in_non_posts - * @uses Parsely\Parsely::set_managed_options - * @uses Parsely\Parsely::site_id_is_set - * @uses Parsely\Permissions::build_pch_permissions_settings_array - * @uses Parsely\Permissions::current_user_can_use_pch_feature - * @uses Parsely\Permissions::get_user_roles_with_edit_posts_cap - * @uses Parsely\REST_API\Base_API_Controller::__construct - * @uses Parsely\REST_API\Base_Endpoint::__construct - * @uses Parsely\REST_API\Base_Endpoint::init - * @uses Parsely\REST_API\Base_Endpoint::apply_capability_filters - * @uses Parsely\REST_API\Base_Endpoint::is_available_to_current_user - * @uses Parsely\REST_API\Base_Endpoint::validate_apikey_and_secret - * @uses Parsely\Utils\Utils::convert_endpoint_to_filter_key + * @uses \Parsely\Endpoints\Base_Endpoint::__construct + * @uses \Parsely\Parsely::api_secret_is_set + * @uses \Parsely\Parsely::get_managed_credentials + * @uses \Parsely\Parsely::get_options + * @uses \Parsely\Parsely::set_default_full_metadata_in_non_posts + * @uses \Parsely\Parsely::site_id_is_set + * @uses \Parsely\Permissions::current_user_can_use_pch_feature + * @uses \Parsely\Permissions::get_user_roles_with_edit_posts_cap + * @uses \Parsely\REST_API\Base_API_Controller::__construct + * @uses \Parsely\REST_API\Base_API_Controller::get_parsely + * @uses \Parsely\REST_API\Base_Endpoint::__construct + * @uses \Parsely\REST_API\Base_Endpoint::apply_capability_filters + * @uses \Parsely\REST_API\Base_Endpoint::get_default_access_capability + * @uses \Parsely\REST_API\Base_Endpoint::init + * @uses \Parsely\REST_API\Base_Endpoint::is_available_to_current_user + * @uses \Parsely\REST_API\Base_Endpoint::validate_site_id_and_secret + * @uses \Parsely\Utils\Utils::convert_endpoint_to_filter_key + * / */ public function test_is_available_to_current_user_returns_error_if_feature_disabled(): void { $this->disable_feature(); @@ -102,27 +95,23 @@ public function test_is_available_to_current_user_returns_error_if_feature_disab * @since 3.17.0 * * @covers \Parsely\REST_API\Content_Helper\Content_Helper_Feature::is_available_to_current_user - * @uses Parsely\Endpoints\Base_Endpoint::__construct - * @uses Parsely\Parsely::__construct - * @uses Parsely\Parsely::allow_parsely_remote_requests - * @uses Parsely\Parsely::api_secret_is_set - * @uses Parsely\Parsely::are_credentials_managed - * @uses Parsely\Parsely::get_managed_credentials - * @uses Parsely\Parsely::get_options - * @uses Parsely\Parsely::set_default_content_helper_settings_values - * @uses Parsely\Parsely::set_default_full_metadata_in_non_posts - * @uses Parsely\Parsely::set_managed_options - * @uses Parsely\Parsely::site_id_is_set - * @uses Parsely\Permissions::build_pch_permissions_settings_array - * @uses Parsely\Permissions::current_user_can_use_pch_feature - * @uses Parsely\Permissions::get_user_roles_with_edit_posts_cap - * @uses Parsely\REST_API\Base_API_Controller::__construct - * @uses Parsely\REST_API\Base_Endpoint::__construct - * @uses Parsely\REST_API\Base_Endpoint::init - * @uses Parsely\REST_API\Base_Endpoint::apply_capability_filters - * @uses Parsely\REST_API\Base_Endpoint::is_available_to_current_user - * @uses Parsely\REST_API\Base_Endpoint::validate_apikey_and_secret - * @uses Parsely\Utils\Utils::convert_endpoint_to_filter_key + * @uses \Parsely\Endpoints\Base_Endpoint::__construct + * @uses \Parsely\Parsely::api_secret_is_set + * @uses \Parsely\Parsely::get_managed_credentials + * @uses \Parsely\Parsely::get_options + * @uses \Parsely\Parsely::set_default_full_metadata_in_non_posts + * @uses \Parsely\Parsely::site_id_is_set + * @uses \Parsely\Permissions::current_user_can_use_pch_feature + * @uses \Parsely\Permissions::get_user_roles_with_edit_posts_cap + * @uses \Parsely\REST_API\Base_API_Controller::__construct + * @uses \Parsely\REST_API\Base_API_Controller::get_parsely + * @uses \Parsely\REST_API\Base_Endpoint::__construct + * @uses \Parsely\REST_API\Base_Endpoint::apply_capability_filters + * @uses \Parsely\REST_API\Base_Endpoint::get_default_access_capability + * @uses \Parsely\REST_API\Base_Endpoint::init + * @uses \Parsely\REST_API\Base_Endpoint::is_available_to_current_user + * @uses \Parsely\REST_API\Base_Endpoint::validate_site_id_and_secret + * @uses \Parsely\Utils\Utils::convert_endpoint_to_filter_key */ public function test_is_available_to_current_user_returns_true_if_has_permissions(): void { $this->set_feature_options( @@ -148,27 +137,23 @@ public function test_is_available_to_current_user_returns_true_if_has_permission * @since 3.17.0 * * @covers \Parsely\REST_API\Content_Helper\Content_Helper_Feature::is_available_to_current_user - * @uses Parsely\Endpoints\Base_Endpoint::__construct - * @uses Parsely\Parsely::__construct - * @uses Parsely\Parsely::allow_parsely_remote_requests - * @uses Parsely\Parsely::api_secret_is_set - * @uses Parsely\Parsely::are_credentials_managed - * @uses Parsely\Parsely::get_managed_credentials - * @uses Parsely\Parsely::get_options - * @uses Parsely\Parsely::set_default_content_helper_settings_values - * @uses Parsely\Parsely::set_default_full_metadata_in_non_posts - * @uses Parsely\Parsely::set_managed_options - * @uses Parsely\Parsely::site_id_is_set - * @uses Parsely\Permissions::build_pch_permissions_settings_array - * @uses Parsely\Permissions::current_user_can_use_pch_feature - * @uses Parsely\Permissions::get_user_roles_with_edit_posts_cap - * @uses Parsely\REST_API\Base_API_Controller::__construct - * @uses Parsely\REST_API\Base_Endpoint::__construct - * @uses Parsely\REST_API\Base_Endpoint::init - * @uses Parsely\REST_API\Base_Endpoint::apply_capability_filters - * @uses Parsely\REST_API\Base_Endpoint::is_available_to_current_user - * @uses Parsely\REST_API\Base_Endpoint::validate_apikey_and_secret - * @uses Parsely\Utils\Utils::convert_endpoint_to_filter_key + * @uses \Parsely\Endpoints\Base_Endpoint::__construct + * @uses \Parsely\Parsely::api_secret_is_set + * @uses \Parsely\Parsely::get_managed_credentials + * @uses \Parsely\Parsely::get_options + * @uses \Parsely\Parsely::set_default_full_metadata_in_non_posts + * @uses \Parsely\Parsely::site_id_is_set + * @uses \Parsely\Permissions::current_user_can_use_pch_feature + * @uses \Parsely\Permissions::get_user_roles_with_edit_posts_cap + * @uses \Parsely\REST_API\Base_API_Controller::__construct + * @uses \Parsely\REST_API\Base_API_Controller::get_parsely + * @uses \Parsely\REST_API\Base_Endpoint::__construct + * @uses \Parsely\REST_API\Base_Endpoint::apply_capability_filters + * @uses \Parsely\REST_API\Base_Endpoint::get_default_access_capability + * @uses \Parsely\REST_API\Base_Endpoint::init + * @uses \Parsely\REST_API\Base_Endpoint::is_available_to_current_user + * @uses \Parsely\REST_API\Base_Endpoint::validate_site_id_and_secret + * @uses \Parsely\Utils\Utils::convert_endpoint_to_filter_key */ public function test_is_available_to_current_user_returns_error_if_no_permissions(): void { $this->set_current_user_to_contributor(); @@ -192,18 +177,21 @@ public function test_is_available_to_current_user_returns_error_if_no_permission * * @covers \Parsely\REST_API\Content_Helper\Content_Helper_Feature::is_available_to_current_user * @uses \Parsely\Endpoints\Base_Endpoint::__construct - * @uses \Parsely\Parsely::__construct - * @uses \Parsely\Parsely::allow_parsely_remote_requests - * @uses \Parsely\Parsely::are_credentials_managed + * @uses \Parsely\Parsely::api_secret_is_set * @uses \Parsely\Parsely::get_managed_credentials * @uses \Parsely\Parsely::get_options * @uses \Parsely\Parsely::set_default_full_metadata_in_non_posts - * @uses \Parsely\Parsely::set_managed_options + * @uses \Parsely\Parsely::site_id_is_set * @uses \Parsely\Permissions::current_user_can_use_pch_feature * @uses \Parsely\Permissions::get_user_roles_with_edit_posts_cap * @uses \Parsely\REST_API\Base_API_Controller::__construct + * @uses \Parsely\REST_API\Base_API_Controller::get_parsely * @uses \Parsely\REST_API\Base_Endpoint::__construct + * @uses \Parsely\REST_API\Base_Endpoint::apply_capability_filters + * @uses \Parsely\REST_API\Base_Endpoint::get_default_access_capability * @uses \Parsely\REST_API\Base_Endpoint::init + * @uses \Parsely\REST_API\Base_Endpoint::is_available_to_current_user + * @uses \Parsely\REST_API\Base_Endpoint::validate_site_id_and_secret * @uses \Parsely\Utils\Utils::convert_endpoint_to_filter_key */ public function test_is_available_to_current_user_returns_error_if_no_user(): void { diff --git a/tests/Integration/RestAPI/ContentHelper/EndpointExcerptGeneratorTest.php b/tests/Integration/RestAPI/ContentHelper/EndpointExcerptGeneratorTest.php index c959a8e2d7..9869313747 100644 --- a/tests/Integration/RestAPI/ContentHelper/EndpointExcerptGeneratorTest.php +++ b/tests/Integration/RestAPI/ContentHelper/EndpointExcerptGeneratorTest.php @@ -68,29 +68,28 @@ public function get_endpoint(): \Parsely\REST_API\Base_Endpoint { * * @covers \Parsely\REST_API\Content_Helper\Endpoint_Excerpt_Generator::register_routes * @uses \Parsely\Endpoints\Base_Endpoint::__construct - * @uses \Parsely\Parsely::__construct - * @uses \Parsely\Parsely::allow_parsely_remote_requests * @uses \Parsely\Parsely::api_secret_is_set - * @uses \Parsely\Parsely::are_credentials_managed * @uses \Parsely\Parsely::get_managed_credentials * @uses \Parsely\Parsely::get_options * @uses \Parsely\Parsely::set_default_content_helper_settings_values * @uses \Parsely\Parsely::set_default_full_metadata_in_non_posts - * @uses \Parsely\Parsely::set_managed_options * @uses \Parsely\Parsely::site_id_is_set * @uses \Parsely\Permissions::build_pch_permissions_settings_array * @uses \Parsely\Permissions::current_user_can_use_pch_feature * @uses \Parsely\Permissions::get_user_roles_with_edit_posts_cap * @uses \Parsely\REST_API\Base_API_Controller::__construct * @uses \Parsely\REST_API\Base_API_Controller::get_full_namespace + * @uses \Parsely\REST_API\Base_API_Controller::get_parsely * @uses \Parsely\REST_API\Base_API_Controller::prefix_route * @uses \Parsely\REST_API\Base_Endpoint::__construct - * @uses \Parsely\REST_API\Base_Endpoint::get_endpoint_name * @uses \Parsely\REST_API\Base_Endpoint::get_full_endpoint * @uses \Parsely\REST_API\Base_Endpoint::init * @uses \Parsely\REST_API\Base_Endpoint::is_available_to_current_user * @uses \Parsely\REST_API\Base_Endpoint::register_rest_route * @uses \Parsely\REST_API\Base_Endpoint::validate_site_id_and_secret + * @uses \Parsely\REST_API\Content_Helper\Content_Helper_Controller::get_route_prefix + * @uses \Parsely\REST_API\REST_API_Controller::get_namespace + * @uses \Parsely\REST_API\REST_API_Controller::get_version * @uses \Parsely\Utils\Utils::convert_endpoint_to_filter_key */ public function test_route_is_registered(): void { @@ -114,12 +113,11 @@ public function test_route_is_registered(): void { * * @covers \Parsely\REST_API\Content_Helper\Endpoint_Excerpt_Generator::generate_excerpt * @uses \Parsely\Endpoints\Base_Endpoint::__construct - * @uses \Parsely\Parsely::__construct - * @uses \Parsely\Parsely::allow_parsely_remote_requests - * @uses \Parsely\Parsely::are_credentials_managed - * @uses \Parsely\Parsely::set_managed_options * @uses \Parsely\REST_API\Base_API_Controller::__construct + * @uses \Parsely\REST_API\Base_API_Controller::get_parsely * @uses \Parsely\REST_API\Base_Endpoint::__construct + * @uses \Parsely\REST_API\Base_Endpoint::init + * @uses \Parsely\Utils\Utils::convert_endpoint_to_filter_key */ public function test_generate_excerpt_returns_valid_response(): void { // Mock the Suggest_Brief_API to control the response. @@ -160,12 +158,11 @@ public function test_generate_excerpt_returns_valid_response(): void { * * @covers \Parsely\REST_API\Content_Helper\Endpoint_Excerpt_Generator::generate_excerpt * @uses \Parsely\Endpoints\Base_Endpoint::__construct - * @uses \Parsely\Parsely::__construct - * @uses \Parsely\Parsely::allow_parsely_remote_requests - * @uses \Parsely\Parsely::are_credentials_managed - * @uses \Parsely\Parsely::set_managed_options * @uses \Parsely\REST_API\Base_API_Controller::__construct + * @uses \Parsely\REST_API\Base_API_Controller::get_parsely * @uses \Parsely\REST_API\Base_Endpoint::__construct + * @uses \Parsely\REST_API\Base_Endpoint::init + * @uses \Parsely\Utils\Utils::convert_endpoint_to_filter_key */ public function test_generate_excerpt_returns_error_on_failure(): void { // Mock the Suggest_Brief_API to simulate a failure. diff --git a/tests/Integration/RestAPI/ContentHelper/EndpointSmartLinkingTest.php b/tests/Integration/RestAPI/ContentHelper/EndpointSmartLinkingTest.php index 8389bd7c0c..69cc858b88 100644 --- a/tests/Integration/RestAPI/ContentHelper/EndpointSmartLinkingTest.php +++ b/tests/Integration/RestAPI/ContentHelper/EndpointSmartLinkingTest.php @@ -79,29 +79,30 @@ public function get_endpoint(): \Parsely\REST_API\Base_Endpoint { * * @covers \Parsely\REST_API\Content_Helper\Endpoint_Smart_Linking::register_routes * @uses \Parsely\Endpoints\Base_Endpoint::__construct - * @uses \Parsely\Parsely::__construct - * @uses \Parsely\Parsely::allow_parsely_remote_requests * @uses \Parsely\Parsely::api_secret_is_set - * @uses \Parsely\Parsely::are_credentials_managed * @uses \Parsely\Parsely::get_managed_credentials * @uses \Parsely\Parsely::get_options * @uses \Parsely\Parsely::set_default_content_helper_settings_values * @uses \Parsely\Parsely::set_default_full_metadata_in_non_posts - * @uses \Parsely\Parsely::set_managed_options * @uses \Parsely\Parsely::site_id_is_set * @uses \Parsely\Permissions::build_pch_permissions_settings_array * @uses \Parsely\Permissions::current_user_can_use_pch_feature * @uses \Parsely\Permissions::get_user_roles_with_edit_posts_cap * @uses \Parsely\REST_API\Base_API_Controller::__construct * @uses \Parsely\REST_API\Base_API_Controller::get_full_namespace + * @uses \Parsely\REST_API\Base_API_Controller::get_parsely * @uses \Parsely\REST_API\Base_API_Controller::prefix_route * @uses \Parsely\REST_API\Base_Endpoint::__construct - * @uses \Parsely\REST_API\Base_Endpoint::get_endpoint_name + * @uses \Parsely\REST_API\Base_Endpoint::apply_capability_filters + * @uses \Parsely\REST_API\Base_Endpoint::get_default_access_capability * @uses \Parsely\REST_API\Base_Endpoint::get_full_endpoint * @uses \Parsely\REST_API\Base_Endpoint::init * @uses \Parsely\REST_API\Base_Endpoint::is_available_to_current_user * @uses \Parsely\REST_API\Base_Endpoint::register_rest_route * @uses \Parsely\REST_API\Base_Endpoint::validate_site_id_and_secret + * @uses \Parsely\REST_API\Content_Helper\Content_Helper_Controller::get_route_prefix + * @uses \Parsely\REST_API\REST_API_Controller::get_namespace + * @uses \Parsely\REST_API\REST_API_Controller::get_version * @uses \Parsely\Utils\Utils::convert_endpoint_to_filter_key */ public function test_route_is_registered(): void { @@ -125,12 +126,17 @@ public function test_route_is_registered(): void { * * @covers \Parsely\REST_API\Content_Helper\Endpoint_Smart_Linking::generate_smart_links * @uses \Parsely\Endpoints\Base_Endpoint::__construct - * @uses \Parsely\Parsely::__construct - * @uses \Parsely\Parsely::allow_parsely_remote_requests - * @uses \Parsely\Parsely::are_credentials_managed - * @uses \Parsely\Parsely::set_managed_options + * @uses \Parsely\Models\Base_Model::__construct + * @uses \Parsely\Models\Smart_Link::__construct + * @uses \Parsely\Models\Smart_Link::generate_uid + * @uses \Parsely\Models\Smart_Link::get_post_id_by_url + * @uses \Parsely\Models\Smart_Link::set_href + * @uses \Parsely\Models\Smart_Link::to_array * @uses \Parsely\REST_API\Base_API_Controller::__construct + * @uses \Parsely\REST_API\Base_API_Controller::get_parsely * @uses \Parsely\REST_API\Base_Endpoint::__construct + * @uses \Parsely\REST_API\Base_Endpoint::init + * @uses \Parsely\Utils\Utils::convert_endpoint_to_filter_key */ public function test_generate_smart_links_returns_valid_response(): void { // Mock the Suggest_Linked_Reference_API to control the response. @@ -175,12 +181,11 @@ public function test_generate_smart_links_returns_valid_response(): void { * * @covers \Parsely\REST_API\Content_Helper\Endpoint_Smart_Linking::generate_smart_links * @uses \Parsely\Endpoints\Base_Endpoint::__construct - * @uses \Parsely\Parsely::__construct - * @uses \Parsely\Parsely::allow_parsely_remote_requests - * @uses \Parsely\Parsely::are_credentials_managed - * @uses \Parsely\Parsely::set_managed_options * @uses \Parsely\REST_API\Base_API_Controller::__construct + * @uses \Parsely\REST_API\Base_API_Controller::get_parsely * @uses \Parsely\REST_API\Base_Endpoint::__construct + * @uses \Parsely\REST_API\Base_Endpoint::init + * @uses \Parsely\Utils\Utils::convert_endpoint_to_filter_key */ public function test_generate_smart_links_returns_error_on_failure(): void { // Mock the Suggest_Linked_Reference_API to simulate a failure. @@ -238,15 +243,19 @@ public function test_generate_smart_links_returns_error_on_failure(): void { * @uses \Parsely\Permissions::get_user_roles_with_edit_posts_cap * @uses \Parsely\REST_API\Base_API_Controller::__construct * @uses \Parsely\REST_API\Base_API_Controller::get_full_namespace + * @uses \Parsely\REST_API\Base_API_Controller::get_parsely * @uses \Parsely\REST_API\Base_API_Controller::prefix_route * @uses \Parsely\REST_API\Base_Endpoint::__construct * @uses \Parsely\REST_API\Base_Endpoint::apply_capability_filters - * @uses \Parsely\REST_API\Base_Endpoint::get_endpoint_name + * @uses \Parsely\REST_API\Base_Endpoint::get_default_access_capability * @uses \Parsely\REST_API\Base_Endpoint::get_full_endpoint * @uses \Parsely\REST_API\Base_Endpoint::init * @uses \Parsely\REST_API\Base_Endpoint::is_available_to_current_user * @uses \Parsely\REST_API\Base_Endpoint::register_rest_route * @uses \Parsely\REST_API\Base_Endpoint::validate_site_id_and_secret + * @uses \Parsely\REST_API\Content_Helper\Content_Helper_Controller::get_route_prefix + * @uses \Parsely\REST_API\REST_API_Controller::get_namespace + * @uses \Parsely\REST_API\REST_API_Controller::get_version * @uses \Parsely\Utils\Utils::convert_endpoint_to_filter_key */ public function test_add_smart_link_returns_valid_response(): void { @@ -307,7 +316,6 @@ public function test_add_smart_link_returns_valid_response(): void { * @since 3.17.0 * * @covers \Parsely\REST_API\Content_Helper\Endpoint_Smart_Linking::add_multiple_smart_links - * * @uses \Parsely\Endpoints\Base_Endpoint::__construct * @uses \Parsely\Models\Base_Model::__construct * @uses \Parsely\Models\Base_Model::serialize @@ -335,15 +343,19 @@ public function test_add_smart_link_returns_valid_response(): void { * @uses \Parsely\Permissions::get_user_roles_with_edit_posts_cap * @uses \Parsely\REST_API\Base_API_Controller::__construct * @uses \Parsely\REST_API\Base_API_Controller::get_full_namespace + * @uses \Parsely\REST_API\Base_API_Controller::get_parsely * @uses \Parsely\REST_API\Base_API_Controller::prefix_route * @uses \Parsely\REST_API\Base_Endpoint::__construct * @uses \Parsely\REST_API\Base_Endpoint::apply_capability_filters - * @uses \Parsely\REST_API\Base_Endpoint::get_endpoint_name + * @uses \Parsely\REST_API\Base_Endpoint::get_default_access_capability * @uses \Parsely\REST_API\Base_Endpoint::get_full_endpoint * @uses \Parsely\REST_API\Base_Endpoint::init * @uses \Parsely\REST_API\Base_Endpoint::is_available_to_current_user * @uses \Parsely\REST_API\Base_Endpoint::register_rest_route * @uses \Parsely\REST_API\Base_Endpoint::validate_site_id_and_secret + * @uses \Parsely\REST_API\Content_Helper\Content_Helper_Controller::get_route_prefix + * @uses \Parsely\REST_API\REST_API_Controller::get_namespace + * @uses \Parsely\REST_API\REST_API_Controller::get_version * @uses \Parsely\Utils\Utils::convert_endpoint_to_filter_key */ public function test_add_multiple_smart_links_returns_valid_response(): void { diff --git a/tests/Integration/RestAPI/ContentHelper/EndpointTitleSuggestionsTest.php b/tests/Integration/RestAPI/ContentHelper/EndpointTitleSuggestionsTest.php index a54e9391b3..a3a5f47fc6 100644 --- a/tests/Integration/RestAPI/ContentHelper/EndpointTitleSuggestionsTest.php +++ b/tests/Integration/RestAPI/ContentHelper/EndpointTitleSuggestionsTest.php @@ -67,29 +67,28 @@ public function get_endpoint(): \Parsely\REST_API\Base_Endpoint { * * @covers \Parsely\REST_API\Content_Helper\Endpoint_Title_Suggestions::register_routes * @uses \Parsely\Endpoints\Base_Endpoint::__construct - * @uses \Parsely\Parsely::__construct - * @uses \Parsely\Parsely::allow_parsely_remote_requests * @uses \Parsely\Parsely::api_secret_is_set - * @uses \Parsely\Parsely::are_credentials_managed * @uses \Parsely\Parsely::get_managed_credentials * @uses \Parsely\Parsely::get_options * @uses \Parsely\Parsely::set_default_content_helper_settings_values * @uses \Parsely\Parsely::set_default_full_metadata_in_non_posts - * @uses \Parsely\Parsely::set_managed_options * @uses \Parsely\Parsely::site_id_is_set * @uses \Parsely\Permissions::build_pch_permissions_settings_array * @uses \Parsely\Permissions::current_user_can_use_pch_feature * @uses \Parsely\Permissions::get_user_roles_with_edit_posts_cap * @uses \Parsely\REST_API\Base_API_Controller::__construct * @uses \Parsely\REST_API\Base_API_Controller::get_full_namespace + * @uses \Parsely\REST_API\Base_API_Controller::get_parsely * @uses \Parsely\REST_API\Base_API_Controller::prefix_route * @uses \Parsely\REST_API\Base_Endpoint::__construct - * @uses \Parsely\REST_API\Base_Endpoint::get_endpoint_name * @uses \Parsely\REST_API\Base_Endpoint::get_full_endpoint * @uses \Parsely\REST_API\Base_Endpoint::init * @uses \Parsely\REST_API\Base_Endpoint::is_available_to_current_user * @uses \Parsely\REST_API\Base_Endpoint::register_rest_route * @uses \Parsely\REST_API\Base_Endpoint::validate_site_id_and_secret + * @uses \Parsely\REST_API\Content_Helper\Content_Helper_Controller::get_route_prefix + * @uses \Parsely\REST_API\REST_API_Controller::get_namespace + * @uses \Parsely\REST_API\REST_API_Controller::get_version * @uses \Parsely\Utils\Utils::convert_endpoint_to_filter_key */ public function test_route_is_registered(): void { @@ -116,6 +115,7 @@ public function test_route_is_registered(): void { * @uses \Parsely\Parsely::are_credentials_managed * @uses \Parsely\Parsely::set_managed_options * @uses \Parsely\REST_API\Base_API_Controller::__construct + * @uses \Parsely\REST_API\Base_API_Controller::get_parsely * @uses \Parsely\REST_API\Base_Endpoint::__construct * @uses \Parsely\REST_API\Base_Endpoint::init * @uses \Parsely\Utils\Utils::convert_endpoint_to_filter_key @@ -164,6 +164,7 @@ public function test_generate_titles_returns_valid_response(): void { * @uses \Parsely\Parsely::are_credentials_managed * @uses \Parsely\Parsely::set_managed_options * @uses \Parsely\REST_API\Base_API_Controller::__construct + * @uses \Parsely\REST_API\Base_API_Controller::get_parsely * @uses \Parsely\REST_API\Base_Endpoint::__construct * @uses \Parsely\REST_API\Base_Endpoint::init * @uses \Parsely\Utils\Utils::convert_endpoint_to_filter_key diff --git a/tests/Integration/RestAPI/Stats/EndpointPostTest.php b/tests/Integration/RestAPI/Stats/EndpointPostTest.php new file mode 100644 index 0000000000..acba94a64b --- /dev/null +++ b/tests/Integration/RestAPI/Stats/EndpointPostTest.php @@ -0,0 +1,674 @@ +api_controller = new Stats_Controller( $this->parsely ); + $this->endpoint = new Endpoint_Post( $this->api_controller ); + + parent::set_up(); + } + + /** + * Get the test endpoint instance. + * + * @since 3.17.0 + * + * @return Endpoint_Post + */ + public function get_endpoint(): \Parsely\REST_API\Base_Endpoint { + return $this->endpoint; + } + + /** + * Test that the route is registered. + * + * @since 3.17.0 + * + * @covers \Parsely\REST_API\Stats\Endpoint_Post::register_routes + * @uses \Parsely\Endpoints\Base_Endpoint::__construct + * @uses \Parsely\Parsely::api_secret_is_set + * @uses \Parsely\Parsely::get_managed_credentials + * @uses \Parsely\Parsely::get_options + * @uses \Parsely\Parsely::set_default_content_helper_settings_values + * @uses \Parsely\Parsely::set_default_full_metadata_in_non_posts + * @uses \Parsely\Parsely::site_id_is_set + * @uses \Parsely\Permissions::build_pch_permissions_settings_array + * @uses \Parsely\Permissions::get_user_roles_with_edit_posts_cap + * @uses \Parsely\REST_API\Base_API_Controller::__construct + * @uses \Parsely\REST_API\Base_API_Controller::get_full_namespace + * @uses \Parsely\REST_API\Base_API_Controller::get_parsely + * @uses \Parsely\REST_API\Base_API_Controller::prefix_route + * @uses \Parsely\REST_API\Base_Endpoint::__construct + * @uses \Parsely\REST_API\Base_Endpoint::get_full_endpoint + * @uses \Parsely\REST_API\Base_Endpoint::get_registered_routes + * @uses \Parsely\REST_API\Base_Endpoint::init + * @uses \Parsely\REST_API\Base_Endpoint::is_available_to_current_user + * @uses \Parsely\REST_API\Base_Endpoint::register_rest_route + * @uses \Parsely\REST_API\Base_Endpoint::validate_site_id_and_secret + * @uses \Parsely\REST_API\REST_API_Controller::get_namespace + * @uses \Parsely\REST_API\REST_API_Controller::get_version + * @uses \Parsely\REST_API\Stats\Endpoint_Post::__construct + * @uses \Parsely\REST_API\Stats\Endpoint_Post::get_endpoint_name + * @uses \Parsely\REST_API\Stats\Post_Data_Trait::get_itm_source_param_args + * @uses \Parsely\REST_API\Stats\Related_Posts_Trait::get_related_posts_param_args + * @uses \Parsely\REST_API\Stats\Stats_Controller::get_route_prefix + * @uses \Parsely\REST_API\Use_Post_ID_Parameter_Trait::register_rest_route_with_post_id + * @uses \Parsely\Utils\Utils::convert_endpoint_to_filter_key + */ + public function test_route_is_registered(): void { + $routes = rest_get_server()->get_routes(); + $registered_routes = $this->get_endpoint()->get_registered_routes(); + + // Assert that the routes are registered when the filter returns true. + foreach ( $registered_routes as $route ) { + $expected_route = $this->get_endpoint()->get_full_endpoint( $route ); + $route_data = $routes[ $expected_route ]; + self::assertArrayHasKey( $expected_route, $routes ); + + // Check that the route is associated with the GET method, since all + // the routes in this endpoint are GET routes. + self::assertArrayHasKey( 'GET', $route_data[0]['methods'] ); + } + } + + /** + * Test that the endpoint is not available if the API key is not set. + * + * @covers \Parsely\REST_API\Stats\Endpoint_Post::is_available_to_current_user + * @uses \Parsely\Endpoints\Base_Endpoint::__construct + * @uses \Parsely\Parsely::api_secret_is_set + * @uses \Parsely\Parsely::get_managed_credentials + * @uses \Parsely\Parsely::get_options + * @uses \Parsely\Parsely::set_default_content_helper_settings_values + * @uses \Parsely\Parsely::set_default_full_metadata_in_non_posts + * @uses \Parsely\Parsely::site_id_is_set + * @uses \Parsely\Permissions::build_pch_permissions_settings_array + * @uses \Parsely\Permissions::get_user_roles_with_edit_posts_cap + * @uses \Parsely\REST_API\Base_API_Controller::__construct + * @uses \Parsely\REST_API\Base_API_Controller::get_full_namespace + * @uses \Parsely\REST_API\Base_API_Controller::get_parsely + * @uses \Parsely\REST_API\Base_API_Controller::prefix_route + * @uses \Parsely\REST_API\Base_Endpoint::__construct + * @uses \Parsely\REST_API\Base_Endpoint::get_full_endpoint + * @uses \Parsely\REST_API\Base_Endpoint::init + * @uses \Parsely\REST_API\Base_Endpoint::register_rest_route + * @uses \Parsely\REST_API\Base_Endpoint::validate_site_id_and_secret + * @uses \Parsely\REST_API\REST_API_Controller::get_namespace + * @uses \Parsely\REST_API\REST_API_Controller::get_version + * @uses \Parsely\REST_API\Stats\Endpoint_Post::__construct + * @uses \Parsely\REST_API\Stats\Endpoint_Post::get_endpoint_name + * @uses \Parsely\REST_API\Stats\Endpoint_Post::register_routes + * @uses \Parsely\REST_API\Stats\Post_Data_Trait::get_itm_source_param_args + * @uses \Parsely\REST_API\Stats\Related_Posts_Trait::get_related_posts_param_args + * @uses \Parsely\REST_API\Stats\Stats_Controller::get_route_prefix + * @uses \Parsely\REST_API\Use_Post_ID_Parameter_Trait::register_rest_route_with_post_id + * @uses \Parsely\REST_API\Use_Post_ID_Parameter_Trait::validate_post_id + * @uses \Parsely\Utils\Utils::convert_endpoint_to_filter_key + */ + public function test_access_error_if_api_secret_is_not_set(): void { + $test_post_id = $this->create_test_post(); + TestCase::set_options( + array( + 'apikey' => 'test', + ) + ); + $route = $this->get_endpoint()->get_full_endpoint( '/' . $test_post_id . '/details' ); + $response = rest_get_server()->dispatch( + new WP_REST_Request( 'GET', $route ) + ); + + $error = $response->as_error(); + self::assertNotNull( $error ); + self::assertSame( 403, $response->get_status() ); + self::assertSame( 'parsely_api_secret_not_set', $error->get_error_code() ); + self::assertSame( + 'A Parse.ly API Secret must be set in site options to use this endpoint', + $error->get_error_message() + ); + } + + + /** + * Verifies forbidden error when current user doesn't have proper + * capabilities. + * + * @covers \Parsely\REST_API\Stats\Endpoint_Post::is_available_to_current_user + * @uses \Parsely\Endpoints\Base_Endpoint::__construct + * @uses \Parsely\Parsely::api_secret_is_set + * @uses \Parsely\Parsely::get_managed_credentials + * @uses \Parsely\Parsely::get_options + * @uses \Parsely\Parsely::set_default_content_helper_settings_values + * @uses \Parsely\Parsely::set_default_full_metadata_in_non_posts + * @uses \Parsely\Parsely::site_id_is_set + * @uses \Parsely\Permissions::build_pch_permissions_settings_array + * @uses \Parsely\Permissions::get_user_roles_with_edit_posts_cap + * @uses \Parsely\REST_API\Base_API_Controller::__construct + * @uses \Parsely\REST_API\Base_API_Controller::get_full_namespace + * @uses \Parsely\REST_API\Base_API_Controller::get_parsely + * @uses \Parsely\REST_API\Base_API_Controller::prefix_route + * @uses \Parsely\REST_API\Base_Endpoint::__construct + * @uses \Parsely\REST_API\Base_Endpoint::apply_capability_filters + * @uses \Parsely\REST_API\Base_Endpoint::get_default_access_capability + * @uses \Parsely\REST_API\Base_Endpoint::get_full_endpoint + * @uses \Parsely\REST_API\Base_Endpoint::init + * @uses \Parsely\REST_API\Base_Endpoint::register_rest_route + * @uses \Parsely\REST_API\Base_Endpoint::validate_site_id_and_secret + * @uses \Parsely\REST_API\REST_API_Controller::get_namespace + * @uses \Parsely\REST_API\REST_API_Controller::get_version + * @uses \Parsely\REST_API\Stats\Endpoint_Post::__construct + * @uses \Parsely\REST_API\Stats\Endpoint_Post::get_endpoint_name + * @uses \Parsely\REST_API\Stats\Endpoint_Post::register_routes + * @uses \Parsely\REST_API\Stats\Post_Data_Trait::get_itm_source_param_args + * @uses \Parsely\REST_API\Stats\Related_Posts_Trait::get_related_posts_param_args + * @uses \Parsely\REST_API\Stats\Stats_Controller::get_route_prefix + * @uses \Parsely\REST_API\Use_Post_ID_Parameter_Trait::register_rest_route_with_post_id + * @uses \Parsely\REST_API\Use_Post_ID_Parameter_Trait::validate_post_id + * @uses \Parsely\Utils\Utils::convert_endpoint_to_filter_key + */ + public function test_access_of_stats_post_endpoint_is_forbidden(): void { + $test_post_id = $this->create_test_post(); + TestCase::set_options( + array( + 'apikey' => 'test-api-key', + 'api_secret' => 'test-secret', + ) + ); + $this->set_current_user_to_contributor(); + + $route = $this->get_endpoint()->get_full_endpoint( '/' . $test_post_id . '/details' ); + $response = rest_get_server()->dispatch( + new WP_REST_Request( 'GET', $route ) + ); + /** + * Variable. + * + * @var WP_Error $error + */ + $error = $response->as_error(); + + self::assertSame( 403, $response->get_status() ); + self::assertSame( 'rest_forbidden', $error->get_error_code() ); + self::assertSame( + 'Sorry, you are not allowed to do that.', + $error->get_error_message() + ); + } + + /** + * Verifies that calls to the `stats/{post_id}/details` return the expected data, in the + * expected format. + * + * @since 3.17.0 + * + * @covers \Parsely\REST_API\Stats\Endpoint_Post::get_post_details + * @uses \Parsely\Endpoints\Base_Endpoint::__construct + * @uses \Parsely\Parsely::api_secret_is_set + * @uses \Parsely\Parsely::get_api_secret + * @uses \Parsely\Parsely::get_dash_url + * @uses \Parsely\Parsely::get_managed_credentials + * @uses \Parsely\Parsely::get_options + * @uses \Parsely\Parsely::get_site_id + * @uses \Parsely\Parsely::get_url_with_itm_source + * @uses \Parsely\Parsely::set_default_content_helper_settings_values + * @uses \Parsely\Parsely::set_default_full_metadata_in_non_posts + * @uses \Parsely\Parsely::site_id_is_set + * @uses \Parsely\Permissions::build_pch_permissions_settings_array + * @uses \Parsely\Permissions::get_user_roles_with_edit_posts_cap + * @uses \Parsely\REST_API\Base_API_Controller::__construct + * @uses \Parsely\REST_API\Base_API_Controller::get_full_namespace + * @uses \Parsely\REST_API\Base_API_Controller::get_parsely + * @uses \Parsely\REST_API\Base_API_Controller::prefix_route + * @uses \Parsely\REST_API\Base_Endpoint::__construct + * @uses \Parsely\REST_API\Base_Endpoint::apply_capability_filters + * @uses \Parsely\REST_API\Base_Endpoint::get_default_access_capability + * @uses \Parsely\REST_API\Base_Endpoint::get_full_endpoint + * @uses \Parsely\REST_API\Base_Endpoint::init + * @uses \Parsely\REST_API\Base_Endpoint::is_available_to_current_user + * @uses \Parsely\REST_API\Base_Endpoint::register_rest_route + * @uses \Parsely\REST_API\Base_Endpoint::validate_site_id_and_secret + * @uses \Parsely\REST_API\REST_API_Controller::get_namespace + * @uses \Parsely\REST_API\REST_API_Controller::get_version + * @uses \Parsely\REST_API\Stats\Endpoint_Post::__construct + * @uses \Parsely\REST_API\Stats\Endpoint_Post::get_endpoint_name + * @uses \Parsely\REST_API\Stats\Endpoint_Post::register_routes + * @uses \Parsely\REST_API\Stats\Post_Data_Trait::extract_post_data + * @uses \Parsely\REST_API\Stats\Post_Data_Trait::get_itm_source_param_args + * @uses \Parsely\REST_API\Stats\Post_Data_Trait::set_itm_source_from_request + * @uses \Parsely\REST_API\Stats\Related_Posts_Trait::get_related_posts_param_args + * @uses \Parsely\REST_API\Stats\Stats_Controller::get_route_prefix + * @uses \Parsely\REST_API\Use_Post_ID_Parameter_Trait::register_rest_route_with_post_id + * @uses \Parsely\REST_API\Use_Post_ID_Parameter_Trait::validate_post_id + * @uses \Parsely\RemoteAPI\Base_Endpoint_Remote::get_api_url + * @uses \Parsely\RemoteAPI\Base_Endpoint_Remote::get_items + * @uses \Parsely\RemoteAPI\Base_Endpoint_Remote::get_request_options + * @uses \Parsely\RemoteAPI\Base_Endpoint_Remote::validate_required_constraints + * @uses \Parsely\Utils\Utils::convert_endpoint_to_filter_key + * @uses \Parsely\Utils\Utils::get_formatted_duration + * @uses \Parsely\Utils\Utils::parsely_is_https_supported + */ + public function test_get_details(): void { + $test_post_id = $this->create_test_post(); + $route = $this->get_endpoint()->get_full_endpoint( '/' . $test_post_id . '/details' ); + + TestCase::set_options( + array( + 'apikey' => 'example.com', + 'api_secret' => 'test-secret', + ) + ); + $this->set_current_user_to_admin(); + + $dispatched = 0; + + add_filter( + 'pre_http_request', + function () use ( &$dispatched ): array { + $dispatched++; + return array( + 'body' => ' + {"data":[{ + "avg_engaged": 1.911, + "metrics": { + "views": 2158, + "visitors": 1537 + }, + "url": "https://example.com" + }]} + ', + ); + } + ); + + $response = rest_get_server()->dispatch( new WP_REST_Request( 'GET', $route ) ); + + /** + * The response data. + * + * @var array $response_data + */ + $response_data = $response->get_data(); + + self::assertSame( 1, $dispatched ); + self::assertSame( 200, $response->get_status() ); + self::assertEquals( + array( + array( + 'avgEngaged' => '1:55', + 'dashUrl' => Parsely::DASHBOARD_BASE_URL . '/example.com/find?url=https%3A%2F%2Fexample.com', + 'id' => 'https://example.com', + 'postId' => 0, + 'url' => 'https://example.com', + 'views' => '2,158', + 'visitors' => '1,537', + 'rawUrl' => 'https://example.com', + ), + ), + $response_data['data'] + ); + } + + /** + * Verifies that calls to the `stats/{post_id}/referrers` return the expected data, in the + * expected format. + * + * @since 3.17.0 + * + * @covers \Parsely\REST_API\Stats\Endpoint_Post::get_post_referrers + * @uses \Parsely\Endpoints\Base_Endpoint::__construct + * @uses \Parsely\Parsely::api_secret_is_set + * @uses \Parsely\Parsely::get_api_secret + * @uses \Parsely\Parsely::get_managed_credentials + * @uses \Parsely\Parsely::get_options + * @uses \Parsely\Parsely::get_site_id + * @uses \Parsely\Parsely::set_default_content_helper_settings_values + * @uses \Parsely\Parsely::set_default_full_metadata_in_non_posts + * @uses \Parsely\Parsely::site_id_is_set + * @uses \Parsely\Permissions::build_pch_permissions_settings_array + * @uses \Parsely\Permissions::get_user_roles_with_edit_posts_cap + * @uses \Parsely\REST_API\Base_API_Controller::__construct + * @uses \Parsely\REST_API\Base_API_Controller::get_full_namespace + * @uses \Parsely\REST_API\Base_API_Controller::get_parsely + * @uses \Parsely\REST_API\Base_API_Controller::prefix_route + * @uses \Parsely\REST_API\Base_Endpoint::__construct + * @uses \Parsely\REST_API\Base_Endpoint::apply_capability_filters + * @uses \Parsely\REST_API\Base_Endpoint::get_default_access_capability + * @uses \Parsely\REST_API\Base_Endpoint::get_full_endpoint + * @uses \Parsely\REST_API\Base_Endpoint::init + * @uses \Parsely\REST_API\Base_Endpoint::is_available_to_current_user + * @uses \Parsely\REST_API\Base_Endpoint::register_rest_route + * @uses \Parsely\REST_API\Base_Endpoint::validate_site_id_and_secret + * @uses \Parsely\REST_API\REST_API_Controller::get_namespace + * @uses \Parsely\REST_API\REST_API_Controller::get_version + * @uses \Parsely\REST_API\Stats\Endpoint_Post::__construct + * @uses \Parsely\REST_API\Stats\Endpoint_Post::generate_referrer_types_data + * @uses \Parsely\REST_API\Stats\Endpoint_Post::generate_referrers_data + * @uses \Parsely\REST_API\Stats\Endpoint_Post::get_endpoint_name + * @uses \Parsely\REST_API\Stats\Endpoint_Post::get_i18n_percentage + * @uses \Parsely\REST_API\Stats\Endpoint_Post::register_routes + * @uses \Parsely\REST_API\Stats\Post_Data_Trait::get_itm_source_param_args + * @uses \Parsely\REST_API\Stats\Post_Data_Trait::set_itm_source_from_request + * @uses \Parsely\REST_API\Stats\Related_Posts_Trait::get_related_posts_param_args + * @uses \Parsely\REST_API\Stats\Stats_Controller::get_route_prefix + * @uses \Parsely\REST_API\Use_Post_ID_Parameter_Trait::register_rest_route_with_post_id + * @uses \Parsely\REST_API\Use_Post_ID_Parameter_Trait::validate_post_id + * @uses \Parsely\RemoteAPI\Base_Endpoint_Remote::get_api_url + * @uses \Parsely\RemoteAPI\Base_Endpoint_Remote::get_items + * @uses \Parsely\RemoteAPI\Base_Endpoint_Remote::get_request_options + * @uses \Parsely\RemoteAPI\Base_Endpoint_Remote::validate_required_constraints + * @uses \Parsely\Utils\Utils::convert_endpoint_to_filter_key + * @uses \Parsely\Utils\Utils::convert_to_positive_integer + */ + public function test_get_referrers(): void { + $test_post_id = $this->create_test_post(); + $route = $this->get_endpoint()->get_full_endpoint( '/' . $test_post_id . '/referrers' ); + + TestCase::set_options( + array( + 'apikey' => 'example.com', + 'api_secret' => 'test-secret', + ) + ); + $this->set_current_user_to_admin(); + + $dispatched = 0; + + add_filter( + 'pre_http_request', + function () use ( &$dispatched ): array { + $dispatched++; + return array( + 'body' => '{"data":[ + { + "metrics": {"referrers_views": 1500}, + "name": "google", + "type": "search" + }, + { + "metrics": {"referrers_views": 100}, + "name": "blog.parse.ly", + "type": "internal" + }, + { + "metrics": {"referrers_views": 50}, + "name": "bing", + "type": "search" + }, + { + "metrics": {"referrers_views": 30}, + "name": "facebook.com", + "type": "social" + }, + { + "metrics": {"referrers_views": 10}, + "name": "okt.to", + "type": "other" + }, + { + "metrics": {"referrers_views": 10}, + "name": "yandex", + "type": "search" + }, + { + "metrics": {"referrers_views": 10}, + "name": "parse.ly", + "type": "internal" + }, + { + "metrics": {"referrers_views": 10}, + "name": "yahoo!", + "type": "search" + }, + { + "metrics": {"referrers_views": 5}, + "name": "site1.com", + "type": "other" + }, + { + "metrics": {"referrers_views": 5}, + "name": "link.site2.com", + "type": "other" + } + ]}', + ); + } + ); + + $expected_top = (object) array( + 'direct' => (object) array( + 'views' => '770', + 'viewsPercentage' => '30.80', + 'datasetViewsPercentage' => '31.43', + ), + 'google' => (object) array( + 'views' => '1,500', + 'viewsPercentage' => '60.00', + 'datasetViewsPercentage' => '61.22', + ), + 'blog.parse.ly' => (object) array( + 'views' => '100', + 'viewsPercentage' => '4.00', + 'datasetViewsPercentage' => '4.08', + ), + 'bing' => (object) array( + 'views' => '50', + 'viewsPercentage' => '2.00', + 'datasetViewsPercentage' => '2.04', + ), + 'facebook.com' => (object) array( + 'views' => '30', + 'viewsPercentage' => '1.20', + 'datasetViewsPercentage' => '1.22', + ), + 'totals' => (object) array( + 'views' => '2,450', + 'viewsPercentage' => '98.00', + 'datasetViewsPercentage' => '100.00', + ), + ); + + $expected_types = (object) array( + 'social' => (object) array( + 'views' => '30', + 'viewsPercentage' => '1.20', + ), + 'search' => (object) array( + 'views' => '1,570', + 'viewsPercentage' => '62.80', + ), + 'other' => (object) array( + 'views' => '20', + 'viewsPercentage' => '0.80', + ), + 'internal' => (object) array( + 'views' => '110', + 'viewsPercentage' => '4.40', + ), + 'direct' => (object) array( + 'views' => '770', + 'viewsPercentage' => '30.80', + ), + 'totals' => (object) array( + 'views' => '2,500', + 'viewsPercentage' => '100.00', + ), + ); + + $request = new WP_REST_Request( 'GET', $route ); + $request->set_param( 'total_views', '2,500' ); + + $response = rest_get_server()->dispatch( $request ); + + /** + * The response data. + * + * @var array $response_data + */ + $response_data = $response->get_data(); + + self::assertSame( 1, $dispatched ); + self::assertSame( 200, $response->get_status() ); + self::assertEquals( + array( + 'top' => $expected_top, + 'types' => $expected_types, + ), + $response_data['data'] + ); + } + + + /** + * Verifies that calls to the `stats/{post_id}/related` return the expected data, in the + * expected format. + * + * @since 3.17.0 + * + * @covers \Parsely\REST_API\Stats\Endpoint_Post::get_related_posts + * @uses \Parsely\Endpoints\Base_Endpoint::__construct + * @uses \Parsely\Parsely::api_secret_is_set + * @uses \Parsely\Parsely::get_api_secret + * @uses \Parsely\Parsely::get_managed_credentials + * @uses \Parsely\Parsely::get_options + * @uses \Parsely\Parsely::get_site_id + * @uses \Parsely\Parsely::get_url_with_itm_source + * @uses \Parsely\Parsely::set_default_content_helper_settings_values + * @uses \Parsely\Parsely::set_default_full_metadata_in_non_posts + * @uses \Parsely\Parsely::site_id_is_set + * @uses \Parsely\Permissions::build_pch_permissions_settings_array + * @uses \Parsely\Permissions::get_user_roles_with_edit_posts_cap + * @uses \Parsely\REST_API\Base_API_Controller::__construct + * @uses \Parsely\REST_API\Base_API_Controller::get_full_namespace + * @uses \Parsely\REST_API\Base_API_Controller::get_parsely + * @uses \Parsely\REST_API\Base_API_Controller::prefix_route + * @uses \Parsely\REST_API\Base_Endpoint::__construct + * @uses \Parsely\REST_API\Base_Endpoint::apply_capability_filters + * @uses \Parsely\REST_API\Base_Endpoint::get_default_access_capability + * @uses \Parsely\REST_API\Base_Endpoint::get_full_endpoint + * @uses \Parsely\REST_API\Base_Endpoint::init + * @uses \Parsely\REST_API\Base_Endpoint::is_available_to_current_user + * @uses \Parsely\REST_API\Base_Endpoint::register_rest_route + * @uses \Parsely\REST_API\Base_Endpoint::validate_site_id_and_secret + * @uses \Parsely\REST_API\REST_API_Controller::get_namespace + * @uses \Parsely\REST_API\REST_API_Controller::get_version + * @uses \Parsely\REST_API\Stats\Endpoint_Post::__construct + * @uses \Parsely\REST_API\Stats\Endpoint_Post::get_endpoint_name + * @uses \Parsely\REST_API\Stats\Endpoint_Post::register_routes + * @uses \Parsely\REST_API\Stats\Post_Data_Trait::get_itm_source_param_args + * @uses \Parsely\REST_API\Stats\Post_Data_Trait::set_itm_source_from_request + * @uses \Parsely\REST_API\Stats\Related_Posts_Trait::get_related_posts_of_url + * @uses \Parsely\REST_API\Stats\Related_Posts_Trait::get_related_posts_param_args + * @uses \Parsely\REST_API\Stats\Stats_Controller::get_route_prefix + * @uses \Parsely\REST_API\Use_Post_ID_Parameter_Trait::register_rest_route_with_post_id + * @uses \Parsely\REST_API\Use_Post_ID_Parameter_Trait::validate_post_id + * @uses \Parsely\RemoteAPI\Base_Endpoint_Remote::get_api_url + * @uses \Parsely\RemoteAPI\Base_Endpoint_Remote::get_items + * @uses \Parsely\RemoteAPI\Base_Endpoint_Remote::get_request_options + * @uses \Parsely\RemoteAPI\Base_Endpoint_Remote::validate_required_constraints + * @uses \Parsely\Utils\Utils::convert_endpoint_to_filter_key + */ + public function test_get_related_posts(): void { + $test_post_id = $this->create_test_post(); + $route = $this->get_endpoint()->get_full_endpoint( '/' . $test_post_id . '/related' ); + + TestCase::set_options( + array( + 'apikey' => 'example.com', + 'api_secret' => 'test-secret', + ) + ); + $this->set_current_user_to_admin(); + + $dispatched = 0; + + add_filter( + 'pre_http_request', + function () use ( &$dispatched ): array { + $dispatched++; + return array( + 'body' => '{"data":[ + { + "image_url":"https:\/\/example.com\/img.png", + "thumb_url_medium":"https:\/\/example.com\/thumb.png", + "title":"something", + "url":"https:\/\/example.com" + }, + { + "image_url":"https:\/\/example.com\/img2.png", + "thumb_url_medium":"https:\/\/example.com\/thumb2.png", + "title":"something2", + "url":"https:\/\/example.com\/2" + } + ]}', + ); + } + ); + + $response = rest_get_server()->dispatch( new WP_REST_Request( 'GET', $route ) ); + /** + * The response data. + * + * @var array $response_data + */ + $response_data = $response->get_data(); + + self::assertSame( 1, $dispatched ); + self::assertSame( 200, $response->get_status() ); + self::assertEquals( + array( + (object) array( + 'image_url' => 'https://example.com/img.png', + 'thumb_url_medium' => 'https://example.com/thumb.png', + 'title' => 'something', + 'url' => 'https://example.com', + ), + (object) array( + 'image_url' => 'https://example.com/img2.png', + 'thumb_url_medium' => 'https://example.com/thumb2.png', + 'title' => 'something2', + 'url' => 'https://example.com/2', + ), + ), + $response_data['data'] + ); + } +} diff --git a/tests/Integration/RestAPI/Stats/EndpointPostsTest.php b/tests/Integration/RestAPI/Stats/EndpointPostsTest.php new file mode 100644 index 0000000000..0cecdd1cb5 --- /dev/null +++ b/tests/Integration/RestAPI/Stats/EndpointPostsTest.php @@ -0,0 +1,347 @@ +api_controller = new Stats_Controller( $this->parsely ); + $this->endpoint = new Endpoint_Posts( $this->api_controller ); + + parent::set_up(); + } + + /** + * Get the test endpoint instance. + * + * @since 3.17.0 + * + * @return Endpoint_Posts + */ + public function get_endpoint(): \Parsely\REST_API\Base_Endpoint { + return $this->endpoint; + } + + /** + * Test the route is registered. + * + * @since 3.17.0 + * + * @covers \Parsely\REST_API\Stats\Endpoint_Posts::register_routes + * @uses \Parsely\Endpoints\Base_Endpoint::__construct + * @uses \Parsely\Parsely::api_secret_is_set + * @uses \Parsely\Parsely::get_managed_credentials + * @uses \Parsely\Parsely::get_options + * @uses \Parsely\Parsely::set_default_content_helper_settings_values + * @uses \Parsely\Parsely::set_default_full_metadata_in_non_posts + * @uses \Parsely\Parsely::site_id_is_set + * @uses \Parsely\Permissions::build_pch_permissions_settings_array + * @uses \Parsely\Permissions::get_user_roles_with_edit_posts_cap + * @uses \Parsely\REST_API\Base_API_Controller::__construct + * @uses \Parsely\REST_API\Base_API_Controller::get_full_namespace + * @uses \Parsely\REST_API\Base_API_Controller::get_parsely + * @uses \Parsely\REST_API\Base_API_Controller::prefix_route + * @uses \Parsely\REST_API\Base_Endpoint::__construct + * @uses \Parsely\REST_API\Base_Endpoint::get_full_endpoint + * @uses \Parsely\REST_API\Base_Endpoint::init + * @uses \Parsely\REST_API\Base_Endpoint::is_available_to_current_user + * @uses \Parsely\REST_API\Base_Endpoint::register_rest_route + * @uses \Parsely\REST_API\Base_Endpoint::validate_site_id_and_secret + * @uses \Parsely\REST_API\REST_API_Controller::get_namespace + * @uses \Parsely\REST_API\REST_API_Controller::get_version + * @uses \Parsely\REST_API\Stats\Stats_Controller::get_route_prefix + * @uses \Parsely\Utils\Utils::convert_endpoint_to_filter_key + */ + public function test_route_is_registered(): void { + $routes = rest_get_server()->get_routes(); + + // Check that the excerpt-generator/generate route is registered. + $expected_route = $this->get_endpoint()->get_full_endpoint( '/' ); + self::assertArrayHasKey( $expected_route, $routes ); + + // Check that the route is associated with the POST method. + $route_data = $routes[ $expected_route ]; + self::assertArrayHasKey( 'GET', $route_data[0]['methods'] ); + } + + /** + * Test that the endpoint is not available if the API key is not set. + * + * @covers \Parsely\REST_API\Stats\Endpoint_Posts::is_available_to_current_user + * @uses \Parsely\Endpoints\Base_Endpoint::__construct + * @uses \Parsely\Parsely::api_secret_is_set + * @uses \Parsely\Parsely::get_managed_credentials + * @uses \Parsely\Parsely::get_options + * @uses \Parsely\Parsely::set_default_content_helper_settings_values + * @uses \Parsely\Parsely::set_default_full_metadata_in_non_posts + * @uses \Parsely\Parsely::site_id_is_set + * @uses \Parsely\Permissions::build_pch_permissions_settings_array + * @uses \Parsely\Permissions::get_user_roles_with_edit_posts_cap + * @uses \Parsely\REST_API\Base_API_Controller::__construct + * @uses \Parsely\REST_API\Base_API_Controller::get_full_namespace + * @uses \Parsely\REST_API\Base_API_Controller::get_parsely + * @uses \Parsely\REST_API\Base_API_Controller::prefix_route + * @uses \Parsely\REST_API\Base_Endpoint::__construct + * @uses \Parsely\REST_API\Base_Endpoint::get_full_endpoint + * @uses \Parsely\REST_API\Base_Endpoint::init + * @uses \Parsely\REST_API\Base_Endpoint::register_rest_route + * @uses \Parsely\REST_API\Base_Endpoint::validate_site_id_and_secret + * @uses \Parsely\REST_API\REST_API_Controller::get_namespace + * @uses \Parsely\REST_API\REST_API_Controller::get_version + * @uses \Parsely\REST_API\Stats\Stats_Controller::get_route_prefix + * @uses \Parsely\Utils\Utils::convert_endpoint_to_filter_key + */ + public function test_access_error_if_api_secret_is_not_set(): void { + TestCase::set_options( + array( + 'apikey' => 'test', + ) + ); + $route = $this->get_endpoint()->get_full_endpoint( '/' ); + $response = rest_get_server()->dispatch( + new WP_REST_Request( 'GET', $route ) + ); + + $error = $response->as_error(); + self::assertNotNull( $error ); + self::assertSame( 403, $response->get_status() ); + self::assertSame( 'parsely_api_secret_not_set', $error->get_error_code() ); + self::assertSame( + 'A Parse.ly API Secret must be set in site options to use this endpoint', + $error->get_error_message() + ); + } + + + /** + * Verifies forbidden error when current user doesn't have proper + * capabilities. + * + * @covers \Parsely\REST_API\Stats\Endpoint_Posts::is_available_to_current_user + * @uses \Parsely\Endpoints\Base_Endpoint::__construct + * @uses \Parsely\Parsely::api_secret_is_set + * @uses \Parsely\Parsely::get_managed_credentials + * @uses \Parsely\Parsely::get_options + * @uses \Parsely\Parsely::set_default_content_helper_settings_values + * @uses \Parsely\Parsely::set_default_full_metadata_in_non_posts + * @uses \Parsely\Parsely::site_id_is_set + * @uses \Parsely\Permissions::build_pch_permissions_settings_array + * @uses \Parsely\Permissions::get_user_roles_with_edit_posts_cap + * @uses \Parsely\REST_API\Base_API_Controller::__construct + * @uses \Parsely\REST_API\Base_API_Controller::get_full_namespace + * @uses \Parsely\REST_API\Base_API_Controller::get_parsely + * @uses \Parsely\REST_API\Base_API_Controller::prefix_route + * @uses \Parsely\REST_API\Base_Endpoint::__construct + * @uses \Parsely\REST_API\Base_Endpoint::apply_capability_filters + * @uses \Parsely\REST_API\Base_Endpoint::get_default_access_capability + * @uses \Parsely\REST_API\Base_Endpoint::get_full_endpoint + * @uses \Parsely\REST_API\Base_Endpoint::init + * @uses \Parsely\REST_API\Base_Endpoint::register_rest_route + * @uses \Parsely\REST_API\Base_Endpoint::validate_site_id_and_secret + * @uses \Parsely\REST_API\REST_API_Controller::get_namespace + * @uses \Parsely\REST_API\REST_API_Controller::get_version + * @uses \Parsely\REST_API\Stats\Stats_Controller::get_route_prefix + * @uses \Parsely\Utils\Utils::convert_endpoint_to_filter_key + */ + public function test_access_of_stats_posts_endpoint_is_forbidden(): void { + TestCase::set_options( + array( + 'apikey' => 'test-api-key', + 'api_secret' => 'test-secret', + ) + ); + $this->set_current_user_to_contributor(); + + $route = $this->get_endpoint()->get_full_endpoint( '/' ); + $response = rest_get_server()->dispatch( + new WP_REST_Request( 'GET', $route ) + ); + /** + * Variable. + * + * @var WP_Error $error + */ + $error = $response->as_error(); + + self::assertSame( 403, $response->get_status() ); + self::assertSame( 'rest_forbidden', $error->get_error_code() ); + self::assertSame( + 'Sorry, you are not allowed to do that.', + $error->get_error_message() + ); + } + + /** + * Verifies that calls to the endpoint return the expected data, in the + * expected format. + * + * @covers \Parsely\REST_API\Stats\Endpoint_Posts::get_posts + * @uses \Parsely\Endpoints\Base_Endpoint::__construct + * @uses \Parsely\Parsely::api_secret_is_set + * @uses \Parsely\Parsely::get_api_secret + * @uses \Parsely\Parsely::get_dash_url + * @uses \Parsely\Parsely::get_managed_credentials + * @uses \Parsely\Parsely::get_options + * @uses \Parsely\Parsely::get_site_id + * @uses \Parsely\Parsely::get_url_with_itm_source + * @uses \Parsely\Parsely::set_default_content_helper_settings_values + * @uses \Parsely\Parsely::set_default_full_metadata_in_non_posts + * @uses \Parsely\Parsely::site_id_is_set + * @uses \Parsely\Permissions::build_pch_permissions_settings_array + * @uses \Parsely\Permissions::get_user_roles_with_edit_posts_cap + * @uses \Parsely\REST_API\Base_API_Controller::__construct + * @uses \Parsely\REST_API\Base_API_Controller::get_full_namespace + * @uses \Parsely\REST_API\Base_API_Controller::get_parsely + * @uses \Parsely\REST_API\Base_API_Controller::prefix_route + * @uses \Parsely\REST_API\Base_Endpoint::__construct + * @uses \Parsely\REST_API\Base_Endpoint::apply_capability_filters + * @uses \Parsely\REST_API\Base_Endpoint::get_default_access_capability + * @uses \Parsely\REST_API\Base_Endpoint::init + * @uses \Parsely\REST_API\Base_Endpoint::is_available_to_current_user + * @uses \Parsely\REST_API\Base_Endpoint::register_rest_route + * @uses \Parsely\REST_API\Base_Endpoint::validate_site_id_and_secret + * @uses \Parsely\REST_API\REST_API_Controller::get_namespace + * @uses \Parsely\REST_API\REST_API_Controller::get_version + * @uses \Parsely\REST_API\Stats\Stats_Controller::get_route_prefix + * @uses \Parsely\RemoteAPI\Analytics_Posts_API::get_request_options + * @uses \Parsely\RemoteAPI\Base_Endpoint_Remote::get_api_url + * @uses \Parsely\RemoteAPI\Base_Endpoint_Remote::get_items + * @uses \Parsely\RemoteAPI\Base_Endpoint_Remote::validate_required_constraints + * @uses \Parsely\Utils\Utils::convert_endpoint_to_filter_key + * @uses \Parsely\Utils\Utils::get_date_format + * @uses \Parsely\Utils\Utils::parsely_is_https_supported + */ + public function test_get_posts(): void { + TestCase::set_options( + array( + 'apikey' => 'example.com', + 'api_secret' => 'test-secret', + ) + ); + $this->set_current_user_to_admin(); + + $dispatched = 0; + $date_format = Utils::get_date_format(); + + add_filter( + 'pre_http_request', + function () use ( &$dispatched ): array { + $dispatched++; + + return array( + 'body' => '{"data":[ + { + "author": "Aakash Shah", + "metrics": {"views": 142}, + "pub_date": "2020-04-06T13:30:58", + "thumb_url_medium": "https://images.parsely.com/XCmTXuOf8yVbUYTxj2abQ4RSDkM=/85x85/smart/https%3A//blog.parse.ly/wp-content/uploads/2021/06/Web-Analytics-Tool.png%3Fw%3D150%26h%3D150%26crop%3D1", + "title": "9 Types of Web Analytics Tools \u2014 And How to Know Which Ones You Really Need", + "url": "https://blog.parse.ly/web-analytics-software-tools/?itm_source=parsely-api" + }, + { + "author": "Stephanie Schwartz and Andrew Butler", + "metrics": {"views": 40}, + "pub_date": "2021-04-30T20:30:24", + "thumb_url_medium": "https://images.parsely.com/ap3YSufqxnLpz6zzQshoks3snXI=/85x85/smart/https%3A//blog.parse.ly/wp-content/uploads/2021/05/pexels-brett-jordan-998501-1024x768-2.jpeg%3Fw%3D150%26h%3D150%26crop%3D1", + "title": "5 Tagging Best Practices For Getting the Most Out of Your Content Strategy", + "url": "https://blog.parse.ly/5-tagging-best-practices-content-strategy/?itm_source=parsely-api" + } + ]}', + ); + } + ); + + $rest_request = new WP_REST_Request( 'GET', '/wp-parsely/v2/stats/posts' ); + $rest_request->set_param( 'itm_source', 'wp-parsely-content-helper' ); + + $response = rest_get_server()->dispatch( $rest_request ); + + /** + * The response data. + * + * @var array $response_data + */ + $response_data = $response->get_data(); + + self::assertSame( 1, $dispatched ); + self::assertSame( 200, $response->get_status() ); + self::assertEquals( + array( + array( + 'author' => 'Aakash Shah', + 'date' => wp_date( $date_format, strtotime( '2020-04-06T13:30:58' ) ), + 'id' => 'https://blog.parse.ly/web-analytics-software-tools/', + 'dashUrl' => PARSELY::DASHBOARD_BASE_URL . '/example.com/find?url=https%3A%2F%2Fblog.parse.ly%2Fweb-analytics-software-tools%2F', + 'thumbnailUrl' => 'https://images.parsely.com/XCmTXuOf8yVbUYTxj2abQ4RSDkM=/85x85/smart/https%3A//blog.parse.ly/wp-content/uploads/2021/06/Web-Analytics-Tool.png%3Fw%3D150%26h%3D150%26crop%3D1', + 'title' => '9 Types of Web Analytics Tools — And How to Know Which Ones You Really Need', + 'url' => 'https://blog.parse.ly/web-analytics-software-tools/?itm_source=wp-parsely-content-helper', + 'views' => '142', + 'postId' => 0, + 'rawUrl' => 'https://blog.parse.ly/web-analytics-software-tools/', + ), + array( + 'author' => 'Stephanie Schwartz and Andrew Butler', + 'date' => wp_date( $date_format, strtotime( '2021-04-30T20:30:24' ) ), + 'id' => 'https://blog.parse.ly/5-tagging-best-practices-content-strategy/', + 'dashUrl' => PARSELY::DASHBOARD_BASE_URL . '/example.com/find?url=https%3A%2F%2Fblog.parse.ly%2F5-tagging-best-practices-content-strategy%2F', + 'thumbnailUrl' => 'https://images.parsely.com/ap3YSufqxnLpz6zzQshoks3snXI=/85x85/smart/https%3A//blog.parse.ly/wp-content/uploads/2021/05/pexels-brett-jordan-998501-1024x768-2.jpeg%3Fw%3D150%26h%3D150%26crop%3D1', + 'title' => '5 Tagging Best Practices For Getting the Most Out of Your Content Strategy', + 'url' => 'https://blog.parse.ly/5-tagging-best-practices-content-strategy/?itm_source=wp-parsely-content-helper', + 'views' => '40', + 'postId' => 0, + 'rawUrl' => 'https://blog.parse.ly/5-tagging-best-practices-content-strategy/', + ), + ), + $response_data['data'] + ); + + self::assertEquals( + array( + 'limit' => 5, + 'sort' => 'views', + 'page' => 1, + 'itm_source' => 'wp-parsely-content-helper', + ), + $response_data['params'] + ); + } +} diff --git a/tests/Integration/RestAPI/Stats/EndpointRelatedTest.php b/tests/Integration/RestAPI/Stats/EndpointRelatedTest.php new file mode 100644 index 0000000000..463886c852 --- /dev/null +++ b/tests/Integration/RestAPI/Stats/EndpointRelatedTest.php @@ -0,0 +1,316 @@ +api_controller = new Stats_Controller( $this->parsely ); + $this->endpoint = new Endpoint_Related( $this->api_controller ); + + parent::set_up(); + } + + + /** + * Get the test endpoint instance. + * + * @since 3.17.0 + * + * @return Endpoint_Related + */ + public function get_endpoint(): \Parsely\REST_API\Base_Endpoint { + return $this->endpoint; + } + + /** + * Test the route is registered. + * + * @since 3.17.0 + * + * @covers \Parsely\REST_API\Stats\Endpoint_Related::register_routes + * @uses \Parsely\Endpoints\Base_Endpoint::__construct + * @uses \Parsely\REST_API\Base_API_Controller::__construct + * @uses \Parsely\REST_API\Base_API_Controller::get_full_namespace + * @uses \Parsely\REST_API\Base_API_Controller::get_parsely + * @uses \Parsely\REST_API\Base_API_Controller::prefix_route + * @uses \Parsely\REST_API\Base_Endpoint::__construct + * @uses \Parsely\REST_API\Base_Endpoint::get_full_endpoint + * @uses \Parsely\REST_API\Base_Endpoint::init + * @uses \Parsely\REST_API\Base_Endpoint::register_rest_route + * @uses \Parsely\REST_API\REST_API_Controller::get_namespace + * @uses \Parsely\REST_API\REST_API_Controller::get_version + * @uses \Parsely\REST_API\Stats\Endpoint_Related::__construct + * @uses \Parsely\REST_API\Stats\Endpoint_Related::get_endpoint_name + * @uses \Parsely\REST_API\Stats\Endpoint_Related::is_available_to_current_user + * @uses \Parsely\REST_API\Stats\Post_Data_Trait::get_itm_source_param_args + * @uses \Parsely\REST_API\Stats\Related_Posts_Trait::get_related_posts_param_args + * @uses \Parsely\REST_API\Stats\Stats_Controller::get_route_prefix + * @uses \Parsely\Utils\Utils::convert_endpoint_to_filter_key + */ + public function test_route_is_registered(): void { + $routes = rest_get_server()->get_routes(); + + // Check that the excerpt-generator/generate route is registered. + $expected_route = $this->get_endpoint()->get_full_endpoint( '/' ); + self::assertArrayHasKey( $expected_route, $routes ); + + // Check that the route is associated with the POST method. + $route_data = $routes[ $expected_route ]; + self::assertArrayHasKey( 'GET', $route_data[0]['methods'] ); + } + + /** + * Test that the endpoint is available to everyone, even if they are not logged in. + * + * @since 3.17.0 + * + * @covers \Parsely\REST_API\Stats\Endpoint_Related::is_available_to_current_user + * @uses \Parsely\Endpoints\Base_Endpoint::__construct + * @uses \Parsely\Parsely::api_secret_is_set + * @uses \Parsely\Parsely::get_api_secret + * @uses \Parsely\Parsely::get_managed_credentials + * @uses \Parsely\Parsely::get_options + * @uses \Parsely\Parsely::get_site_id + * @uses \Parsely\Parsely::get_url_with_itm_source + * @uses \Parsely\Parsely::set_default_content_helper_settings_values + * @uses \Parsely\Parsely::set_default_full_metadata_in_non_posts + * @uses \Parsely\Parsely::site_id_is_set + * @uses \Parsely\Permissions::build_pch_permissions_settings_array + * @uses \Parsely\Permissions::get_user_roles_with_edit_posts_cap + * @uses \Parsely\REST_API\Base_API_Controller::__construct + * @uses \Parsely\REST_API\Base_API_Controller::get_full_namespace + * @uses \Parsely\REST_API\Base_API_Controller::get_parsely + * @uses \Parsely\REST_API\Base_API_Controller::prefix_route + * @uses \Parsely\REST_API\Base_Endpoint::__construct + * @uses \Parsely\REST_API\Base_Endpoint::get_full_endpoint + * @uses \Parsely\REST_API\Base_Endpoint::init + * @uses \Parsely\REST_API\Base_Endpoint::register_rest_route + * @uses \Parsely\REST_API\REST_API_Controller::get_namespace + * @uses \Parsely\REST_API\REST_API_Controller::get_version + * @uses \Parsely\REST_API\Stats\Endpoint_Related::__construct + * @uses \Parsely\REST_API\Stats\Endpoint_Related::get_endpoint_name + * @uses \Parsely\REST_API\Stats\Endpoint_Related::get_related_posts + * @uses \Parsely\REST_API\Stats\Endpoint_Related::register_routes + * @uses \Parsely\REST_API\Stats\Post_Data_Trait::get_itm_source_param_args + * @uses \Parsely\REST_API\Stats\Post_Data_Trait::set_itm_source_from_request + * @uses \Parsely\REST_API\Stats\Related_Posts_Trait::get_related_posts_of_url + * @uses \Parsely\REST_API\Stats\Related_Posts_Trait::get_related_posts_param_args + * @uses \Parsely\REST_API\Stats\Stats_Controller::get_route_prefix + * @uses \Parsely\RemoteAPI\Base_Endpoint_Remote::get_api_url + * @uses \Parsely\RemoteAPI\Base_Endpoint_Remote::get_items + * @uses \Parsely\RemoteAPI\Base_Endpoint_Remote::get_request_options + * @uses \Parsely\RemoteAPI\Base_Endpoint_Remote::validate_required_constraints + * @uses \Parsely\Utils\Utils::convert_endpoint_to_filter_key + */ + public function test_access_of_related_posts_is_available_to_everyone(): void { + TestCase::set_options( + array( + 'apikey' => 'test-api-key', + 'api_secret' => 'test-secret', + ) + ); + wp_set_current_user( 0 ); + + $dispatched = 0; + $this->mock_api_response( $dispatched ); + + $route = $this->get_endpoint()->get_full_endpoint( '/' ); + $request = new WP_REST_Request( 'GET', $route ); + $request->set_param( 'url', 'https://example.com/a-post' ); + $response = rest_get_server()->dispatch( $request ); + + self::assertEquals( 1, $dispatched ); + self::assertSame( 200, $response->get_status() ); + } + + /** + * Mock the API response of the Parse.ly API. + * + * @since 3.17.0 + * + * @param int &$dispatched The number of times the API was dispatched. + */ + private function mock_api_response( int &$dispatched ): void { + add_filter( + 'pre_http_request', + function () use ( &$dispatched ): array { + $dispatched++; + return array( + 'body' => '{"data":[ + { + "image_url":"https:\/\/example.com\/img.png", + "thumb_url_medium":"https:\/\/example.com\/thumb.png", + "title":"something", + "url":"https:\/\/example.com" + }, + { + "image_url":"https:\/\/example.com\/img2.png", + "thumb_url_medium":"https:\/\/example.com\/thumb2.png", + "title":"something2", + "url":"https:\/\/example.com\/2" + } + ]}', + ); + } + ); + } + + /** + * Verifies that calls to the `stats/related` return the expected data, in the + * expected format, despite the user being unauthenticated. + * + * @since 3.17.0 + * + * @covers \Parsely\REST_API\Stats\Endpoint_Related::get_related_posts + * @uses \Parsely\Endpoints\Base_Endpoint::__construct + * @uses \Parsely\Parsely::api_secret_is_set + * @uses \Parsely\Parsely::get_api_secret + * @uses \Parsely\Parsely::get_managed_credentials + * @uses \Parsely\Parsely::get_options + * @uses \Parsely\Parsely::get_site_id + * @uses \Parsely\Parsely::get_url_with_itm_source + * @uses \Parsely\Parsely::set_default_content_helper_settings_values + * @uses \Parsely\Parsely::set_default_full_metadata_in_non_posts + * @uses \Parsely\Parsely::site_id_is_set + * @uses \Parsely\Permissions::build_pch_permissions_settings_array + * @uses \Parsely\Permissions::get_user_roles_with_edit_posts_cap + * @uses \Parsely\REST_API\Base_API_Controller::__construct + * @uses \Parsely\REST_API\Base_API_Controller::get_full_namespace + * @uses \Parsely\REST_API\Base_API_Controller::get_parsely + * @uses \Parsely\REST_API\Base_API_Controller::prefix_route + * @uses \Parsely\REST_API\Base_Endpoint::__construct + * @uses \Parsely\REST_API\Base_Endpoint::get_full_endpoint + * @uses \Parsely\REST_API\Base_Endpoint::init + * @uses \Parsely\REST_API\Base_Endpoint::register_rest_route + * @uses \Parsely\REST_API\REST_API_Controller::get_namespace + * @uses \Parsely\REST_API\REST_API_Controller::get_version + * @uses \Parsely\REST_API\Stats\Endpoint_Related::__construct + * @uses \Parsely\REST_API\Stats\Endpoint_Related::get_endpoint_name + * @uses \Parsely\REST_API\Stats\Endpoint_Related::is_available_to_current_user + * @uses \Parsely\REST_API\Stats\Endpoint_Related::register_routes + * @uses \Parsely\REST_API\Stats\Post_Data_Trait::get_itm_source_param_args + * @uses \Parsely\REST_API\Stats\Post_Data_Trait::set_itm_source_from_request + * @uses \Parsely\REST_API\Stats\Related_Posts_Trait::get_related_posts_of_url + * @uses \Parsely\REST_API\Stats\Related_Posts_Trait::get_related_posts_param_args + * @uses \Parsely\REST_API\Stats\Stats_Controller::get_route_prefix + * @uses \Parsely\RemoteAPI\Base_Endpoint_Remote::get_api_url + * @uses \Parsely\RemoteAPI\Base_Endpoint_Remote::get_items + * @uses \Parsely\RemoteAPI\Base_Endpoint_Remote::get_request_options + * @uses \Parsely\RemoteAPI\Base_Endpoint_Remote::validate_required_constraints + * @uses \Parsely\Utils\Utils::convert_endpoint_to_filter_key + */ + public function test_get_related_posts(): void { + $route = $this->get_endpoint()->get_full_endpoint( '/' ); + + TestCase::set_options( + array( + 'apikey' => 'example.com', + 'api_secret' => 'test-secret', + ) + ); + + $dispatched = 0; + + add_filter( + 'pre_http_request', + function () use ( &$dispatched ): array { + $dispatched++; + return array( + 'body' => '{"data":[ + { + "image_url":"https:\/\/example.com\/img.png", + "thumb_url_medium":"https:\/\/example.com\/thumb.png", + "title":"something", + "url":"https:\/\/example.com" + }, + { + "image_url":"https:\/\/example.com\/img2.png", + "thumb_url_medium":"https:\/\/example.com\/thumb2.png", + "title":"something2", + "url":"https:\/\/example.com\/2" + } + ]}', + ); + } + ); + + $request = new WP_REST_Request( 'GET', $route ); + $request->set_param( 'url', 'https://example.com/a-post' ); + $response = rest_get_server()->dispatch( $request ); + + /** + * The response data. + * + * @var array $response_data + */ + $response_data = $response->get_data(); + + self::assertSame( 1, $dispatched ); + self::assertSame( 200, $response->get_status() ); + self::assertEquals( + array( + (object) array( + 'image_url' => 'https://example.com/img.png', + 'thumb_url_medium' => 'https://example.com/thumb.png', + 'title' => 'something', + 'url' => 'https://example.com', + ), + (object) array( + 'image_url' => 'https://example.com/img2.png', + 'thumb_url_medium' => 'https://example.com/thumb2.png', + 'title' => 'something2', + 'url' => 'https://example.com/2', + ), + ), + $response_data['data'] + ); + } + + /** + * Test that the endpoint is not available if the API secret is not set. + * This test should be disabled since the endpoint does not requires the API secret. + * + * @since 3.17.0 + * @coversNothing + */ + public function test_is_available_to_current_user_returns_error_api_secret_not_set(): void { + // This test is disabled since the endpoint does not requires the API secret. + self::assertTrue( true ); + } +} diff --git a/tests/Integration/RestAPI/Stats/StatsControllerTest.php b/tests/Integration/RestAPI/Stats/StatsControllerTest.php new file mode 100644 index 0000000000..0635125f95 --- /dev/null +++ b/tests/Integration/RestAPI/Stats/StatsControllerTest.php @@ -0,0 +1,60 @@ +stats_controller = new Stats_Controller( $parsely ); + } + + /** + * Test the constructor sets up the correct namespace and version. + * + * @since 3.17.0 + * + * @covers \Parsely\REST_API\Stats\Stats_Controller::__construct + * @uses \Parsely\REST_API\Stats\Stats_Controller::get_full_namespace + * @uses \Parsely\REST_API\REST_API_Controller::get_namespace + * @uses \Parsely\REST_API\REST_API_Controller::get_version + */ + public function test_constructor_sets_up_namespace_and_version(): void { + self::assertEquals( 'wp-parsely/v2', $this->stats_controller->get_full_namespace() ); + } +} From 508b1997a27056892d78e567a02e1fee274ff487 Mon Sep 17 00:00:00 2001 From: Henrique Mouta Date: Thu, 29 Aug 2024 14:00:03 +0100 Subject: [PATCH 15/49] Apply code review suggestions by @acicovic --- src/rest-api/class-base-endpoint.php | 2 +- .../RestAPI/BaseAPIControllerTest.php | 23 +-- .../Integration/RestAPI/BaseEndpointTest.php | 26 +-- .../ContentHelperControllerTest.php | 10 +- .../ContentHelperFeatureTestTrait.php | 190 +++++++++--------- .../EndpointExcerptGeneratorTest.php | 12 +- .../EndpointSmartLinkingTest.php | 15 +- .../EndpointTitleSuggestionsTest.php | 12 +- .../RestAPI/RestAPIControllerTest.php | 8 +- 9 files changed, 147 insertions(+), 151 deletions(-) diff --git a/src/rest-api/class-base-endpoint.php b/src/rest-api/class-base-endpoint.php index 3229b4bb80..653a13f278 100644 --- a/src/rest-api/class-base-endpoint.php +++ b/src/rest-api/class-base-endpoint.php @@ -133,11 +133,11 @@ abstract public function register_routes(): void; * @param string[] $methods Array with the allowed methods. * @param callable $callback Callback function to call when the endpoint is hit. * @param array $args The endpoint arguments definition. - * @return void */ public function register_rest_route( string $route, array $methods, callable $callback, array $args = array() ): void { // Trim any possible slashes from the route. $route = trim( $route, '/' ); + // Store the route for later reference. $this->registered_routes[] = $route; diff --git a/tests/Integration/RestAPI/BaseAPIControllerTest.php b/tests/Integration/RestAPI/BaseAPIControllerTest.php index 3b5b037317..eef6e6d548 100644 --- a/tests/Integration/RestAPI/BaseAPIControllerTest.php +++ b/tests/Integration/RestAPI/BaseAPIControllerTest.php @@ -38,14 +38,13 @@ class BaseAPIControllerTest extends TestCase { * * @since 3.17.0 */ - public function setUp(): void { - parent::setUp(); + public function set_up(): void { + parent::set_up(); TestCase::set_options(); $parsely = self::createMock( Parsely::class ); $this->test_controller = new class($parsely) extends Base_API_Controller { - /** - * Get the namespace for the API. + * Gets the namespace for the API. * * @since 3.17.0 * @@ -56,7 +55,7 @@ protected function get_namespace(): string { } /** - * Get the version for the API. + * Gets the version for the API. * * @since 3.17.0 * @@ -67,14 +66,14 @@ protected function get_version(): string { } /** - * Initialize the test controller. + * Initializes the test controller. * * @since 3.17.0 */ protected function init(): void {} /** - * Expose the protected method for testing. + * Exposes the protected method for testing. * * @param Base_Endpoint[] $endpoints The endpoints to register. */ @@ -83,7 +82,7 @@ public function testable_register_endpoints( array $endpoints ): void { } /** - * Expose the protected method for testing. + * Exposes the protected method for testing. * * @param Base_Endpoint $endpoint The endpoint to register. */ @@ -94,7 +93,7 @@ public function testable_register_endpoint( Base_Endpoint $endpoint ): void { } /** - * Test the get_namespace method. + * Tests the get_namespace method. * * @since 3.17.0 * @@ -106,7 +105,7 @@ public function test_get_namespace(): void { } /** - * Test the prefix_route method. + * Tests the prefix_route method. * * @since 3.17.0 * @@ -152,7 +151,7 @@ public function get_route_prefix(): string { } /** - * Test that endpoints are registered correctly. + * Tests that endpoints are registered correctly. * * @since 3.17.0 * @@ -172,7 +171,7 @@ public function test_register_endpoint(): void { } /** - * Test that multiple endpoints are registered correctly using a helper method. + * Tests that multiple endpoints are registered correctly using a helper method. * * @since 3.17.0 * diff --git a/tests/Integration/RestAPI/BaseEndpointTest.php b/tests/Integration/RestAPI/BaseEndpointTest.php index 4b0b015053..f437531842 100644 --- a/tests/Integration/RestAPI/BaseEndpointTest.php +++ b/tests/Integration/RestAPI/BaseEndpointTest.php @@ -94,7 +94,7 @@ public function __construct() { } /** - * Set up the test environment. + * Sets up the test environment. * * @since 3.17.0 */ @@ -109,7 +109,7 @@ public function set_up(): void { $this->test_endpoint = new class($this->api_controller) extends Base_Endpoint { /** - * Get the endpoint name. + * Gets the endpoint name. * * @since 3.17.0 * @@ -120,7 +120,7 @@ public function get_endpoint_name(): string { } /** - * Register the test route. + * Registers the test route. * * @since 3.17.0 */ @@ -133,7 +133,7 @@ public function register_routes(): void { } /** - * Get test data. + * Gets test data. * * @since 3.17.0 * @@ -148,7 +148,7 @@ public function get_test_data(): array { } /** - * Tear down the test environment. + * Tears down the test environment. * * @since 3.17.0 */ @@ -161,7 +161,7 @@ public function tear_down(): void { } /** - * Return the test endpoint instance. + * Returns the test endpoint instance. * * @since 3.17.0 * @@ -172,7 +172,7 @@ public function get_endpoint(): Base_Endpoint { } /** - * Test that the route is correctly registered in WordPress. + * Tests that the route is correctly registered in WordPress. * * @since 3.17.0 * @@ -213,7 +213,7 @@ public function test_route_is_registered(): void { } /** - * Test that the route is correctly registered in WordPress, depending on the filter. + * Tests that the route is correctly registered in WordPress, depending on the filter. * * @since 3.17.0 * @@ -278,7 +278,7 @@ public function test_endpoint_is_registered_based_on_filter(): void { } /** - * Test is_available_to_current_user returns WP_Error if API key or secret is not set. + * Tests is_available_to_current_user returns WP_Error if API key or secret is not set. * * @since 3.17.0 * @@ -317,7 +317,7 @@ public function test_is_available_to_current_user_returns_error_site_id_not_set( } /** - * Test is_available_to_current_user returns WP_Error if API key or secret is not set. + * Tests is_available_to_current_user returns WP_Error if API key or secret is not set. * * @since 3.17.0 * @@ -357,7 +357,7 @@ public function test_is_available_to_current_user_returns_error_api_secret_not_s } /** - * Test apply_capability_filters method. + * Tests apply_capability_filters method. * * @covers \Parsely\REST_API\Base_Endpoint::apply_capability_filters * @uses \Parsely\REST_API\Base_API_Controller::__construct @@ -383,7 +383,7 @@ function () { } /** - * Test validate_apikey_and_secret returns true when API key and secret are set. + * Tests validate_apikey_and_secret returns true when API key and secret are set. * * @covers \Parsely\REST_API\Base_Endpoint::validate_site_id_and_secret * @uses \Parsely\REST_API\Base_API_Controller::__construct @@ -439,7 +439,7 @@ protected function set_protected_property( $obj, string $property_name, $value ) } /** - * Initialize the REST endpoint. + * Initializes the REST endpoint. * * @since 3.17.0 */ diff --git a/tests/Integration/RestAPI/ContentHelper/ContentHelperControllerTest.php b/tests/Integration/RestAPI/ContentHelper/ContentHelperControllerTest.php index fcc563d076..5b22320e87 100644 --- a/tests/Integration/RestAPI/ContentHelper/ContentHelperControllerTest.php +++ b/tests/Integration/RestAPI/ContentHelper/ContentHelperControllerTest.php @@ -41,15 +41,15 @@ class ContentHelperControllerTest extends RestAPIControllerTest { * * @since 3.17.0 */ - public function setUp(): void { - parent::setUp(); + public function set_up(): void { + parent::set_up(); TestCase::set_options(); $parsely = self::createMock( Parsely::class ); $this->content_helper_controller = new Content_Helper_Controller( $parsely ); } /** - * Test the constructor sets up the correct namespace and version. + * Tests the constructor sets up the correct namespace and version. * * @since 3.17.0 * @@ -61,7 +61,7 @@ public function test_constructor_sets_up_namespace_and_version(): void { } /** - * Test that the route prefix is set correctly. + * Tests that the route prefix is set correctly. * * @since 3.17.0 * @@ -72,7 +72,7 @@ public function test_route_prefix(): void { } /** - * Test that the init method registers the correct endpoints. + * Tests that the init method registers the correct endpoints. * * @since 3.17.0 * diff --git a/tests/Integration/RestAPI/ContentHelper/ContentHelperFeatureTestTrait.php b/tests/Integration/RestAPI/ContentHelper/ContentHelperFeatureTestTrait.php index 47615307f8..32e859dbb7 100644 --- a/tests/Integration/RestAPI/ContentHelper/ContentHelperFeatureTestTrait.php +++ b/tests/Integration/RestAPI/ContentHelper/ContentHelperFeatureTestTrait.php @@ -22,35 +22,33 @@ * @covers \Parsely\REST_API\Content_Helper\Content_Helper_Feature */ trait ContentHelperFeatureTestTrait { - - /** - * Test that the endpoint is available to the current user. + * Tests that the endpoint is available to the current user. * * @since 3.17.0 * * @covers \Parsely\REST_API\Content_Helper\Content_Helper_Feature::is_available_to_current_user - * @uses Parsely\Endpoints\Base_Endpoint::__construct - * @uses Parsely\Parsely::__construct - * @uses Parsely\Parsely::allow_parsely_remote_requests - * @uses Parsely\Parsely::api_secret_is_set - * @uses Parsely\Parsely::are_credentials_managed - * @uses Parsely\Parsely::get_managed_credentials - * @uses Parsely\Parsely::get_options - * @uses Parsely\Parsely::set_default_content_helper_settings_values - * @uses Parsely\Parsely::set_default_full_metadata_in_non_posts - * @uses Parsely\Parsely::set_managed_options - * @uses Parsely\Parsely::site_id_is_set - * @uses Parsely\Permissions::build_pch_permissions_settings_array - * @uses Parsely\Permissions::current_user_can_use_pch_feature - * @uses Parsely\Permissions::get_user_roles_with_edit_posts_cap - * @uses Parsely\REST_API\Base_API_Controller::__construct - * @uses Parsely\REST_API\Base_Endpoint::__construct - * @uses Parsely\REST_API\Base_Endpoint::init - * @uses Parsely\REST_API\Base_Endpoint::apply_capability_filters - * @uses Parsely\REST_API\Base_Endpoint::is_available_to_current_user - * @uses Parsely\REST_API\Base_Endpoint::validate_apikey_and_secret - * @uses Parsely\Utils\Utils::convert_endpoint_to_filter_key + * @uses \Parsely\Endpoints\Base_Endpoint::__construct + * @uses \Parsely\Parsely::__construct + * @uses \Parsely\Parsely::allow_parsely_remote_requests + * @uses \Parsely\Parsely::api_secret_is_set + * @uses \Parsely\Parsely::are_credentials_managed + * @uses \Parsely\Parsely::get_managed_credentials + * @uses \Parsely\Parsely::get_options + * @uses \Parsely\Parsely::set_default_content_helper_settings_values + * @uses \Parsely\Parsely::set_default_full_metadata_in_non_posts + * @uses \Parsely\Parsely::set_managed_options + * @uses \Parsely\Parsely::site_id_is_set + * @uses \Parsely\Permissions::build_pch_permissions_settings_array + * @uses \Parsely\Permissions::current_user_can_use_pch_feature + * @uses \Parsely\Permissions::get_user_roles_with_edit_posts_cap + * @uses \Parsely\REST_API\Base_API_Controller::__construct + * @uses \Parsely\REST_API\Base_Endpoint::__construct + * @uses \Parsely\REST_API\Base_Endpoint::init + * @uses \Parsely\REST_API\Base_Endpoint::apply_capability_filters + * @uses \Parsely\REST_API\Base_Endpoint::is_available_to_current_user + * @uses \Parsely\REST_API\Base_Endpoint::validate_apikey_and_secret + * @uses \Parsely\Utils\Utils::convert_endpoint_to_filter_key */ public function test_is_available_to_current_user_returns_true_if_feature_enabled(): void { $this->enable_feature(); @@ -61,32 +59,32 @@ public function test_is_available_to_current_user_returns_true_if_feature_enable } /** - * Test that the endpoint is not available to the current user. + * Tests that the endpoint is not available to the current user. * * @since 3.17.0 * * @covers \Parsely\REST_API\Content_Helper\Content_Helper_Feature::is_available_to_current_user - * @uses Parsely\Endpoints\Base_Endpoint::__construct - * @uses Parsely\Parsely::__construct - * @uses Parsely\Parsely::allow_parsely_remote_requests - * @uses Parsely\Parsely::api_secret_is_set - * @uses Parsely\Parsely::are_credentials_managed - * @uses Parsely\Parsely::get_managed_credentials - * @uses Parsely\Parsely::get_options - * @uses Parsely\Parsely::set_default_content_helper_settings_values - * @uses Parsely\Parsely::set_default_full_metadata_in_non_posts - * @uses Parsely\Parsely::set_managed_options - * @uses Parsely\Parsely::site_id_is_set - * @uses Parsely\Permissions::build_pch_permissions_settings_array - * @uses Parsely\Permissions::current_user_can_use_pch_feature - * @uses Parsely\Permissions::get_user_roles_with_edit_posts_cap - * @uses Parsely\REST_API\Base_API_Controller::__construct - * @uses Parsely\REST_API\Base_Endpoint::__construct - * @uses Parsely\REST_API\Base_Endpoint::init - * @uses Parsely\REST_API\Base_Endpoint::apply_capability_filters - * @uses Parsely\REST_API\Base_Endpoint::is_available_to_current_user - * @uses Parsely\REST_API\Base_Endpoint::validate_apikey_and_secret - * @uses Parsely\Utils\Utils::convert_endpoint_to_filter_key + * @uses \Parsely\Endpoints\Base_Endpoint::__construct + * @uses \Parsely\Parsely::__construct + * @uses \Parsely\Parsely::allow_parsely_remote_requests + * @uses \Parsely\Parsely::api_secret_is_set + * @uses \Parsely\Parsely::are_credentials_managed + * @uses \Parsely\Parsely::get_managed_credentials + * @uses \Parsely\Parsely::get_options + * @uses \Parsely\Parsely::set_default_content_helper_settings_values + * @uses \Parsely\Parsely::set_default_full_metadata_in_non_posts + * @uses \Parsely\Parsely::set_managed_options + * @uses \Parsely\Parsely::site_id_is_set + * @uses \Parsely\Permissions::build_pch_permissions_settings_array + * @uses \Parsely\Permissions::current_user_can_use_pch_feature + * @uses \Parsely\Permissions::get_user_roles_with_edit_posts_cap + * @uses \Parsely\REST_API\Base_API_Controller::__construct + * @uses \Parsely\REST_API\Base_Endpoint::__construct + * @uses \Parsely\REST_API\Base_Endpoint::init + * @uses \Parsely\REST_API\Base_Endpoint::apply_capability_filters + * @uses \Parsely\REST_API\Base_Endpoint::is_available_to_current_user + * @uses \Parsely\REST_API\Base_Endpoint::validate_apikey_and_secret + * @uses \Parsely\Utils\Utils::convert_endpoint_to_filter_key */ public function test_is_available_to_current_user_returns_error_if_feature_disabled(): void { $this->disable_feature(); @@ -97,32 +95,32 @@ public function test_is_available_to_current_user_returns_error_if_feature_disab } /** - * Test that the endpoint is available to the current user, since the user has the required role. + * Tests that the endpoint is available to the current user, since the user has the required role. * * @since 3.17.0 * * @covers \Parsely\REST_API\Content_Helper\Content_Helper_Feature::is_available_to_current_user - * @uses Parsely\Endpoints\Base_Endpoint::__construct - * @uses Parsely\Parsely::__construct - * @uses Parsely\Parsely::allow_parsely_remote_requests - * @uses Parsely\Parsely::api_secret_is_set - * @uses Parsely\Parsely::are_credentials_managed - * @uses Parsely\Parsely::get_managed_credentials - * @uses Parsely\Parsely::get_options - * @uses Parsely\Parsely::set_default_content_helper_settings_values - * @uses Parsely\Parsely::set_default_full_metadata_in_non_posts - * @uses Parsely\Parsely::set_managed_options - * @uses Parsely\Parsely::site_id_is_set - * @uses Parsely\Permissions::build_pch_permissions_settings_array - * @uses Parsely\Permissions::current_user_can_use_pch_feature - * @uses Parsely\Permissions::get_user_roles_with_edit_posts_cap - * @uses Parsely\REST_API\Base_API_Controller::__construct - * @uses Parsely\REST_API\Base_Endpoint::__construct - * @uses Parsely\REST_API\Base_Endpoint::init - * @uses Parsely\REST_API\Base_Endpoint::apply_capability_filters - * @uses Parsely\REST_API\Base_Endpoint::is_available_to_current_user - * @uses Parsely\REST_API\Base_Endpoint::validate_apikey_and_secret - * @uses Parsely\Utils\Utils::convert_endpoint_to_filter_key + * @uses \Parsely\Endpoints\Base_Endpoint::__construct + * @uses \Parsely\Parsely::__construct + * @uses \Parsely\Parsely::allow_parsely_remote_requests + * @uses \Parsely\Parsely::api_secret_is_set + * @uses \Parsely\Parsely::are_credentials_managed + * @uses \Parsely\Parsely::get_managed_credentials + * @uses \Parsely\Parsely::get_options + * @uses \Parsely\Parsely::set_default_content_helper_settings_values + * @uses \Parsely\Parsely::set_default_full_metadata_in_non_posts + * @uses \Parsely\Parsely::set_managed_options + * @uses \Parsely\Parsely::site_id_is_set + * @uses \Parsely\Permissions::build_pch_permissions_settings_array + * @uses \Parsely\Permissions::current_user_can_use_pch_feature + * @uses \Parsely\Permissions::get_user_roles_with_edit_posts_cap + * @uses \Parsely\REST_API\Base_API_Controller::__construct + * @uses \Parsely\REST_API\Base_Endpoint::__construct + * @uses \Parsely\REST_API\Base_Endpoint::init + * @uses \Parsely\REST_API\Base_Endpoint::apply_capability_filters + * @uses \Parsely\REST_API\Base_Endpoint::is_available_to_current_user + * @uses \Parsely\REST_API\Base_Endpoint::validate_apikey_and_secret + * @uses \Parsely\Utils\Utils::convert_endpoint_to_filter_key */ public function test_is_available_to_current_user_returns_true_if_has_permissions(): void { $this->set_feature_options( @@ -142,33 +140,33 @@ public function test_is_available_to_current_user_returns_true_if_has_permission } /** - * Test that the endpoint is not available to the current user, since the user does not have the + * Tests that the endpoint is not available to the current user, since the user does not have the * required role. * * @since 3.17.0 * * @covers \Parsely\REST_API\Content_Helper\Content_Helper_Feature::is_available_to_current_user - * @uses Parsely\Endpoints\Base_Endpoint::__construct - * @uses Parsely\Parsely::__construct - * @uses Parsely\Parsely::allow_parsely_remote_requests - * @uses Parsely\Parsely::api_secret_is_set - * @uses Parsely\Parsely::are_credentials_managed - * @uses Parsely\Parsely::get_managed_credentials - * @uses Parsely\Parsely::get_options - * @uses Parsely\Parsely::set_default_content_helper_settings_values - * @uses Parsely\Parsely::set_default_full_metadata_in_non_posts - * @uses Parsely\Parsely::set_managed_options - * @uses Parsely\Parsely::site_id_is_set - * @uses Parsely\Permissions::build_pch_permissions_settings_array - * @uses Parsely\Permissions::current_user_can_use_pch_feature - * @uses Parsely\Permissions::get_user_roles_with_edit_posts_cap - * @uses Parsely\REST_API\Base_API_Controller::__construct - * @uses Parsely\REST_API\Base_Endpoint::__construct - * @uses Parsely\REST_API\Base_Endpoint::init - * @uses Parsely\REST_API\Base_Endpoint::apply_capability_filters - * @uses Parsely\REST_API\Base_Endpoint::is_available_to_current_user - * @uses Parsely\REST_API\Base_Endpoint::validate_apikey_and_secret - * @uses Parsely\Utils\Utils::convert_endpoint_to_filter_key + * @uses \Parsely\Endpoints\Base_Endpoint::__construct + * @uses \Parsely\Parsely::__construct + * @uses \Parsely\Parsely::allow_parsely_remote_requests + * @uses \Parsely\Parsely::api_secret_is_set + * @uses \Parsely\Parsely::are_credentials_managed + * @uses \Parsely\Parsely::get_managed_credentials + * @uses \Parsely\Parsely::get_options + * @uses \Parsely\Parsely::set_default_content_helper_settings_values + * @uses \Parsely\Parsely::set_default_full_metadata_in_non_posts + * @uses \Parsely\Parsely::set_managed_options + * @uses \Parsely\Parsely::site_id_is_set + * @uses \Parsely\Permissions::build_pch_permissions_settings_array + * @uses \Parsely\Permissions::current_user_can_use_pch_feature + * @uses \Parsely\Permissions::get_user_roles_with_edit_posts_cap + * @uses \Parsely\REST_API\Base_API_Controller::__construct + * @uses \Parsely\REST_API\Base_Endpoint::__construct + * @uses \Parsely\REST_API\Base_Endpoint::init + * @uses \Parsely\REST_API\Base_Endpoint::apply_capability_filters + * @uses \Parsely\REST_API\Base_Endpoint::is_available_to_current_user + * @uses \Parsely\REST_API\Base_Endpoint::validate_apikey_and_secret + * @uses \Parsely\Utils\Utils::convert_endpoint_to_filter_key */ public function test_is_available_to_current_user_returns_error_if_no_permissions(): void { $this->set_current_user_to_contributor(); @@ -186,7 +184,7 @@ public function test_is_available_to_current_user_returns_error_if_no_permission /** - * Test that the endpoint is not available to the current user, since the user is not logged in. + * Tests that the endpoint is not available to the current user, since the user is not logged in. * * @since 3.17.0 * @@ -226,7 +224,7 @@ public function test_is_available_to_current_user_returns_error_if_no_user(): vo abstract protected function get_endpoint(): Base_Endpoint; /** - * Set the specific feature options. + * Sets the specific feature options. * * @since 3.17.0 * @@ -248,7 +246,7 @@ private function set_feature_options( array $options ): void { } /** - * Disable the specific feature. + * Disables the specific feature. * * @since 3.17.0 */ @@ -262,7 +260,7 @@ private function disable_feature(): void { } /** - * Enable the specific feature. + * Enables the specific feature. * * @since 3.17.0 */ @@ -278,14 +276,14 @@ private function enable_feature(): void { } /** - * Set the current user to an administrator. + * Sets the current user to an administrator. * * @since 3.17.0 */ abstract protected function set_current_user_to_admin(): void; /** - * Set the current user to a contributor. + * Sets the current user to a contributor. * * @since 3.17.0 */ diff --git a/tests/Integration/RestAPI/ContentHelper/EndpointExcerptGeneratorTest.php b/tests/Integration/RestAPI/ContentHelper/EndpointExcerptGeneratorTest.php index c959a8e2d7..a7eb36d773 100644 --- a/tests/Integration/RestAPI/ContentHelper/EndpointExcerptGeneratorTest.php +++ b/tests/Integration/RestAPI/ContentHelper/EndpointExcerptGeneratorTest.php @@ -3,7 +3,7 @@ * Integration tests for the Endpoint_Excerpt_Generator class. * * @package Parsely - * @since 3.17.0 + * @since 3.17.0 */ declare(strict_types=1); @@ -38,7 +38,7 @@ class EndpointExcerptGeneratorTest extends BaseEndpointTest { private $endpoint; /** - * Set up the test environment. + * Sets up the test environment. * * @since 3.17.0 */ @@ -51,7 +51,7 @@ public function set_up(): void { } /** - * Get the test endpoint instance. + * Gets the test endpoint instance. * * @since 3.17.0 * @@ -62,7 +62,7 @@ public function get_endpoint(): \Parsely\REST_API\Base_Endpoint { } /** - * Test that the endpoint is correctly registered. + * Tests that the endpoint is correctly registered. * * @since 3.17.0 * @@ -108,7 +108,7 @@ public function test_route_is_registered(): void { } /** - * Test that the generate_excerpt method returns a valid response. + * Tests that the generate_excerpt method returns a valid response. * * @since 3.17.0 * @@ -154,7 +154,7 @@ public function test_generate_excerpt_returns_valid_response(): void { } /** - * Test that the generate_excerpt method returns an error if Suggest_Brief_API fails. + * Tests that the generate_excerpt method returns an error if Suggest_Brief_API fails. * * @since 3.17.0 * diff --git a/tests/Integration/RestAPI/ContentHelper/EndpointSmartLinkingTest.php b/tests/Integration/RestAPI/ContentHelper/EndpointSmartLinkingTest.php index 8389bd7c0c..99b9c5c103 100644 --- a/tests/Integration/RestAPI/ContentHelper/EndpointSmartLinkingTest.php +++ b/tests/Integration/RestAPI/ContentHelper/EndpointSmartLinkingTest.php @@ -3,7 +3,7 @@ * Integration tests for the Endpoint_Smart_Linking class. * * @package Parsely - * @since 3.17.0 + * @since 3.17.0 */ declare(strict_types=1); @@ -41,7 +41,7 @@ class EndpointSmartLinkingTest extends BaseEndpointTest { private $endpoint; /** - * Set up the test environment. + * Sets up the test environment. * * @since 3.17.0 */ @@ -62,7 +62,7 @@ public function set_up(): void { } /** - * Get the test endpoint instance. + * Gets the test endpoint instance. * * @since 3.17.0 * @@ -73,7 +73,7 @@ public function get_endpoint(): \Parsely\REST_API\Base_Endpoint { } /** - * Test that the endpoint is correctly registered. + * Tests that the endpoint is correctly registered. * * @since 3.17.0 * @@ -119,7 +119,7 @@ public function test_route_is_registered(): void { } /** - * Test that the generate_smart_links method returns a valid response. + * Tests that the generate_smart_links method returns a valid response. * * @since 3.17.0 * @@ -169,7 +169,7 @@ public function test_generate_smart_links_returns_valid_response(): void { } /** - * Test that the generate_smart_links method returns an error if Suggest_Linked_Reference_API fails. + * Tests that the generate_smart_links method returns an error if Suggest_Linked_Reference_API fails. * * @since 3.17.0 * @@ -206,7 +206,7 @@ public function test_generate_smart_links_returns_error_on_failure(): void { } /** - * Test that the add_smart_link method returns a valid response when adding a new smart link. + * Tests that the add_smart_link method returns a valid response when adding a new smart link. * * @since 3.17.0 * @@ -307,7 +307,6 @@ public function test_add_smart_link_returns_valid_response(): void { * @since 3.17.0 * * @covers \Parsely\REST_API\Content_Helper\Endpoint_Smart_Linking::add_multiple_smart_links - * * @uses \Parsely\Endpoints\Base_Endpoint::__construct * @uses \Parsely\Models\Base_Model::__construct * @uses \Parsely\Models\Base_Model::serialize diff --git a/tests/Integration/RestAPI/ContentHelper/EndpointTitleSuggestionsTest.php b/tests/Integration/RestAPI/ContentHelper/EndpointTitleSuggestionsTest.php index a54e9391b3..15fa9e622f 100644 --- a/tests/Integration/RestAPI/ContentHelper/EndpointTitleSuggestionsTest.php +++ b/tests/Integration/RestAPI/ContentHelper/EndpointTitleSuggestionsTest.php @@ -3,7 +3,7 @@ * Integration tests for the Endpoint_Title_Suggestions class. * * @package Parsely - * @since 3.17.0 + * @since 3.17.0 */ declare(strict_types=1); @@ -38,7 +38,7 @@ class EndpointTitleSuggestionsTest extends BaseEndpointTest { private $endpoint; /** - * Set up the test environment. + * Sets up the test environment. * * @since 3.17.0 */ @@ -51,7 +51,7 @@ public function set_up(): void { } /** - * Get the test endpoint instance. + * Gets the test endpoint instance. * * @return Endpoint_Title_Suggestions * @since 3.17.0 @@ -61,7 +61,7 @@ public function get_endpoint(): \Parsely\REST_API\Base_Endpoint { } /** - * Test that the endpoint is correctly registered. + * Tests that the endpoint is correctly registered. * * @since 3.17.0 * @@ -105,7 +105,7 @@ public function test_route_is_registered(): void { } /** - * Test that the generate_titles method returns a valid response. + * Tests that the generate_titles method returns a valid response. * * @since 3.17.0 * @@ -153,7 +153,7 @@ public function test_generate_titles_returns_valid_response(): void { } /** - * Test that the generate_titles method returns an error if Suggest_Headline_API fails. + * Tests that the generate_titles method returns an error if Suggest_Headline_API fails. * * @since 3.17.0 * diff --git a/tests/Integration/RestAPI/RestAPIControllerTest.php b/tests/Integration/RestAPI/RestAPIControllerTest.php index 0261c5ec40..f53c8961bd 100644 --- a/tests/Integration/RestAPI/RestAPIControllerTest.php +++ b/tests/Integration/RestAPI/RestAPIControllerTest.php @@ -33,19 +33,19 @@ class RestAPIControllerTest extends TestCase { private $test_controller = null; /** - * Set up the test controller. + * Sets up the test controller. * * @since 3.17.0 */ - public function setUp(): void { - parent::setUp(); + public function set_up(): void { + parent::set_up(); TestCase::set_options(); $parsely = self::createMock( Parsely::class ); $this->test_controller = new REST_API_Controller( $parsely ); } /** - * Test the constructor sets up the correct namespace and version. + * Tests the constructor sets up the correct namespace and version. * * @since 3.17.0 * From 5e606da3623f962f999e9fd21f4b22477606cc13 Mon Sep 17 00:00:00 2001 From: Henrique Mouta Date: Thu, 29 Aug 2024 11:54:54 +0100 Subject: [PATCH 16/49] Add `stats` endpoint --- src/RemoteAPI/class-base-endpoint-remote.php | 6 +- src/rest-api/class-base-endpoint.php | 8 +- src/rest-api/class-rest-api-controller.php | 2 + .../class-endpoint-smart-linking.php | 83 +-- src/rest-api/stats/class-endpoint-post.php | 495 ++++++++++++++++++ src/rest-api/stats/class-endpoint-posts.php | 249 +++++++++ src/rest-api/stats/class-endpoint-related.php | 112 ++++ src/rest-api/stats/class-stats-controller.php | 48 ++ src/rest-api/stats/trait-post-data.php | 145 +++++ src/rest-api/stats/trait-related-posts.php | 165 ++++++ src/rest-api/trait-use-post-id-parameter.php | 88 ++++ 11 files changed, 1332 insertions(+), 69 deletions(-) create mode 100644 src/rest-api/stats/class-endpoint-post.php create mode 100644 src/rest-api/stats/class-endpoint-posts.php create mode 100644 src/rest-api/stats/class-endpoint-related.php create mode 100644 src/rest-api/stats/class-stats-controller.php create mode 100644 src/rest-api/stats/trait-post-data.php create mode 100644 src/rest-api/stats/trait-related-posts.php create mode 100644 src/rest-api/trait-use-post-id-parameter.php diff --git a/src/RemoteAPI/class-base-endpoint-remote.php b/src/RemoteAPI/class-base-endpoint-remote.php index 334f38e0d9..e9ec20eb38 100644 --- a/src/RemoteAPI/class-base-endpoint-remote.php +++ b/src/RemoteAPI/class-base-endpoint-remote.php @@ -108,7 +108,11 @@ public function get_items( array $query, bool $associative = false ) { } if ( ! property_exists( $decoded, 'data' ) ) { - return new WP_Error( $decoded->code ?? 400, $decoded->message ?? __( 'Unable to read data from upstream API', 'wp-parsely' ) ); + return new WP_Error( + $decoded->code ?? 400, + $decoded->message ?? __( 'Unable to read data from upstream API', 'wp-parsely' ), + array( 'status' => $decoded->code ?? 400 ) + ); } if ( ! is_array( $decoded->data ) ) { diff --git a/src/rest-api/class-base-endpoint.php b/src/rest-api/class-base-endpoint.php index 653a13f278..e489174323 100644 --- a/src/rest-api/class-base-endpoint.php +++ b/src/rest-api/class-base-endpoint.php @@ -169,7 +169,13 @@ public function register_rest_route( string $route, array $methods, callable $ca * @return string */ public function get_full_endpoint( string $route = '' ): string { - $route = $this->get_endpoint_name() . '/' . $route; + $route = trim( $route, '/' ); + + if ( '' !== $route ) { + $route = $this->get_endpoint_name() . '/' . $route; + } else { + $route = $this->get_endpoint_name(); + } return '/' . $this->api_controller->get_full_namespace() . diff --git a/src/rest-api/class-rest-api-controller.php b/src/rest-api/class-rest-api-controller.php index e864e59fad..5ce4e76fac 100644 --- a/src/rest-api/class-rest-api-controller.php +++ b/src/rest-api/class-rest-api-controller.php @@ -11,6 +11,7 @@ namespace Parsely\REST_API; use Parsely\REST_API\Content_Helper\Content_Helper_Controller; +use Parsely\REST_API\Stats\Stats_Controller; /** * The REST API Controller. @@ -60,6 +61,7 @@ public function init(): void { // Register the controllers for each namespace. $controllers = array( new Content_Helper_Controller( $this->get_parsely() ), + new Stats_Controller( $this->get_parsely() ), ); // Initialize the controllers. diff --git a/src/rest-api/content-helper/class-endpoint-smart-linking.php b/src/rest-api/content-helper/class-endpoint-smart-linking.php index fe37e324ff..cce1c99c43 100644 --- a/src/rest-api/content-helper/class-endpoint-smart-linking.php +++ b/src/rest-api/content-helper/class-endpoint-smart-linking.php @@ -14,6 +14,7 @@ use Parsely\Models\Smart_Link; use Parsely\RemoteAPI\ContentSuggestions\Suggest_Linked_Reference_API; use Parsely\REST_API\Base_Endpoint; +use Parsely\REST_API\Use_Post_ID_Parameter_Trait; use WP_Error; use WP_Post; use WP_REST_Request; @@ -28,6 +29,7 @@ */ class Endpoint_Smart_Linking extends Base_Endpoint { use Content_Helper_Feature; + use Use_Post_ID_Parameter_Trait; /** * The Suggest Linked Reference API instance. @@ -110,40 +112,28 @@ public function register_routes(): void { * GET /smart-linking/{post_id}/get * Gets the smart links for a post. */ - $this->register_rest_route( - '(?P\d+)/get', + $this->register_rest_route_with_post_id( + '/get', array( 'GET' ), - array( $this, 'get_smart_links' ), - array( - 'post_id' => array( - 'required' => true, - 'description' => __( 'The post ID.', 'wp-parsely' ), - 'validate_callback' => array( $this, 'validate_post_id' ), - ), - ) + array( $this, 'get_smart_links' ) ); /** * POST /smart-linking/{post_id}/add * Adds a smart link to a post. */ - $this->register_rest_route( - '(?P\d+)/add', + $this->register_rest_route_with_post_id( + '/add', array( 'POST' ), array( $this, 'add_smart_link' ), array( - 'post_id' => array( - 'required' => true, - 'description' => __( 'The post ID.', 'wp-parsely' ), - 'validate_callback' => array( $this, 'validate_post_id' ), - ), - 'link' => array( + 'link' => array( 'required' => true, 'type' => 'object', 'description' => __( 'The smart link data to add.', 'wp-parsely' ), 'validate_callback' => array( $this, 'validate_smart_link_params' ), ), - 'update' => array( + 'update' => array( 'type' => 'boolean', 'description' => __( 'Whether to update the existing smart link.', 'wp-parsely' ), 'default' => false, @@ -155,23 +145,18 @@ public function register_routes(): void { * POST /smart-linking/{post_id}/add-multiple * Adds multiple smart links to a post. */ - $this->register_rest_route( - '(?P\d+)/add-multiple', + $this->register_rest_route_with_post_id( + '/add-multiple', array( 'POST' ), array( $this, 'add_multiple_smart_links' ), array( - 'post_id' => array( - 'required' => true, - 'description' => __( 'The post ID.', 'wp-parsely' ), - 'validate_callback' => array( $this, 'validate_post_id' ), - ), - 'links' => array( + 'links' => array( 'required' => true, 'type' => 'array', 'description' => __( 'The multiple smart links data to add.', 'wp-parsely' ), 'validate_callback' => array( $this, 'validate_multiple_smart_links' ), ), - 'update' => array( + 'update' => array( 'type' => 'boolean', 'description' => __( 'Whether to update the existing smart links.', 'wp-parsely' ), 'default' => false, @@ -183,17 +168,12 @@ public function register_routes(): void { * POST /smart-linking/{post_id}/set * Updates the smart links of a given post and removes the ones that are not in the request. */ - $this->register_rest_route( - '(?P\d+)/set', + $this->register_rest_route_with_post_id( + '/set', array( 'POST' ), array( $this, 'set_smart_links' ), array( - 'post_id' => array( - 'required' => true, - 'description' => __( 'The post ID.', 'wp-parsely' ), - 'validate_callback' => array( $this, 'validate_post_id' ), - ), - 'links' => array( + 'links' => array( 'required' => true, 'type' => 'array', 'description' => __( 'The smart links data to set.', 'wp-parsely' ), @@ -548,37 +528,6 @@ public function url_to_post_type( WP_REST_Request $request ): WP_REST_Response { return new WP_REST_Response( $response, 200 ); } - /** - * Validates the post ID parameter. - * - * The callback sets the post object in the request object if the parameter is valid. - * - * @since 3.16.0 - * @access private - * - * @param string $param The parameter value. - * @param WP_REST_Request $request The request object. - * @return bool Whether the parameter is valid. - */ - public function validate_post_id( string $param, WP_REST_Request $request ): bool { - if ( ! is_numeric( $param ) ) { - return false; - } - - $param = filter_var( $param, FILTER_VALIDATE_INT ); - - if ( false === $param ) { - return false; - } - - // Validate if the post ID exists. - $post = get_post( $param ); - // Set the post attribute in the request. - $request->set_param( 'post', $post ); - - return null !== $post; - } - /** * Validates the URL exclusion list parameter. * diff --git a/src/rest-api/stats/class-endpoint-post.php b/src/rest-api/stats/class-endpoint-post.php new file mode 100644 index 0000000000..c837133150 --- /dev/null +++ b/src/rest-api/stats/class-endpoint-post.php @@ -0,0 +1,495 @@ +analytics_post_detail_api = new Analytics_Post_Detail_API( $this->parsely ); + $this->referrers_post_detail_api = new Referrers_Post_Detail_API( $this->parsely ); + $this->related_posts_api = new Related_API( $this->parsely ); + } + + /** + * Returns the endpoint name. + * + * @since 3.17.0 + * + * @return string The endpoint name. + */ + public function get_endpoint_name(): string { + return 'post'; + } + + /** + * Registers the routes for the endpoint. + * + * @since 3.17.0 + */ + public function register_routes(): void { + /** + * GET /stats/post/{post_id}/details + * Returns the analytics details of a post. + */ + $this->register_rest_route_with_post_id( + '/details', + array( 'GET' ), + array( $this, 'get_post_details' ), + array_merge( + array( + 'period_start' => array( + 'description' => __( 'The start of the period.', 'wp-parsely' ), + 'type' => 'string', + 'required' => false, + ), + 'period_end' => array( + 'description' => __( 'The end of the period.', 'wp-parsely' ), + 'type' => 'string', + 'required' => false, + ), + ), + $this->get_itm_source_param_args() + ) + ); + + /** + * GET /stats/post/{post_id}/referrers + * Returns the referrers of a post. + */ + $this->register_rest_route_with_post_id( + '/referrers', + array( 'GET' ), + array( $this, 'get_post_referrers' ), + array( + 'period_start' => array( + 'description' => __( 'The start of the period.', 'wp-parsely' ), + 'type' => 'string', + 'required' => false, + ), + 'period_end' => array( + 'description' => __( 'The end of the period.', 'wp-parsely' ), + 'type' => 'string', + 'required' => false, + ), + 'total_views' => array( + 'description' => __( 'The total views of the post.', 'wp-parsely' ), + 'type' => 'string', + 'required' => false, + 'default' => '0', + ), + ) + ); + + /** + * GET /stats/post/{post_id}/related + * Returns the related posts of a post. + */ + $this->register_rest_route_with_post_id( + '/related', + array( 'GET' ), + array( $this, 'get_related_posts' ), + $this->get_related_posts_param_args() + ); + } + + /** + * API Endpoint: GET /stats/post/{post_id}/details + * + * Gets the details of a post. + * + * @since 3.17.0 + * + * @param WP_REST_Request $request The request object. + * @return WP_REST_Response|WP_Error The response object. + */ + public function get_post_details( WP_REST_Request $request ) { + /** + * The post object. + * + * @var WP_Post $post + */ + $post = $request->get_param( 'post' ); + $permalink = get_permalink( $post->ID ); + + // Set the itm_source parameter. + $this->set_itm_source_from_request( $request ); + + // Get the data from the API. + $analytics_request = $this->analytics_post_detail_api->get_items( + array( + 'url' => $permalink, + 'period_start' => $request->get_param( 'period_start' ), + 'period_end' => $request->get_param( 'period_end' ), + ) + ); + + if ( is_wp_error( $analytics_request ) ) { + return $analytics_request; + } + + $post_data = array(); + /** + * The analytics data object. + * + * @var array $analytics_request + */ + foreach ( $analytics_request as $data ) { + $post_data[] = $this->extract_post_data( $data ); + } + + $response = array( + 'params' => $request->get_params(), + 'data' => $post_data, + ); + + return new WP_REST_Response( $response, 200 ); + } + + /** + * API Endpoint: GET /stats/post/{post_id}/referrers + * + * Gets the referrers of a post. + * + * @since 3.17.0 + * + * @param WP_REST_Request $request The request object. + * @return WP_REST_Response|WP_Error The response object. + */ + public function get_post_referrers( WP_REST_Request $request ) { + /** + * The post object. + * + * @var WP_Post $post + */ + $post = $request->get_param( 'post' ); + $permalink = get_permalink( $post->ID ); + + // Set the itm_source parameter. + $this->set_itm_source_from_request( $request ); + + // Get the total views. + $total_views = $request->get_param( 'total_views' ) ?? 0; + + if ( is_string( $total_views ) ) { + $total_views = Utils::convert_to_positive_integer( $total_views ); + } + + $this->total_views = $total_views; + + // Do the analytics request. + $analytics_request = $this->referrers_post_detail_api->get_items( + array( + 'url' => $permalink, + 'period_start' => $request->get_param( 'period_start' ), + 'period_end' => $request->get_param( 'period_end' ), + ) + ); + + if ( is_wp_error( $analytics_request ) ) { + return $analytics_request; + } + + /** + * The analytics data object. + * + * @var array $analytics_request + */ + $referrers_types = $this->generate_referrer_types_data( $analytics_request ); + $direct_views = Utils::convert_to_positive_integer( + $referrers_types->direct->views ?? '0' + ); + $referrers_top = $this->generate_referrers_data( 5, $analytics_request, $direct_views ); + + $response_data = array( + 'params' => $request->get_params(), + 'data' => array( + 'top' => $referrers_top, + 'types' => $referrers_types, + ), + ); + + return new WP_REST_Response( $response_data, 200 ); + } + + /** + * API Endpoint: GET /stats/post/{post_id}/related + * + * Gets the related posts of a post. + * + * @since 3.17.0 + * + * @param WP_REST_Request $request The request object. + * @return WP_REST_Response|WP_Error The response data. + */ + public function get_related_posts( WP_REST_Request $request ) { + /** + * The post object. + * + * @var WP_Post $post + */ + $post = $request->get_param( 'post' ); + + /** + * The post permalink. + * + * @var string $permalink + */ + $permalink = get_permalink( $post->ID ); + + $related_posts = $this->get_related_posts_of_url( $request, $permalink ); + + $response_data = array( + 'params' => $request->get_params(), + 'data' => $related_posts, + ); + return new WP_REST_Response( $response_data, 200 ); + } + + /** + * Generates the referrer types data. + * + * Referrer types are: + * - `social`: Views coming from social media. + * - `search`: Views coming from search engines. + * - `other`: Views coming from other referrers, like external websites. + * - `internal`: Views coming from linking pages of the same website. + * + * Returned object properties: + * - `views`: The number of views. + * - `viewPercentage`: The number of views as a percentage, compared to the + * total views of all referrer types. + * + * @since 3.6.0 + * @since 3.17.0 Moved from the `Referrers_Post_Detail_API_Proxy` class. + * + * @param array $response The response received by the proxy. + * @return stdClass The generated data. + */ + private function generate_referrer_types_data( array $response ): stdClass { + $result = new stdClass(); + $total_referrer_views = 0; // Views from all referrer types combined. + + // Set referrer type order as it is displayed in the Parse.ly dashboard. + $referrer_type_keys = array( 'social', 'search', 'other', 'internal', 'direct' ); + foreach ( $referrer_type_keys as $key ) { + $result->$key = (object) array( 'views' => 0 ); + } + + // Set views and views totals. + foreach ( $response as $referrer_data ) { + /** + * Variable. + * + * @var int + */ + $current_views = $referrer_data->metrics->referrers_views ?? 0; + $total_referrer_views += $current_views; + + /** + * Variable. + * + * @var string + */ + $current_key = $referrer_data->type ?? ''; + if ( '' !== $current_key ) { + if ( ! isset( $result->$current_key->views ) ) { + $result->$current_key = (object) array( 'views' => 0 ); + } + + $result->$current_key->views += $current_views; + } + } + + // Add direct and total views to the object. + $result->direct->views = $this->total_views - $total_referrer_views; + $result->totals = (object) array( 'views' => $this->total_views ); + + // Remove referrer types without views. + foreach ( $referrer_type_keys as $key ) { + if ( 0 === $result->$key->views ) { + unset( $result->$key ); + } + } + + // Set percentage values and format numbers. + // @phpstan-ignore-next-line. + foreach ( $result as $key => $value ) { + // Set and format percentage values. + $result->{ $key }->viewsPercentage = $this->get_i18n_percentage( + absint( $value->views ), + $this->total_views + ); + + // Format views values. + $result->{ $key }->views = number_format_i18n( $result->{ $key }->views ); + } + + return $result; + } + + /** + * Generates the top referrers data. + * + * Returned object properties: + * - `views`: The number of views. + * - `viewPercentage`: The number of views as a percentage, compared to the + * total views of all referrer types. + * - `datasetViewsPercentage: The number of views as a percentage, compared + * to the total views of the current dataset. + * + * @since 3.6.0 + * @since 3.17.0 Moved from the `Referrers_Post_Detail_API_Proxy` class. + * + * @param int $limit The limit of returned referrers. + * @param array $response The response received by the proxy. + * @param int $direct_views The count of direct views. + * @return stdClass The generated data. + */ + private function generate_referrers_data( + int $limit, + array $response, + int $direct_views + ): stdClass { + $temp_views = array(); + $totals = 0; + $referrer_count = count( $response ); + + // Set views and views totals. + $loop_count = $referrer_count > $limit ? $limit : $referrer_count; + for ( $i = 0; $i < $loop_count; $i++ ) { + $data = $response[ $i ]; + + /** + * Variable. + * + * @var int + */ + $referrer_views = $data->metrics->referrers_views ?? 0; + $totals += $referrer_views; + if ( isset( $data->name ) ) { + $temp_views[ $data->name ] = $referrer_views; + } + } + + // If applicable, add the direct views. + if ( isset( $referrer_views ) && $direct_views >= $referrer_views ) { + $temp_views['direct'] = $direct_views; + $totals += $direct_views; + arsort( $temp_views ); + if ( count( $temp_views ) > $limit ) { + $totals -= array_pop( $temp_views ); + } + } + + // Convert temporary array to result object and add totals. + $result = new stdClass(); + foreach ( $temp_views as $key => $value ) { + $result->$key = (object) array( 'views' => $value ); + } + $result->totals = (object) array( 'views' => $totals ); + + // Set percentages values and format numbers. + // @phpstan-ignore-next-line. + foreach ( $result as $key => $value ) { + // Percentage against all referrer views, even those not included + // in the dataset due to the $limit argument. + $result->{ $key }->viewsPercentage = $this + ->get_i18n_percentage( absint( $value->views ), $this->total_views ); + + // Percentage against the current dataset that is limited due to the + // $limit argument. + $result->{ $key }->datasetViewsPercentage = $this + ->get_i18n_percentage( absint( $value->views ), $totals ); + + // Format views values. + $result->{ $key }->views = number_format_i18n( $result->{ $key }->views ); + } + + return $result; + } + + /** + * Returns the passed number compared to the passed total, in an + * internationalized percentage format. + * + * @since 3.6.0 + * @since 3.17.0 Moved from the `Referrers_Post_Detail_API_Proxy` class. + * + * @param int $number The number to be calculated as a percentage. + * @param int $total The total number to compare against. + * @return string|false The internationalized percentage or false on error. + */ + private function get_i18n_percentage( int $number, int $total ) { + if ( 0 === $total ) { + return false; + } + + return number_format_i18n( $number / $total * 100, 2 ); + } +} diff --git a/src/rest-api/stats/class-endpoint-posts.php b/src/rest-api/stats/class-endpoint-posts.php new file mode 100644 index 0000000000..cb97310771 --- /dev/null +++ b/src/rest-api/stats/class-endpoint-posts.php @@ -0,0 +1,249 @@ +analytics_posts_api = new Analytics_Posts_API( $this->parsely ); + } + + /** + * Returns the endpoint name. + * + * @since 3.17.0 + * + * @return string + */ + public function get_endpoint_name(): string { + return 'posts'; + } + + /** + * Registers the routes for the objects of the controller. + */ + public function register_routes(): void { + /** + * GET /posts + * Retrieves the top posts for the given period. + */ + $this->register_rest_route( + '/', + array( 'GET' ), + array( $this, 'get_posts' ), + array_merge( + array( + 'period_start' => array( + 'description' => 'The start of the period to query.', + 'type' => 'string', + 'required' => false, + ), + 'period_end' => array( + 'description' => 'The end of the period to query.', + 'type' => 'string', + 'required' => false, + ), + 'pub_date_start' => array( + 'description' => 'The start of the publication date range to query.', + 'type' => 'string', + 'required' => false, + ), + 'pub_date_end' => array( + 'description' => 'The end of the publication date range to query.', + 'type' => 'string', + 'required' => false, + ), + 'limit' => array( + 'description' => 'The number of posts to return.', + 'type' => 'integer', + 'required' => false, + 'default' => self::TOP_POSTS_DEFAULT_LIMIT, + ), + 'sort' => array( + 'description' => 'The sort order of the posts.', + 'type' => 'string', + 'enum' => self::SORT_METRICS, + 'default' => self::SORT_DEFAULT, + 'required' => false, + ), + 'page' => array( + 'description' => 'The page to fetch.', + 'type' => 'integer', + 'required' => false, + 'default' => 1, + ), + 'author' => array( + 'description' => 'The author to filter by.', + 'type' => 'array', + 'items' => array( + 'type' => 'string', + ), + 'required' => false, + 'maxItems' => 5, + ), + 'section' => array( + 'description' => 'The section to filter by.', + 'type' => 'string', + 'required' => false, + ), + 'tag' => array( + 'description' => 'The tag to filter by.', + 'type' => 'array', + 'items' => array( + 'type' => 'string', + ), + 'required' => false, + 'maxItems' => 5, + ), + 'segment' => array( + 'description' => 'The segment to filter by.', + 'type' => 'string', + 'required' => false, + ), + ), + $this->get_itm_source_param_args() + ) + ); + } + + /** + * API Endpoint: GET /stats/posts + * + * Retrieves the top posts for the given query parameters. + * + * @param WP_REST_Request $request The request. + * + * @return stdClass[]|WP_Error|WP_REST_Response + */ + public function get_posts( WP_REST_Request $request ) { + $params = $request->get_params(); + + // Setup the itm_source if it is provided. + $this->set_itm_source_from_request( $request ); + + // TODO: Needed before the Public API refactor. + // Convert array of authors to a string with the first element. + if ( isset( $params['author'] ) && is_array( $params['author'] ) ) { + $params['author'] = $params['author'][0]; + } + // Convert array of tags to a string with the first element. + if ( isset( $params['tag'] ) && is_array( $params['tag'] ) ) { + $params['tag'] = $params['tag'][0]; + } + // TODO END. + + // Do the analytics request. + /** + * The raw analytics data. + * + * @var stdClass[]|WP_Error $analytics_request + */ + $analytics_request = $this->analytics_posts_api->get_items( + array( + 'period_start' => $params['period_start'] ?? null, + 'period_end' => $params['period_end'] ?? null, + 'pub_date_start' => $params['pub_date_start'] ?? null, + 'pub_date_end' => $params['pub_date_end'] ?? null, + 'limit' => $params['limit'] ?? self::TOP_POSTS_DEFAULT_LIMIT, + 'sort' => $params['sort'] ?? self::SORT_DEFAULT, + 'page' => $params['page'] ?? 1, + 'author' => $params['author'] ?? null, + 'section' => $params['section'] ?? null, + 'tag' => $params['tag'] ?? null, + 'segment' => $params['segment'] ?? null, + 'itm_source' => $params['itm_source'] ?? null, + ) + ); + + if ( is_wp_error( $analytics_request ) ) { + return $analytics_request; + } + + // Process the data. + $posts = array(); + foreach ( $analytics_request as $item ) { + $posts[] = $this->extract_post_data( $item ); + } + + $response = array( + 'params' => $params, + 'data' => $posts, + ); + + return new WP_REST_Response( $response, 200 ); + } +} diff --git a/src/rest-api/stats/class-endpoint-related.php b/src/rest-api/stats/class-endpoint-related.php new file mode 100644 index 0000000000..d675e9e923 --- /dev/null +++ b/src/rest-api/stats/class-endpoint-related.php @@ -0,0 +1,112 @@ +related_posts_api = new Related_API( $this->parsely ); + } + + /** + * Returns the endpoint name. + * + * @since 3.17.0 + * + * @return string + */ + public function get_endpoint_name(): string { + return 'related'; + } + + /** + * Registers the routes for the endpoint. + * + * @since 3.17.0 + */ + public function register_routes(): void { + /** + * GET /related + * Gets related posts. + */ + $this->register_rest_route( + '/', + array( 'GET' ), + array( $this, 'get_related_posts' ), + array( + 'url' => array( + 'description' => __( 'The URL of the post.', 'wp-parsely' ), + 'type' => 'string', + 'required' => true, + ), + $this->get_related_posts_param_args(), + ) + ); + } + + /** + * API Endpoint: GET /stats/related + * + * Gets related posts for a given URL. + * + * @since 3.17.0 + * + * @param WP_REST_Request $request The request object. + * @return WP_REST_Response|WP_Error + */ + public function get_related_posts( WP_REST_Request $request ) { + $url = $request->get_param( 'url' ); + + $related_posts = $this->get_related_posts_of_url( $request, $url ); + + if ( is_wp_error( $related_posts ) ) { + return $related_posts; + } + + return new WP_REST_Response( array( 'data' => $related_posts ), 200 ); + } + + /** + * Returns whether the endpoint is available for access by the current + * user. + * + * @since 3.17.0 + * + * @param WP_REST_Request|null $request The request object. + * @return bool + */ + public function is_available_to_current_user( ?WP_REST_Request $request = null ): bool { + return true; + } +} diff --git a/src/rest-api/stats/class-stats-controller.php b/src/rest-api/stats/class-stats-controller.php new file mode 100644 index 0000000000..ecae2387be --- /dev/null +++ b/src/rest-api/stats/class-stats-controller.php @@ -0,0 +1,48 @@ +register_endpoints( $endpoints ); + } +} diff --git a/src/rest-api/stats/trait-post-data.php b/src/rest-api/stats/trait-post-data.php new file mode 100644 index 0000000000..4698d2a9a4 --- /dev/null +++ b/src/rest-api/stats/trait-post-data.php @@ -0,0 +1,145 @@ +itm_source = $source; + } + + /** + * Sets the itm_source value from the request, if it exists. + * + * @since 3.17.0 + * + * @param WP_REST_Request $request The request object. + */ + private function set_itm_source_from_request( WP_REST_Request $request ): void { + $source = $request->get_param( 'itm_source' ); + if ( null !== $source ) { + $this->set_itm_source( $source ); + } + } + + /** + * Returns the itm_source parameter arguments, to be used in the REST API + * route registration. + * + * @since 3.17.0 + * + * @return array + */ + private function get_itm_source_param_args(): array { + return array( + 'itm_source' => array( + 'description' => __( 'The source of the item.', 'wp-parsely' ), + 'type' => 'string', + 'required' => false, + 'validate_callback' => array( $this, 'validate_itm_source' ), + ), + ); + } + + /** + * Extracts the post data from the passed object. + * + * Should only be used with endpoints that return post data. + * + * @since 3.10.0 + * @since 3.17.0 Moved from the old `Base_API_Proxy` class. + * + * @param stdClass $item The object to extract the data from. + * @return array The extracted data. + */ + protected function extract_post_data( stdClass $item ): array { + $data = array(); + + if ( isset( $item->author ) ) { + $data['author'] = $item->author; + } + + if ( isset( $item->metrics->views ) ) { + $data['views'] = number_format_i18n( $item->metrics->views ); + } + + if ( isset( $item->metrics->visitors ) ) { + $data['visitors'] = number_format_i18n( $item->metrics->visitors ); + } + + // The avg_engaged metric can be in different locations depending on the + // endpoint and passed sort/url parameters. + $avg_engaged = $item->metrics->avg_engaged ?? $item->avg_engaged ?? null; + if ( null !== $avg_engaged ) { + $data['avgEngaged'] = Utils::get_formatted_duration( (float) $avg_engaged ); + } + + if ( isset( $item->pub_date ) ) { + $data['date'] = wp_date( Utils::get_date_format(), strtotime( $item->pub_date ) ); + } + + if ( isset( $item->title ) ) { + $data['title'] = $item->title; + } + + if ( isset( $item->url ) ) { + $site_id = $this->parsely->get_site_id(); + // phpcs:ignore WordPressVIPMinimum.Functions.RestrictedFunctions.url_to_postid_url_to_postid + $post_id = url_to_postid( $item->url ); // 0 if the post cannot be found. + + $post_url = Parsely::get_url_with_itm_source( $item->url, null ); + if ( Utils::parsely_is_https_supported() ) { + $post_url = str_replace( 'http://', 'https://', $post_url ); + } + + $data['rawUrl'] = $post_url; + $data['dashUrl'] = Parsely::get_dash_url( $site_id, $post_url ); + $data['id'] = Parsely::get_url_with_itm_source( $post_url, null ); // Unique. + $data['postId'] = $post_id; // Might not be unique. + $data['url'] = Parsely::get_url_with_itm_source( $post_url, $this->itm_source ); + + // Set thumbnail URL, falling back to the Parse.ly thumbnail if needed. + $thumbnail_url = get_the_post_thumbnail_url( $post_id, 'thumbnail' ); + if ( false !== $thumbnail_url ) { + $data['thumbnailUrl'] = $thumbnail_url; + } elseif ( isset( $item->thumb_url_medium ) ) { + $data['thumbnailUrl'] = $item->thumb_url_medium; + } + } + + return $data; + } +} diff --git a/src/rest-api/stats/trait-related-posts.php b/src/rest-api/stats/trait-related-posts.php new file mode 100644 index 0000000000..dadab370a3 --- /dev/null +++ b/src/rest-api/stats/trait-related-posts.php @@ -0,0 +1,165 @@ + + */ + private function get_related_posts_param_args(): array { + return array_merge( + array( + 'sort' => array( + 'description' => __( 'The sort order.', 'wp-parsely' ), + 'type' => 'string', + 'enum' => array( '_score', 'pub_date' ), + 'required' => false, + 'default' => '_score', + ), + 'limit' => array( + 'description' => __( 'The number of related posts to return.', 'wp-parsely' ), + 'type' => 'integer', + 'required' => false, + 'default' => 10, + ), + 'pub_date_start' => array( + 'description' => __( 'The start of the publication date.', 'wp-parsely' ), + 'type' => 'string', + 'required' => false, + ), + 'pub_date_end' => array( + 'description' => __( 'The end of the publication date.', 'wp-parsely' ), + 'type' => 'string', + 'required' => false, + ), + 'page' => array( + 'description' => __( 'The page number.', 'wp-parsely' ), + 'type' => 'integer', + 'required' => false, + 'default' => 1, + ), + 'section' => array( + 'description' => __( 'The section of the post.', 'wp-parsely' ), + 'type' => 'string', + 'required' => false, + ), + 'tag' => array( + 'description' => __( 'The tag of the post.', 'wp-parsely' ), + 'type' => 'string', + 'required' => false, + ), + 'author' => array( + 'description' => __( 'The author of the post.', 'wp-parsely' ), + 'type' => 'string', + 'required' => false, + ), + ), + $this->get_itm_source_param_args() + ); + } + + /** + * Get related posts for a given URL. + * + * @since 3.17.0 + * + * @param WP_REST_Request $request The request object. + * @param string $url The URL to get related posts for. + * @return array|WP_Error + */ + public function get_related_posts_of_url( WP_REST_Request $request, string $url ) { + // Set the itm_source parameter. + $this->set_itm_source_from_request( $request ); + + // Get the data from the API. + /** + * The related posts request. + * + * @var array|WP_Error $related_posts_request + */ + $related_posts_request = $this->related_posts_api->get_items( + array( + 'url' => $url, + 'sort' => $request->get_param( 'sort' ), + 'limit' => $request->get_param( 'limit' ), + 'pub_date_start' => $request->get_param( 'pub_date_start' ), + 'pub_date_end' => $request->get_param( 'pub_date_end' ), + 'page' => $request->get_param( 'page' ), + 'section' => $request->get_param( 'section' ), + 'tag' => $request->get_param( 'tag' ), + 'author' => $request->get_param( 'author' ), + ) + ); + + if ( is_wp_error( $related_posts_request ) ) { + return $related_posts_request; + } + + $itm_source = $this->itm_source; + + $related_posts = array_map( + static function ( stdClass $item ) use ( $itm_source ) { + return (object) array( + 'image_url' => $item->image_url, + 'thumb_url_medium' => $item->thumb_url_medium, + 'title' => $item->title, + 'url' => Parsely::get_url_with_itm_source( $item->url, $itm_source ), + ); + }, + $related_posts_request + ); + + return $related_posts; + } +} diff --git a/src/rest-api/trait-use-post-id-parameter.php b/src/rest-api/trait-use-post-id-parameter.php new file mode 100644 index 0000000000..4459636e90 --- /dev/null +++ b/src/rest-api/trait-use-post-id-parameter.php @@ -0,0 +1,88 @@ + $methods The HTTP methods. + * @param callable $callback The callback function. + * @param array $args The route arguments. + */ + public function register_rest_route_with_post_id( + string $route, + array $methods, + callable $callback, + array $args = array() + ): void { + // Append the post_id parameter to the route. + $route = '/(?P\d+)/' . trim( $route, '/' ); + + // Add the post_id parameter to the args. + $args = array_merge( + $args, + array( + 'post_id' => array( + 'description' => __( 'The ID of the post.', 'wp-parsely' ), + 'type' => 'integer', + 'required' => true, + 'validate_callback' => array( $this, 'validate_post_id' ), + ), + ) + ); + + // Register the route. + $this->register_rest_route( $route, $methods, $callback, $args ); + } + + /** + * Validates the post ID parameter. + * + * The callback sets the post object in the request object if the parameter is valid. + * + * @since 3.16.0 + * @access private + * + * @param string $param The parameter value. + * @param WP_REST_Request $request The request object. + * @return bool Whether the parameter is valid. + */ + public function validate_post_id( string $param, WP_REST_Request $request ): bool { + if ( ! is_numeric( $param ) ) { + return false; + } + + $param = filter_var( $param, FILTER_VALIDATE_INT ); + + if ( false === $param ) { + return false; + } + + // Validate if the post ID exists. + $post = get_post( $param ); + // Set the post attribute in the request. + $request->set_param( 'post', $post ); + + return null !== $post; + } +} From 89ef1bba02293f5eeae5a63064d82843db44b23d Mon Sep 17 00:00:00 2001 From: Henrique Mouta Date: Thu, 29 Aug 2024 11:56:38 +0100 Subject: [PATCH 17/49] Update the UI to use the new API endpoints --- build/blocks/recommendations/edit.asset.php | 2 +- build/blocks/recommendations/edit.js | 2 +- build/blocks/recommendations/view.asset.php | 2 +- build/blocks/recommendations/view.js | 2 +- .../content-helper/dashboard-widget.asset.php | 2 +- build/content-helper/dashboard-widget.js | 2 +- build/content-helper/editor-sidebar.asset.php | 2 +- build/content-helper/editor-sidebar.js | 8 ++--- .../parsely-recommendations-fetcher.tsx | 2 +- .../dashboard-widget/provider.ts | 2 +- .../performance-stats/provider.ts | 36 +++++++++---------- .../editor-sidebar/related-posts/provider.ts | 2 +- 12 files changed, 31 insertions(+), 33 deletions(-) diff --git a/build/blocks/recommendations/edit.asset.php b/build/blocks/recommendations/edit.asset.php index 0f36c73fce..ce1f1eb731 100644 --- a/build/blocks/recommendations/edit.asset.php +++ b/build/blocks/recommendations/edit.asset.php @@ -1 +1 @@ - array('react', 'wp-api-fetch', 'wp-block-editor', 'wp-blocks', 'wp-components', 'wp-compose', 'wp-element', 'wp-i18n', 'wp-url'), 'version' => '393b4c8be66f3bde527e'); + array('react', 'wp-api-fetch', 'wp-block-editor', 'wp-blocks', 'wp-components', 'wp-compose', 'wp-element', 'wp-i18n', 'wp-url'), 'version' => 'f1121e53d4c8acf98db5'); diff --git a/build/blocks/recommendations/edit.js b/build/blocks/recommendations/edit.js index 06593e46cf..c923405cf3 100644 --- a/build/blocks/recommendations/edit.js +++ b/build/blocks/recommendations/edit.js @@ -1 +1 @@ -!function(){"use strict";var e,n={271:function(e,n,r){var t,o,a=r(848),i=window.wp.blockEditor,l=window.wp.blocks,s=window.wp.i18n,c=window.wp.components,u=JSON.parse('{"UU":"wp-parsely/recommendations","uK":{"imagestyle":{"type":"string","default":"original"},"limit":{"type":"number","default":3},"openlinksinnewtab":{"type":"boolean","default":false},"showimages":{"type":"boolean","default":true},"sort":{"type":"string","default":"score"},"title":{"type":"string","default":"Related Content"}}}'),d=window.wp.element;(o=t||(t={}))[o.Error=0]="Error",o[o.Loaded=1]="Loaded",o[o.Recommendations=2]="Recommendations";var p=function(){return p=Object.assign||function(e){for(var n,r=1,t=arguments.length;r0&&o[o.length-1])||6!==l[0]&&2!==l[0])){i=0;continue}if(3===l[0]&&(!o||l[1]>o[0]&&l[1]=a)&&Object.keys(t.O).every((function(e){return t.O[e](r[s])}))?r.splice(s--,1):(l=!1,a0&&e[u-1][2]>a;u--)e[u]=e[u-1];e[u]=[r,o,a]},t.n=function(e){var n=e&&e.__esModule?function(){return e.default}:function(){return e};return t.d(n,{a:n}),n},t.d=function(e,n){for(var r in n)t.o(n,r)&&!t.o(e,r)&&Object.defineProperty(e,r,{enumerable:!0,get:n[r]})},t.o=function(e,n){return Object.prototype.hasOwnProperty.call(e,n)},function(){var e={335:0,203:0};t.O.j=function(n){return 0===e[n]};var n=function(n,r){var o,a,i=r[0],l=r[1],s=r[2],c=0;if(i.some((function(n){return 0!==e[n]}))){for(o in l)t.o(l,o)&&(t.m[o]=l[o]);if(s)var u=s(t)}for(n&&n(r);c0&&o[o.length-1])||6!==l[0]&&2!==l[0])){i=0;continue}if(3===l[0]&&(!o||l[1]>o[0]&&l[1]=a)&&Object.keys(t.O).every((function(e){return t.O[e](r[s])}))?r.splice(s--,1):(l=!1,a0&&e[u-1][2]>a;u--)e[u]=e[u-1];e[u]=[r,o,a]},t.n=function(e){var n=e&&e.__esModule?function(){return e.default}:function(){return e};return t.d(n,{a:n}),n},t.d=function(e,n){for(var r in n)t.o(n,r)&&!t.o(e,r)&&Object.defineProperty(e,r,{enumerable:!0,get:n[r]})},t.o=function(e,n){return Object.prototype.hasOwnProperty.call(e,n)},function(){var e={335:0,203:0};t.O.j=function(n){return 0===e[n]};var n=function(n,r){var o,a,i=r[0],l=r[1],s=r[2],c=0;if(i.some((function(n){return 0!==e[n]}))){for(o in l)t.o(l,o)&&(t.m[o]=l[o]);if(s)var u=s(t)}for(n&&n(r);c array('react', 'wp-api-fetch', 'wp-components', 'wp-compose', 'wp-dom-ready', 'wp-element', 'wp-i18n', 'wp-url'), 'version' => '827c1c0c5b711b046baf'); + array('react', 'wp-api-fetch', 'wp-components', 'wp-compose', 'wp-dom-ready', 'wp-element', 'wp-i18n', 'wp-url'), 'version' => 'af6eb0946975f32d53b1'); diff --git a/build/blocks/recommendations/view.js b/build/blocks/recommendations/view.js index ac32818be9..d8f4b45779 100644 --- a/build/blocks/recommendations/view.js +++ b/build/blocks/recommendations/view.js @@ -1 +1 @@ -!function(){"use strict";var e={20:function(e,r,n){var t=n(609),o=Symbol.for("react.element"),a=Symbol.for("react.fragment"),i=Object.prototype.hasOwnProperty,s=t.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED.ReactCurrentOwner,l={key:!0,ref:!0,__self:!0,__source:!0};function u(e,r,n){var t,a={},u=null,c=null;for(t in void 0!==n&&(u=""+n),void 0!==r.key&&(u=""+r.key),void 0!==r.ref&&(c=r.ref),r)i.call(r,t)&&!l.hasOwnProperty(t)&&(a[t]=r[t]);if(e&&e.defaultProps)for(t in r=e.defaultProps)void 0===a[t]&&(a[t]=r[t]);return{$$typeof:o,type:e,key:u,ref:c,props:a,_owner:s.current}}r.Fragment=a,r.jsx=u,r.jsxs=u},848:function(e,r,n){e.exports=n(20)},609:function(e){e.exports=window.React}},r={};function n(t){var o=r[t];if(void 0!==o)return o.exports;var a=r[t]={exports:{}};return e[t](a,a.exports,n),a.exports}n.n=function(e){var r=e&&e.__esModule?function(){return e.default}:function(){return e};return n.d(r,{a:r}),r},n.d=function(e,r){for(var t in r)n.o(r,t)&&!n.o(e,t)&&Object.defineProperty(e,t,{enumerable:!0,get:r[t]})},n.o=function(e,r){return Object.prototype.hasOwnProperty.call(e,r)},function(){var e,r,t=n(848),o=n(609),a=window.wp.domReady,i=n.n(a),s=window.wp.element,l=window.wp.i18n;(r=e||(e={}))[r.Error=0]="Error",r[r.Loaded=1]="Loaded",r[r.Recommendations=2]="Recommendations";var u=function(){return u=Object.assign||function(e){for(var r,n=1,t=arguments.length;n0&&o[o.length-1])||6!==s[0]&&2!==s[0])){i=0;continue}if(3===s[0]&&(!o||s[1]>o[0]&&s[1]0&&o[o.length-1])||6!==s[0]&&2!==s[0])){i=0;continue}if(3===s[0]&&(!o||s[1]>o[0]&&s[1] array('react', 'wp-api-fetch', 'wp-components', 'wp-data', 'wp-element', 'wp-i18n', 'wp-url'), 'version' => 'a741adf6df6b723b2f3d'); + array('react', 'wp-api-fetch', 'wp-components', 'wp-data', 'wp-element', 'wp-i18n', 'wp-url'), 'version' => 'd852566173ef137708b0'); diff --git a/build/content-helper/dashboard-widget.js b/build/content-helper/dashboard-widget.js index 50392fc4be..7b17d4192c 100644 --- a/build/content-helper/dashboard-widget.js +++ b/build/content-helper/dashboard-widget.js @@ -1 +1 @@ -!function(){"use strict";var e={20:function(e,t,r){var n=r(609),a=Symbol.for("react.element"),s=Symbol.for("react.fragment"),o=Object.prototype.hasOwnProperty,i=n.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED.ReactCurrentOwner,l={key:!0,ref:!0,__self:!0,__source:!0};function c(e,t,r){var n,s={},c=null,u=null;for(n in void 0!==r&&(c=""+r),void 0!==t.key&&(c=""+t.key),void 0!==t.ref&&(u=t.ref),t)o.call(t,n)&&!l.hasOwnProperty(n)&&(s[n]=t[n]);if(e&&e.defaultProps)for(n in t=e.defaultProps)void 0===s[n]&&(s[n]=t[n]);return{$$typeof:a,type:e,key:c,ref:u,props:s,_owner:i.current}}t.Fragment=s,t.jsx=c,t.jsxs=c},848:function(e,t,r){e.exports=r(20)},609:function(e){e.exports=window.React}},t={};function r(n){var a=t[n];if(void 0!==a)return a.exports;var s=t[n]={exports:{}};return e[n](s,s.exports,r),s.exports}r.n=function(e){var t=e&&e.__esModule?function(){return e.default}:function(){return e};return r.d(t,{a:t}),t},r.d=function(e,t){for(var n in t)r.o(t,n)&&!r.o(e,n)&&Object.defineProperty(e,n,{enumerable:!0,get:t[n]})},r.o=function(e,t){return Object.prototype.hasOwnProperty.call(e,t)},function(){var e,t,n,a=r(848),s=window.wp.element,o=window.wp.i18n,i=function(e){void 0===e&&(e=null);var t="";(null==e?void 0:e.children)&&(t=e.children);var r="content-helper-error-message";return(null==e?void 0:e.className)&&(r+=" "+e.className),(0,a.jsx)("div",{className:r,"data-testid":null==e?void 0:e.testId,dangerouslySetInnerHTML:{__html:t}})},l=function(e){var t;return void 0===e&&(e=null),(0,a.jsx)(i,{className:null==e?void 0:e.className,testId:"empty-credentials-message",children:null!==(t=window.wpParselyEmptyCredentialsMessage)&&void 0!==t?t:(0,o.__)("Please ensure that the Site ID and API Secret given in the plugin's settings are correct.","wp-parsely")})},c=function(){return c=Object.assign||function(e){for(var t,r=1,n=arguments.length;r0&&a[a.length-1])||6!==i[0]&&2!==i[0])){o=0;continue}if(3===i[0]&&(!a||i[1]>a[0]&&i[1]=1e4&&(clearInterval(s),r("Telemetry library not loaded"))}),100);else r("Telemetry not enabled")}))},e.prototype.trackEvent=function(t,r){var n;this.isLoaded?(0!==t.indexOf(e.TRACKS_PREFIX)&&(t=e.TRACKS_PREFIX+t),this.isEventNameValid(t)?(r=this.prepareProperties(r),null===(n=this._tkq)||void 0===n||n.push(["recordEvent",t,r])):console.error("Error tracking event: Invalid event name")):console.error("Error tracking event: Telemetry not loaded")},e.prototype.isTelemetryEnabled=function(){return this.isEnabled},e.prototype.isProprietyValid=function(t){return e.PROPERTY_REGEX.test(t)},e.prototype.isEventNameValid=function(t){return e.EVENT_NAME_REGEX.test(t)},e.prototype.prepareProperties=function(e){return(e=this.sanitizeProperties(e)).parsely_version=wpParselyTracksTelemetry.version,wpParselyTracksTelemetry.user&&(e._ut=wpParselyTracksTelemetry.user.type,e._ui=wpParselyTracksTelemetry.user.id),wpParselyTracksTelemetry.vipgo_env&&(e.vipgo_env=wpParselyTracksTelemetry.vipgo_env),this.sanitizeProperties(e)},e.prototype.sanitizeProperties=function(e){var t=this,r={};return Object.keys(e).forEach((function(n){t.isProprietyValid(n)&&(r[n]=e[n])})),r},e.TRACKS_PREFIX="wpparsely_",e.EVENT_NAME_REGEX=/^(([a-z0-9]+)_){2}([a-z0-9_]+)$/,e.PROPERTY_REGEX=/^[a-z_][a-z0-9_]*$/,e}(),h=(d.trackEvent,function(e){var t=e.defaultValue,r=e.items,n=e.onChange;return(0,a.jsx)("select",{onChange:n,value:t,children:r.map((function(e){return(0,a.jsx)("option",{value:e[0],children:e[1]},e[0])}))})}),f=window.wp.data,y=function(){return y=Object.assign||function(e){for(var t,r=1,n=arguments.length;rhere.',"wp-parsely"):s.code===j.ParselySuggestionsApiOpenAiError||s.code===j.ParselySuggestionsApiOpenAiUnavailable?s.message=(0,o.__)("The Parse.ly API returned an internal server error. Please retry with a different input, or try again later.","wp-parsely"):s.code===j.HttpRequestFailed&&s.message.includes("cURL error 28")?s.message=(0,o.__)("The Parse.ly API did not respond in a timely manner. Please try again later.","wp-parsely"):s.code===j.ParselySuggestionsApiSchemaError?s.message=(0,o.__)("The Parse.ly API returned a validation error. Please try again with different parameters.","wp-parsely"):s.code===j.ParselySuggestionsApiNoData?s.message=(0,o.__)("The Parse.ly API couldn't find any relevant data to fulfill the request. Please retry with a different input.","wp-parsely"):s.code===j.ParselySuggestionsApiOpenAiSchema?s.message=(0,o.__)("The Parse.ly API returned an incorrect response. Please try again later.","wp-parsely"):s.code===j.ParselySuggestionsApiAuthUnavailable&&(s.message=(0,o.__)("The Parse.ly API is currently unavailable. Please try again later.","wp-parsely")),s}return C(t,e),t.prototype.Message=function(e){return void 0===e&&(e=null),[j.PluginCredentialsNotSetMessageDetected,j.PluginSettingsSiteIdNotSet,j.PluginSettingsApiSecretNotSet].includes(this.code)?l(e):(this.code===j.FetchError&&(this.hint=this.Hint((0,o.__)("This error can sometimes be caused by ad-blockers or browser tracking protections. Please add this site to any applicable allow lists and try again.","wp-parsely"))),this.code!==j.ParselyApiForbidden&&this.code!==j.ParselySuggestionsApiNoAuthentication||(this.hint=this.Hint((0,o.__)("Please ensure that the Site ID and API Secret given in the plugin's settings are correct.","wp-parsely"))),this.code===j.HttpRequestFailed&&(this.hint=this.Hint((0,o.__)("The Parse.ly API cannot be reached. Please verify that you are online.","wp-parsely"))),(0,a.jsx)(i,{className:null==e?void 0:e.className,testId:"error",children:"

".concat(this.message,"

").concat(this.hint?this.hint:"")}))},t.prototype.Hint=function(e){return'

'.concat((0,o.__)("Hint:","wp-parsely")," ").concat(e,"

")},t.prototype.createErrorSnackbar=function(){//.test(this.message)||(0,f.dispatch)("core/notices").createNotice("error",this.message,{type:"snackbar"})},t}(Error),O=function(){function e(){this.abortControllers=new Map}return e.prototype.cancelRequest=function(e){if(e)(t=this.abortControllers.get(e))&&(t.abort(),this.abortControllers.delete(e));else{var t,r=Array.from(this.abortControllers.keys()).pop();r&&(t=this.abortControllers.get(r))&&(t.abort(),this.abortControllers.delete(r))}},e.prototype.cancelAll=function(){this.abortControllers.forEach((function(e){return e.abort()})),this.abortControllers.clear()},e.prototype.getOrCreateController=function(e){if(e&&this.abortControllers.has(e))return{abortController:this.abortControllers.get(e),abortId:e};var t=null!=e?e:"auto-"+Date.now(),r=new AbortController;return this.abortControllers.set(t,r),{abortController:r,abortId:t}},e.prototype.fetch=function(e,t){return r=this,n=void 0,s=function(){var r,n,a,s,i,l;return function(e,t){var r,n,a,s,o={label:0,sent:function(){if(1&a[0])throw a[1];return a[1]},trys:[],ops:[]};return s={next:i(0),throw:i(1),return:i(2)},"function"==typeof Symbol&&(s[Symbol.iterator]=function(){return this}),s;function i(i){return function(l){return function(i){if(r)throw new TypeError("Generator is already executing.");for(;s&&(s=0,i[0]&&(o=0)),o;)try{if(r=1,n&&(a=2&i[0]?n.return:i[0]?n.throw||((a=n.return)&&a.call(n),0):n.next)&&!(a=a.call(n,i[1])).done)return a;switch(n=0,a&&(i=[2&i[0],a.value]),i[0]){case 0:case 1:a=i;break;case 4:return o.label++,{value:i[1],done:!1};case 5:o.label++,n=i[1],i=[0];continue;case 7:i=o.ops.pop(),o.trys.pop();continue;default:if(!((a=(a=o.trys).length>0&&a[a.length-1])||6!==i[0]&&2!==i[0])){o=0;continue}if(3===i[0]&&(!a||i[1]>a[0]&&i[1]0&&a[a.length-1])||6!==i[0]&&2!==i[0])){o=0;continue}if(3===i[0]&&(!a||i[1]>a[0]&&i[1]=c){var u=t;(a=n/c)%1>1/i&&(u=a>10?1:2),u=parseFloat(a.toFixed(2))===parseFloat(a.toFixed(0))?0:u,s=a.toFixed(u),o=l}i=c})),s+r+o}function z(e){var t=e.metric,r=e.post,n=e.avgEngagedIcon,s=e.viewsIcon;return"views"===t?(0,a.jsxs)("span",{className:"parsely-post-metric-data",children:[(0,a.jsx)("span",{className:"screen-reader-text",children:(0,o.__)("Number of Views","wp-parsely")}),s,$(r.views.toString())]}):"avg_engaged"===t?(0,a.jsxs)("span",{className:"parsely-post-metric-data",children:[(0,a.jsx)("span",{className:"screen-reader-text",children:(0,o.__)("Average Time","wp-parsely")}),n,r.avgEngaged]}):(0,a.jsx)("span",{className:"parsely-post-metric-data",children:"-"})}function X(e){var t,r=e.metric,n=e.post;return(0,a.jsx)("li",{className:"parsely-top-post",children:(0,a.jsxs)("div",{className:"parsely-top-post-content",children:[(0,a.jsx)(B,{post:n}),(0,a.jsxs)("div",{className:"parsely-top-post-data",children:[(0,a.jsx)(z,{metric:r,post:n}),(0,a.jsx)(Z,{post:n}),(0,a.jsxs)("a",{className:"parsely-top-post-icon-link",href:n.url,target:"_blank",rel:"noreferrer",children:[(0,a.jsx)("span",{className:"screen-reader-text",children:(0,o.__)("View Post (opens in new tab)","wp-parsely")}),(0,a.jsx)(q,{})]}),0!==n.postId&&(0,a.jsxs)("a",{className:"parsely-top-post-icon-link",href:(t=n.postId,"/wp-admin/post.php?post=".concat(t,"&action=edit")),target:"_blank",rel:"noreferrer",children:[(0,a.jsx)("span",{className:"screen-reader-text",children:(0,o.__)("Edit Post (opens in new tab)","wp-parsely")}),(0,a.jsx)(G,{})]}),(0,a.jsxs)("div",{className:"parsely-top-post-metadata",children:[(0,a.jsxs)("span",{className:"parsely-top-post-date",children:[(0,a.jsx)("span",{className:"screen-reader-text",children:(0,o.__)("Date","wp-parsely")}),M(new Date(n.date))]}),(0,a.jsxs)("span",{className:"parsely-top-post-author",children:[(0,a.jsx)("span",{className:"screen-reader-text",children:(0,o.__)("Author","wp-parsely")}),n.author]})]})]})]})},n.id)}function B(e){var t=e.post;return t.thumbnailUrl?(0,a.jsxs)("div",{className:"parsely-top-post-thumbnail",children:[(0,a.jsx)("span",{className:"screen-reader-text",children:(0,o.__)("Thumbnail","wp-parsely")}),(0,a.jsx)("img",{src:t.thumbnailUrl,alt:(0,o.__)("Post thumbnail","wp-parsely")})]}):(0,a.jsx)("div",{className:"parsely-top-post-thumbnail",children:(0,a.jsx)("span",{className:"screen-reader-text",children:(0,o.__)("Post thumbnail not available","wp-parsely")})})}function Z(e){var t=e.post;return(0,a.jsxs)("a",{className:"parsely-top-post-title",href:t.dashUrl,target:"_blank",rel:"noreferrer",children:[(0,a.jsx)("span",{className:"screen-reader-text",children:(0,o.__)("View in Parse.ly (opens in new tab)","wp-parsely")}),t.title]})}var W=function(){return W=Object.assign||function(e){for(var t,r=1,n=arguments.length;r0&&a[a.length-1])||6!==i[0]&&2!==i[0])){o=0;continue}if(3===i[0]&&(!a||i[1]>a[0]&&i[1]0&&e.retryFetch?[4,new Promise((function(e){return setTimeout(e,500)}))]:[3,3];case 1:return r.sent(),[4,t(n-1)];case 2:return r.sent(),[3,4];case 3:f(!1),v(e),r.label=4;case 4:return[2]}}))}))})),[2]}))}))};return f(!0),t(1),function(){f(!1),m([]),v(void 0)}}),[i,S]);var j=function(e,t){d.trackEvent("dash_widget_filter_changed",W({filter:e},t))},N=(0,a.jsxs)("div",{className:"parsely-top-posts-filters",children:[(0,a.jsx)(h,{defaultValue:i.Period,items:Object.values(e).map((function(e){return[e,A(e)]})),onChange:function(t){x(t.target.value,e)&&(l({Period:t.target.value}),j("period",{period:t.target.value}),T(1))}}),(0,a.jsx)(h,{defaultValue:i.Metric,items:Object.values(t).map((function(e){return[e,E(e)]})),onChange:function(e){x(e.target.value,t)&&(l({Metric:e.target.value}),j("metric",{metric:e.target.value}),T(1))}})]}),C=(0,a.jsxs)("div",{className:"parsely-top-posts-navigation",children:[(0,a.jsx)("button",{className:"parsely-top-posts-navigation-prev",disabled:S<=1,"aria-label":(0,o.__)("Previous page","wp-parsely"),onClick:function(){T(S-1),d.trackEvent("dash_widget_navigation",{navigation:"previous",to_page:S-1})},children:(0,o.__)("<< Previous","wp-parsely")}),(0,o.sprintf)(/* translators: 1: Current page */ /* translators: 1: Current page */(0,o.__)("Page %1$d","wp-parsely"),S),(0,a.jsx)("button",{className:"parsely-top-posts-navigation-next",disabled:!u&&_.length<5,"aria-label":(0,o.__)("Next page","wp-parsely"),onClick:function(){T(S+1),d.trackEvent("dash_widget_navigation",{navigation:"next",to_page:S+1})},children:(0,o.__)("Next >>","wp-parsely")})]});if(g)return(0,a.jsxs)(a.Fragment,{children:[N,g.Message(),S>1&&C]});var k=(0,a.jsx)("div",{className:"parsely-spinner-wrapper",children:(0,a.jsx)(p.Spinner,{})});return(0,a.jsxs)(a.Fragment,{children:[N,u?k:(0,a.jsx)("ol",{className:"parsely-top-posts",style:{counterReset:"item "+5*(S-1)},children:_.map((function(e){return(0,a.jsx)(X,{metric:i.Metric,post:e},e.id)}))}),(_.length>=5||S>1)&&C]})}var J=function(r){var n;try{n=JSON.parse(r)}catch(r){return{Metric:t.Views,Period:e.Days7}}return x(null==n?void 0:n.Metric,t)||(n.Metric=t.Views),x(null==n?void 0:n.Period,e)||(n.Period=e.Days7),n};window.addEventListener("load",(function(){var e=document.querySelector("#wp-parsely-dashboard-widget > .inside");if(null!==e){var t=(0,a.jsx)(S,{endpoint:"dashboard-widget-settings",defaultSettings:J(window.wpParselyContentHelperSettings),children:(0,a.jsx)(u,{children:(0,a.jsx)(Q,{})})});s.createRoot?(0,s.createRoot)(e).render(t):(0,s.render)(t,e)}}),!1)}()}(); \ No newline at end of file +!function(){"use strict";var e={20:function(e,t,r){var n=r(609),a=Symbol.for("react.element"),s=Symbol.for("react.fragment"),o=Object.prototype.hasOwnProperty,i=n.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED.ReactCurrentOwner,l={key:!0,ref:!0,__self:!0,__source:!0};function c(e,t,r){var n,s={},c=null,u=null;for(n in void 0!==r&&(c=""+r),void 0!==t.key&&(c=""+t.key),void 0!==t.ref&&(u=t.ref),t)o.call(t,n)&&!l.hasOwnProperty(n)&&(s[n]=t[n]);if(e&&e.defaultProps)for(n in t=e.defaultProps)void 0===s[n]&&(s[n]=t[n]);return{$$typeof:a,type:e,key:c,ref:u,props:s,_owner:i.current}}t.Fragment=s,t.jsx=c,t.jsxs=c},848:function(e,t,r){e.exports=r(20)},609:function(e){e.exports=window.React}},t={};function r(n){var a=t[n];if(void 0!==a)return a.exports;var s=t[n]={exports:{}};return e[n](s,s.exports,r),s.exports}r.n=function(e){var t=e&&e.__esModule?function(){return e.default}:function(){return e};return r.d(t,{a:t}),t},r.d=function(e,t){for(var n in t)r.o(t,n)&&!r.o(e,n)&&Object.defineProperty(e,n,{enumerable:!0,get:t[n]})},r.o=function(e,t){return Object.prototype.hasOwnProperty.call(e,t)},function(){var e,t,n,a=r(848),s=window.wp.element,o=window.wp.i18n,i=function(e){void 0===e&&(e=null);var t="";(null==e?void 0:e.children)&&(t=e.children);var r="content-helper-error-message";return(null==e?void 0:e.className)&&(r+=" "+e.className),(0,a.jsx)("div",{className:r,"data-testid":null==e?void 0:e.testId,dangerouslySetInnerHTML:{__html:t}})},l=function(e){var t;return void 0===e&&(e=null),(0,a.jsx)(i,{className:null==e?void 0:e.className,testId:"empty-credentials-message",children:null!==(t=window.wpParselyEmptyCredentialsMessage)&&void 0!==t?t:(0,o.__)("Please ensure that the Site ID and API Secret given in the plugin's settings are correct.","wp-parsely")})},c=function(){return c=Object.assign||function(e){for(var t,r=1,n=arguments.length;r0&&a[a.length-1])||6!==i[0]&&2!==i[0])){o=0;continue}if(3===i[0]&&(!a||i[1]>a[0]&&i[1]=1e4&&(clearInterval(s),r("Telemetry library not loaded"))}),100);else r("Telemetry not enabled")}))},e.prototype.trackEvent=function(t,r){var n;this.isLoaded?(0!==t.indexOf(e.TRACKS_PREFIX)&&(t=e.TRACKS_PREFIX+t),this.isEventNameValid(t)?(r=this.prepareProperties(r),null===(n=this._tkq)||void 0===n||n.push(["recordEvent",t,r])):console.error("Error tracking event: Invalid event name")):console.error("Error tracking event: Telemetry not loaded")},e.prototype.isTelemetryEnabled=function(){return this.isEnabled},e.prototype.isProprietyValid=function(t){return e.PROPERTY_REGEX.test(t)},e.prototype.isEventNameValid=function(t){return e.EVENT_NAME_REGEX.test(t)},e.prototype.prepareProperties=function(e){return(e=this.sanitizeProperties(e)).parsely_version=wpParselyTracksTelemetry.version,wpParselyTracksTelemetry.user&&(e._ut=wpParselyTracksTelemetry.user.type,e._ui=wpParselyTracksTelemetry.user.id),wpParselyTracksTelemetry.vipgo_env&&(e.vipgo_env=wpParselyTracksTelemetry.vipgo_env),this.sanitizeProperties(e)},e.prototype.sanitizeProperties=function(e){var t=this,r={};return Object.keys(e).forEach((function(n){t.isProprietyValid(n)&&(r[n]=e[n])})),r},e.TRACKS_PREFIX="wpparsely_",e.EVENT_NAME_REGEX=/^(([a-z0-9]+)_){2}([a-z0-9_]+)$/,e.PROPERTY_REGEX=/^[a-z_][a-z0-9_]*$/,e}(),h=(d.trackEvent,function(e){var t=e.defaultValue,r=e.items,n=e.onChange;return(0,a.jsx)("select",{onChange:n,value:t,children:r.map((function(e){return(0,a.jsx)("option",{value:e[0],children:e[1]},e[0])}))})}),f=window.wp.data,y=function(){return y=Object.assign||function(e){for(var t,r=1,n=arguments.length;rhere.',"wp-parsely"):s.code===j.ParselySuggestionsApiOpenAiError||s.code===j.ParselySuggestionsApiOpenAiUnavailable?s.message=(0,o.__)("The Parse.ly API returned an internal server error. Please retry with a different input, or try again later.","wp-parsely"):s.code===j.HttpRequestFailed&&s.message.includes("cURL error 28")?s.message=(0,o.__)("The Parse.ly API did not respond in a timely manner. Please try again later.","wp-parsely"):s.code===j.ParselySuggestionsApiSchemaError?s.message=(0,o.__)("The Parse.ly API returned a validation error. Please try again with different parameters.","wp-parsely"):s.code===j.ParselySuggestionsApiNoData?s.message=(0,o.__)("The Parse.ly API couldn't find any relevant data to fulfill the request. Please retry with a different input.","wp-parsely"):s.code===j.ParselySuggestionsApiOpenAiSchema?s.message=(0,o.__)("The Parse.ly API returned an incorrect response. Please try again later.","wp-parsely"):s.code===j.ParselySuggestionsApiAuthUnavailable&&(s.message=(0,o.__)("The Parse.ly API is currently unavailable. Please try again later.","wp-parsely")),s}return C(t,e),t.prototype.Message=function(e){return void 0===e&&(e=null),[j.PluginCredentialsNotSetMessageDetected,j.PluginSettingsSiteIdNotSet,j.PluginSettingsApiSecretNotSet].includes(this.code)?l(e):(this.code===j.FetchError&&(this.hint=this.Hint((0,o.__)("This error can sometimes be caused by ad-blockers or browser tracking protections. Please add this site to any applicable allow lists and try again.","wp-parsely"))),this.code!==j.ParselyApiForbidden&&this.code!==j.ParselySuggestionsApiNoAuthentication||(this.hint=this.Hint((0,o.__)("Please ensure that the Site ID and API Secret given in the plugin's settings are correct.","wp-parsely"))),this.code===j.HttpRequestFailed&&(this.hint=this.Hint((0,o.__)("The Parse.ly API cannot be reached. Please verify that you are online.","wp-parsely"))),(0,a.jsx)(i,{className:null==e?void 0:e.className,testId:"error",children:"

".concat(this.message,"

").concat(this.hint?this.hint:"")}))},t.prototype.Hint=function(e){return'

'.concat((0,o.__)("Hint:","wp-parsely")," ").concat(e,"

")},t.prototype.createErrorSnackbar=function(){//.test(this.message)||(0,f.dispatch)("core/notices").createNotice("error",this.message,{type:"snackbar"})},t}(Error),O=function(){function e(){this.abortControllers=new Map}return e.prototype.cancelRequest=function(e){if(e)(t=this.abortControllers.get(e))&&(t.abort(),this.abortControllers.delete(e));else{var t,r=Array.from(this.abortControllers.keys()).pop();r&&(t=this.abortControllers.get(r))&&(t.abort(),this.abortControllers.delete(r))}},e.prototype.cancelAll=function(){this.abortControllers.forEach((function(e){return e.abort()})),this.abortControllers.clear()},e.prototype.getOrCreateController=function(e){if(e&&this.abortControllers.has(e))return{abortController:this.abortControllers.get(e),abortId:e};var t=null!=e?e:"auto-"+Date.now(),r=new AbortController;return this.abortControllers.set(t,r),{abortController:r,abortId:t}},e.prototype.fetch=function(e,t){return r=this,n=void 0,s=function(){var r,n,a,s,i,l;return function(e,t){var r,n,a,s,o={label:0,sent:function(){if(1&a[0])throw a[1];return a[1]},trys:[],ops:[]};return s={next:i(0),throw:i(1),return:i(2)},"function"==typeof Symbol&&(s[Symbol.iterator]=function(){return this}),s;function i(i){return function(l){return function(i){if(r)throw new TypeError("Generator is already executing.");for(;s&&(s=0,i[0]&&(o=0)),o;)try{if(r=1,n&&(a=2&i[0]?n.return:i[0]?n.throw||((a=n.return)&&a.call(n),0):n.next)&&!(a=a.call(n,i[1])).done)return a;switch(n=0,a&&(i=[2&i[0],a.value]),i[0]){case 0:case 1:a=i;break;case 4:return o.label++,{value:i[1],done:!1};case 5:o.label++,n=i[1],i=[0];continue;case 7:i=o.ops.pop(),o.trys.pop();continue;default:if(!((a=(a=o.trys).length>0&&a[a.length-1])||6!==i[0]&&2!==i[0])){o=0;continue}if(3===i[0]&&(!a||i[1]>a[0]&&i[1]0&&a[a.length-1])||6!==i[0]&&2!==i[0])){o=0;continue}if(3===i[0]&&(!a||i[1]>a[0]&&i[1]=c){var u=t;(a=n/c)%1>1/i&&(u=a>10?1:2),u=parseFloat(a.toFixed(2))===parseFloat(a.toFixed(0))?0:u,s=a.toFixed(u),o=l}i=c})),s+r+o}function z(e){var t=e.metric,r=e.post,n=e.avgEngagedIcon,s=e.viewsIcon;return"views"===t?(0,a.jsxs)("span",{className:"parsely-post-metric-data",children:[(0,a.jsx)("span",{className:"screen-reader-text",children:(0,o.__)("Number of Views","wp-parsely")}),s,$(r.views.toString())]}):"avg_engaged"===t?(0,a.jsxs)("span",{className:"parsely-post-metric-data",children:[(0,a.jsx)("span",{className:"screen-reader-text",children:(0,o.__)("Average Time","wp-parsely")}),n,r.avgEngaged]}):(0,a.jsx)("span",{className:"parsely-post-metric-data",children:"-"})}function X(e){var t,r=e.metric,n=e.post;return(0,a.jsx)("li",{className:"parsely-top-post",children:(0,a.jsxs)("div",{className:"parsely-top-post-content",children:[(0,a.jsx)(B,{post:n}),(0,a.jsxs)("div",{className:"parsely-top-post-data",children:[(0,a.jsx)(z,{metric:r,post:n}),(0,a.jsx)(Z,{post:n}),(0,a.jsxs)("a",{className:"parsely-top-post-icon-link",href:n.url,target:"_blank",rel:"noreferrer",children:[(0,a.jsx)("span",{className:"screen-reader-text",children:(0,o.__)("View Post (opens in new tab)","wp-parsely")}),(0,a.jsx)(q,{})]}),0!==n.postId&&(0,a.jsxs)("a",{className:"parsely-top-post-icon-link",href:(t=n.postId,"/wp-admin/post.php?post=".concat(t,"&action=edit")),target:"_blank",rel:"noreferrer",children:[(0,a.jsx)("span",{className:"screen-reader-text",children:(0,o.__)("Edit Post (opens in new tab)","wp-parsely")}),(0,a.jsx)(G,{})]}),(0,a.jsxs)("div",{className:"parsely-top-post-metadata",children:[(0,a.jsxs)("span",{className:"parsely-top-post-date",children:[(0,a.jsx)("span",{className:"screen-reader-text",children:(0,o.__)("Date","wp-parsely")}),M(new Date(n.date))]}),(0,a.jsxs)("span",{className:"parsely-top-post-author",children:[(0,a.jsx)("span",{className:"screen-reader-text",children:(0,o.__)("Author","wp-parsely")}),n.author]})]})]})]})},n.id)}function B(e){var t=e.post;return t.thumbnailUrl?(0,a.jsxs)("div",{className:"parsely-top-post-thumbnail",children:[(0,a.jsx)("span",{className:"screen-reader-text",children:(0,o.__)("Thumbnail","wp-parsely")}),(0,a.jsx)("img",{src:t.thumbnailUrl,alt:(0,o.__)("Post thumbnail","wp-parsely")})]}):(0,a.jsx)("div",{className:"parsely-top-post-thumbnail",children:(0,a.jsx)("span",{className:"screen-reader-text",children:(0,o.__)("Post thumbnail not available","wp-parsely")})})}function Z(e){var t=e.post;return(0,a.jsxs)("a",{className:"parsely-top-post-title",href:t.dashUrl,target:"_blank",rel:"noreferrer",children:[(0,a.jsx)("span",{className:"screen-reader-text",children:(0,o.__)("View in Parse.ly (opens in new tab)","wp-parsely")}),t.title]})}var W=function(){return W=Object.assign||function(e){for(var t,r=1,n=arguments.length;r0&&a[a.length-1])||6!==i[0]&&2!==i[0])){o=0;continue}if(3===i[0]&&(!a||i[1]>a[0]&&i[1]0&&e.retryFetch?[4,new Promise((function(e){return setTimeout(e,500)}))]:[3,3];case 1:return r.sent(),[4,t(n-1)];case 2:return r.sent(),[3,4];case 3:f(!1),v(e),r.label=4;case 4:return[2]}}))}))})),[2]}))}))};return f(!0),t(1),function(){f(!1),m([]),v(void 0)}}),[i,S]);var j=function(e,t){d.trackEvent("dash_widget_filter_changed",W({filter:e},t))},N=(0,a.jsxs)("div",{className:"parsely-top-posts-filters",children:[(0,a.jsx)(h,{defaultValue:i.Period,items:Object.values(e).map((function(e){return[e,A(e)]})),onChange:function(t){x(t.target.value,e)&&(l({Period:t.target.value}),j("period",{period:t.target.value}),T(1))}}),(0,a.jsx)(h,{defaultValue:i.Metric,items:Object.values(t).map((function(e){return[e,E(e)]})),onChange:function(e){x(e.target.value,t)&&(l({Metric:e.target.value}),j("metric",{metric:e.target.value}),T(1))}})]}),C=(0,a.jsxs)("div",{className:"parsely-top-posts-navigation",children:[(0,a.jsx)("button",{className:"parsely-top-posts-navigation-prev",disabled:S<=1,"aria-label":(0,o.__)("Previous page","wp-parsely"),onClick:function(){T(S-1),d.trackEvent("dash_widget_navigation",{navigation:"previous",to_page:S-1})},children:(0,o.__)("<< Previous","wp-parsely")}),(0,o.sprintf)(/* translators: 1: Current page */ /* translators: 1: Current page */(0,o.__)("Page %1$d","wp-parsely"),S),(0,a.jsx)("button",{className:"parsely-top-posts-navigation-next",disabled:!u&&_.length<5,"aria-label":(0,o.__)("Next page","wp-parsely"),onClick:function(){T(S+1),d.trackEvent("dash_widget_navigation",{navigation:"next",to_page:S+1})},children:(0,o.__)("Next >>","wp-parsely")})]});if(g)return(0,a.jsxs)(a.Fragment,{children:[N,g.Message(),S>1&&C]});var k=(0,a.jsx)("div",{className:"parsely-spinner-wrapper",children:(0,a.jsx)(p.Spinner,{})});return(0,a.jsxs)(a.Fragment,{children:[N,u?k:(0,a.jsx)("ol",{className:"parsely-top-posts",style:{counterReset:"item "+5*(S-1)},children:_.map((function(e){return(0,a.jsx)(X,{metric:i.Metric,post:e},e.id)}))}),(_.length>=5||S>1)&&C]})}var J=function(r){var n;try{n=JSON.parse(r)}catch(r){return{Metric:t.Views,Period:e.Days7}}return x(null==n?void 0:n.Metric,t)||(n.Metric=t.Views),x(null==n?void 0:n.Period,e)||(n.Period=e.Days7),n};window.addEventListener("load",(function(){var e=document.querySelector("#wp-parsely-dashboard-widget > .inside");if(null!==e){var t=(0,a.jsx)(S,{endpoint:"dashboard-widget-settings",defaultSettings:J(window.wpParselyContentHelperSettings),children:(0,a.jsx)(u,{children:(0,a.jsx)(Q,{})})});s.createRoot?(0,s.createRoot)(e).render(t):(0,s.render)(t,e)}}),!1)}()}(); \ No newline at end of file diff --git a/build/content-helper/editor-sidebar.asset.php b/build/content-helper/editor-sidebar.asset.php index 0a96a20139..a791b1c322 100644 --- a/build/content-helper/editor-sidebar.asset.php +++ b/build/content-helper/editor-sidebar.asset.php @@ -1 +1 @@ - array('react', 'wp-api-fetch', 'wp-block-editor', 'wp-blocks', 'wp-components', 'wp-compose', 'wp-core-data', 'wp-data', 'wp-dom-ready', 'wp-editor', 'wp-element', 'wp-hooks', 'wp-i18n', 'wp-plugins', 'wp-primitives', 'wp-url'), 'version' => 'ceb704108c34274732ed'); + array('react', 'wp-api-fetch', 'wp-block-editor', 'wp-blocks', 'wp-components', 'wp-compose', 'wp-core-data', 'wp-data', 'wp-dom-ready', 'wp-editor', 'wp-element', 'wp-hooks', 'wp-i18n', 'wp-plugins', 'wp-primitives', 'wp-url'), 'version' => 'eaad1ca5764f3fbdb9e6'); diff --git a/build/content-helper/editor-sidebar.js b/build/content-helper/editor-sidebar.js index 3036042520..697b66067d 100644 --- a/build/content-helper/editor-sidebar.js +++ b/build/content-helper/editor-sidebar.js @@ -9,11 +9,11 @@ /* translators: %s: percentage value */,{ /* translators: %s: percentage value */ text:"".concat(c(t)," - ").concat((0,_.sprintf)((0,_.__)("%s%%","wp-parsely"),n.viewsPercentage)),delay:150,children:(0,p.jsx)("div",{"aria-label":r,className:"bar-fill "+t,style:{width:n.viewsPercentage+"%"}})},t)}))}),(0,p.jsx)("div",{className:"percentage-bar-labels",children:Object.entries(t.referrers.types).map((function(e){var t=e[0],n=e[1];return(0,p.jsxs)("div",{className:"single-label "+t,children:[(0,p.jsx)("div",{className:"label-color "+t}),(0,p.jsx)("div",{className:"label-text",children:c(t)}),(0,p.jsx)("div",{className:"label-value",children:Lt(n.views)})]},t)}))})]})]})},Nt=(0,p.jsx)(x.SVG,{viewBox:"0 0 24 24",xmlns:"http://www.w3.org/2000/svg",children:(0,p.jsx)(x.Path,{d:"M3.99961 13C4.67043 13.3354 4.6703 13.3357 4.67017 13.3359L4.67298 13.3305C4.67621 13.3242 4.68184 13.3135 4.68988 13.2985C4.70595 13.2686 4.7316 13.2218 4.76695 13.1608C4.8377 13.0385 4.94692 12.8592 5.09541 12.6419C5.39312 12.2062 5.84436 11.624 6.45435 11.0431C7.67308 9.88241 9.49719 8.75 11.9996 8.75C14.502 8.75 16.3261 9.88241 17.5449 11.0431C18.1549 11.624 18.6061 12.2062 18.9038 12.6419C19.0523 12.8592 19.1615 13.0385 19.2323 13.1608C19.2676 13.2218 19.2933 13.2686 19.3093 13.2985C19.3174 13.3135 19.323 13.3242 19.3262 13.3305L19.3291 13.3359C19.3289 13.3357 19.3288 13.3354 19.9996 13C20.6704 12.6646 20.6703 12.6643 20.6701 12.664L20.6697 12.6632L20.6688 12.6614L20.6662 12.6563L20.6583 12.6408C20.6517 12.6282 20.6427 12.6108 20.631 12.5892C20.6078 12.5459 20.5744 12.4852 20.5306 12.4096C20.4432 12.2584 20.3141 12.0471 20.1423 11.7956C19.7994 11.2938 19.2819 10.626 18.5794 9.9569C17.1731 8.61759 14.9972 7.25 11.9996 7.25C9.00203 7.25 6.82614 8.61759 5.41987 9.9569C4.71736 10.626 4.19984 11.2938 3.85694 11.7956C3.68511 12.0471 3.55605 12.2584 3.4686 12.4096C3.42484 12.4852 3.39142 12.5459 3.36818 12.5892C3.35656 12.6108 3.34748 12.6282 3.34092 12.6408L3.33297 12.6563L3.33041 12.6614L3.32948 12.6632L3.32911 12.664C3.32894 12.6643 3.32879 12.6646 3.99961 13ZM11.9996 16C13.9326 16 15.4996 14.433 15.4996 12.5C15.4996 10.567 13.9326 9 11.9996 9C10.0666 9 8.49961 10.567 8.49961 12.5C8.49961 14.433 10.0666 16 11.9996 16Z"})}),Ct=(0,p.jsx)(x.SVG,{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 24 24",children:(0,p.jsx)(x.Path,{d:"M15.5 9.5a1 1 0 100-2 1 1 0 000 2zm0 1.5a2.5 2.5 0 100-5 2.5 2.5 0 000 5zm-2.25 6v-2a2.75 2.75 0 00-2.75-2.75h-4A2.75 2.75 0 003.75 15v2h1.5v-2c0-.69.56-1.25 1.25-1.25h4c.69 0 1.25.56 1.25 1.25v2h1.5zm7-2v2h-1.5v-2c0-.69-.56-1.25-1.25-1.25H15v-1.5h2.5A2.75 2.75 0 0120.25 15zM9.5 8.5a1 1 0 11-2 0 1 1 0 012 0zm1.5 0a2.5 2.5 0 11-5 0 2.5 2.5 0 015 0z",fillRule:"evenodd"})}),At=(0,p.jsx)(x.SVG,{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 24 24",children:(0,p.jsx)(x.Path,{d:"M12 4V2.2L9 4.8l3 2.5V5.5c3.6 0 6.5 2.9 6.5 6.5 0 2.9-1.9 5.3-4.5 6.2v.2l-.1-.2c-.4.1-.7.2-1.1.2l.2 1.5c.3 0 .6-.1 1-.2 3.5-.9 6-4 6-7.7 0-4.4-3.6-8-8-8zm-7.9 7l1.5.2c.1-1.2.5-2.3 1.2-3.2l-1.1-.9C4.8 8.2 4.3 9.6 4.1 11zm1.5 1.8l-1.5.2c.1.7.3 1.4.5 2 .3.7.6 1.3 1 1.8l1.2-.8c-.3-.5-.6-1-.8-1.5s-.4-1.1-.4-1.7zm1.5 5.5c1.1.9 2.4 1.4 3.8 1.6l.2-1.5c-1.1-.1-2.2-.5-3.1-1.2l-.9 1.1z"})}),Ot=(0,p.jsx)(x.SVG,{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 24 24",children:(0,p.jsx)(x.Path,{d:"M11 13h2v-2h-2v2zm-6 0h2v-2H5v2zm12-2v2h2v-2h-2z"})}),Rt=function(){return Rt=Object.assign||function(e){for(var t,n=1,r=arguments.length;n0&&i[i.length-1])||6!==a[0]&&2!==a[0])){o=0;continue}if(3===a[0]&&(!i||a[1]>i[0]&&a[1]1?[2,Promise.reject(new ie((0,_.sprintf)(/* translators: URL of the published post */ /* translators: URL of the published post */ -(0,_.__)("Multiple results were returned for the post %s by the Parse.ly API.","wp-parsely"),t),$.ParselyApiReturnedTooManyResults))]:[2,n[0]]}}))}))},t.prototype.fetchReferrerDataFromWpEndpoint=function(e,t,n){return Zt(this,void 0,void 0,(function(){return $t(this,(function(r){switch(r.label){case 0:return[4,this.fetch({path:(0,Oe.addQueryArgs)("/wp-parsely/v1/referrers/post/detail",qt(qt({},zt(e)),{itm_source:this.itmSource,total_views:n,url:t}))})];case 1:return[2,r.sent()]}}))}))},t}(Re),Kt=function(){return Kt=Object.assign||function(e){for(var t,n=1,r=arguments.length;n0&&i[i.length-1])||6!==a[0]&&2!==a[0])){o=0;continue}if(3===a[0]&&(!i||a[1]>i[0]&&a[1]0&&e.retryFetch?[4,new Promise((function(e){return setTimeout(e,500)}))]:[3,3];case 1:return t.sent(),[4,n(r-1)];case 2:return t.sent(),[3,4];case 3:a(e),i(!1),t.label=4;case 4:return[2]}}))}))})),[2]}))}))};return i(!0),n(1),function(){a(void 0)}}),[t]),(0,p.jsxs)("div",{className:"wp-parsely-performance-panel",children:[(0,p.jsx)(Tt,{title:(0,_.__)("Performance Stats","wp-parsely"),icon:jt,dropdownChildren:function(e){var t=e.onClose;return(0,p.jsx)(tn,{onClose:t})},children:(0,p.jsx)("div",{className:"panel-settings",children:(0,p.jsx)(f.SelectControl,{size:"__unstable-large",value:h.PerformanceStats.Period,prefix:(0,p.jsx)(f.__experimentalInputControlPrefixWrapper,{children:(0,_.__)("Period: ","wp-parsely")}),onChange:function(e){D(e,y)&&(v({PerformanceStats:Kt(Kt({},h.PerformanceStats),{Period:e})}),P.trackEvent("editor_sidebar_performance_period_changed",{period:e}))},children:Object.values(y).map((function(e){return(0,p.jsx)("option",{value:e,children:F(e)},e)}))})})}),o?o.Message():(0,p.jsxs)(p.Fragment,{children:[en(h,"overview")&&(0,p.jsx)(Gt,{data:c,isLoading:r}),en(h,"categories")&&(0,p.jsx)(Et,{data:c,isLoading:r}),en(h,"referrers")&&(0,p.jsx)(Ht,{data:c,isLoading:r})]}),window.wpParselyPostUrl&&(0,p.jsx)(f.Button,{className:"wp-parsely-view-post",variant:"primary",onClick:function(){P.trackEvent("editor_sidebar_view_post_pressed")},href:window.wpParselyPostUrl,rel:"noopener",target:"_blank",children:(0,_.__)("View this in Parse.ly","wp-parsely")})]})},rn=function(e){var t=e.period;return(0,p.jsx)(f.Panel,{children:(0,p.jsx)(J,{children:(0,p.jsx)(nn,{period:t})})})},sn=function(e,t){var n={};for(var r in e)Object.prototype.hasOwnProperty.call(e,r)&&t.indexOf(r)<0&&(n[r]=e[r]);if(null!=e&&"function"==typeof Object.getOwnPropertySymbols){var i=0;for(r=Object.getOwnPropertySymbols(e);i=1&&(0,p.jsx)(f.__experimentalToggleGroupControlOption,{value:w.Tag,label:(0,_.__)("Tag","wp-parsely")}),r.categories.length>=1&&(0,p.jsx)(f.__experimentalToggleGroupControlOption,{value:w.Section,label:(0,_.__)("Section","wp-parsely")}),r.authors.length>=1&&(0,p.jsx)(f.__experimentalToggleGroupControlOption,{value:w.Author,label:(0,_.__)("Author","wp-parsely")})]})})},an=function(e){var t=e.filter,n=e.label,r=e.postData,i=sn(e,["filter","label","postData"]);return(0,p.jsx)("div",{className:"related-posts-filter-values",children:(0,p.jsx)(f.ComboboxControl,{__next40pxDefaultSize:!0,allowReset:!0,label:n,onChange:function(e){return i.onFilterValueChange(e)},options:w.Tag===t.type?r.tags.map((function(e){return{value:e,label:e}})):w.Section===t.type?r.categories.map((function(e){return{value:e,label:e}})):w.Author===t.type?r.authors.map((function(e){return{value:e,label:e}})):[],value:t.value})})},ln=function(e){var t=e.filter,n=e.postData,r=e.label,i=sn(e,["filter","postData","label"]),s=function(){return n.authors.length>0&&n.categories.length>0||n.authors.length>0&&n.tags.length>0||n.tags.length>0&&n.categories.length>0},o=function(){return w.Tag===t.type&&n.tags.length>1||w.Section===t.type&&n.categories.length>1||w.Author===t.type&&n.authors.length>1};return s()||o()?(0,p.jsxs)("div",{className:"related-posts-filter-settings",children:[s()&&(0,p.jsx)(on,{filter:t,label:r,onFilterTypeChange:i.onFilterTypeChange,postData:n}),o()&&(0,p.jsx)(an,{filter:t,label:s()?void 0:r,onFilterValueChange:i.onFilterValueChange,postData:n})]}):null},cn=(0,p.jsx)(x.SVG,{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 24 24",children:(0,p.jsx)(x.Path,{d:"M10 17.389H8.444A5.194 5.194 0 1 1 8.444 7H10v1.5H8.444a3.694 3.694 0 0 0 0 7.389H10v1.5ZM14 7h1.556a5.194 5.194 0 0 1 0 10.39H14v-1.5h1.556a3.694 3.694 0 0 0 0-7.39H14V7Zm-4.5 6h5v-1.5h-5V13Z"})}),un=(0,p.jsx)(x.SVG,{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 24 24",children:(0,p.jsx)(x.Path,{fillRule:"evenodd",clipRule:"evenodd",d:"M5.625 5.5h9.75c.069 0 .125.056.125.125v9.75a.125.125 0 0 1-.125.125h-9.75a.125.125 0 0 1-.125-.125v-9.75c0-.069.056-.125.125-.125ZM4 5.625C4 4.728 4.728 4 5.625 4h9.75C16.273 4 17 4.728 17 5.625v9.75c0 .898-.727 1.625-1.625 1.625h-9.75A1.625 1.625 0 0 1 4 15.375v-9.75Zm14.5 11.656v-9H20v9C20 18.8 18.77 20 17.251 20H6.25v-1.5h11.001c.69 0 1.249-.528 1.249-1.219Z"})}),dn=function(){return(0,p.jsx)(f.SVG,{xmlns:"http://www.w3.org/2000/svg",width:"1",height:"40",viewBox:"0 0 1 40",fill:"none",children:(0,p.jsx)(f.Rect,{width:"1",height:"40",fill:"#cccccc"})})};function pn(e){var t=e.metric,n=e.post,r=e.avgEngagedIcon,i=e.viewsIcon;return"views"===t?(0,p.jsxs)("span",{className:"parsely-post-metric-data",children:[(0,p.jsx)("span",{className:"screen-reader-text",children:(0,_.__)("Number of Views","wp-parsely")}),i,Lt(n.views.toString())]}):"avg_engaged"===t?(0,p.jsxs)("span",{className:"parsely-post-metric-data",children:[(0,p.jsx)("span",{className:"screen-reader-text",children:(0,_.__)("Average Time","wp-parsely")}),r,n.avgEngaged]}):(0,p.jsx)("span",{className:"parsely-post-metric-data",children:"-"})}var fn,hn=function(e){var t,n,r=e.metric,i=e.post,s=e.postContent,o=(0,h.useDispatch)("core/notices").createNotice,a=s&&(t=s,n=q(i.rawUrl),new RegExp("]*href=[\"'](http://|https://)?.*".concat(n,".*[\"'][^>]*>"),"i").test(t));return(0,p.jsxs)("div",{className:"related-post-single","data-testid":"related-post-single",children:[(0,p.jsx)("div",{className:"related-post-title",children:(0,p.jsxs)("a",{href:i.url,target:"_blank",rel:"noreferrer",children:[(0,p.jsx)("span",{className:"screen-reader-text",children:(0,_.__)("View on website (opens new tab)","wp-parsely")}),i.title]})}),(0,p.jsx)("div",{className:"related-post-actions",children:(0,p.jsxs)("div",{className:"related-post-info",children:[(0,p.jsxs)("div",{children:[(0,p.jsx)("div",{className:"related-post-metric",children:(0,p.jsx)(pn,{metric:r,post:i,viewsIcon:(0,p.jsx)(X,{icon:Nt}),avgEngagedIcon:(0,p.jsx)(f.Dashicon,{icon:"clock",size:24})})}),a&&(0,p.jsx)("div",{className:"related-post-linked",children:(0,p.jsx)(f.Tooltip,{text:(0,_.__)("This post is linked in the content","wp-parsely"),children:(0,p.jsx)(X,{icon:cn,size:24})})})]}),(0,p.jsx)(dn,{}),(0,p.jsxs)("div",{children:[(0,p.jsx)(f.Button,{icon:un,iconSize:24,onClick:function(){navigator.clipboard.writeText(i.rawUrl).then((function(){o("success",(0,_.__)("URL copied to clipboard","wp-parsely"),{type:"snackbar"})}))},label:(0,_.__)("Copy URL to clipboard","wp-parsely")}),(0,p.jsx)(f.Button,{icon:(0,p.jsx)(T,{}),iconSize:18,href:i.dashUrl,target:"_blank",label:(0,_.__)("View in Parse.ly","wp-parsely")})]})]})})]})},vn=window.wp.coreData,gn=function(){var e=function(t,n){return e=Object.setPrototypeOf||{__proto__:[]}instanceof Array&&function(e,t){e.__proto__=t}||function(e,t){for(var n in t)Object.prototype.hasOwnProperty.call(t,n)&&(e[n]=t[n])},e(t,n)};return function(t,n){if("function"!=typeof n&&null!==n)throw new TypeError("Class extends value "+String(n)+" is not a constructor or null");function __(){this.constructor=t}e(t,n),t.prototype=null===n?Object.create(n):(__.prototype=n.prototype,new __)}}(),yn=function(){return yn=Object.assign||function(e){for(var t,n=1,r=arguments.length;n0&&i[i.length-1])||6!==a[0]&&2!==a[0])){o=0;continue}if(3===a[0]&&(!i||a[1]>i[0]&&a[1]0&&i[i.length-1])||6!==a[0]&&2!==a[0])){o=0;continue}if(3===a[0]&&(!i||a[1]>i[0]&&a[1]1?[2,Promise.reject(new ie((0,_.sprintf)(/* translators: URL of the published post */ /* translators: URL of the published post */ +(0,_.__)("Multiple results were returned for the post %d by the Parse.ly API.","wp-parsely"),t),$.ParselyApiReturnedTooManyResults))]:[2,n[0]]}}))}))},t.prototype.fetchReferrerDataFromWpEndpoint=function(e,t,n){return Zt(this,void 0,void 0,(function(){return $t(this,(function(r){switch(r.label){case 0:return[4,this.fetch({path:(0,Oe.addQueryArgs)("/wp-parsely/v2/stats/post/".concat(t,"/referrers"),qt(qt({},zt(e)),{itm_source:this.itmSource,total_views:n}))})];case 1:return[2,r.sent()]}}))}))},t}(Re),Kt=function(){return Kt=Object.assign||function(e){for(var t,n=1,r=arguments.length;n0&&i[i.length-1])||6!==a[0]&&2!==a[0])){o=0;continue}if(3===a[0]&&(!i||a[1]>i[0]&&a[1]0&&e.retryFetch?[4,new Promise((function(e){return setTimeout(e,500)}))]:[3,3];case 1:return t.sent(),[4,n(r-1)];case 2:return t.sent(),[3,4];case 3:a(e),i(!1),t.label=4;case 4:return[2]}}))}))})),[2]}))}))};return i(!0),n(1),function(){a(void 0)}}),[t]),(0,p.jsxs)("div",{className:"wp-parsely-performance-panel",children:[(0,p.jsx)(Tt,{title:(0,_.__)("Performance Stats","wp-parsely"),icon:jt,dropdownChildren:function(e){var t=e.onClose;return(0,p.jsx)(tn,{onClose:t})},children:(0,p.jsx)("div",{className:"panel-settings",children:(0,p.jsx)(f.SelectControl,{size:"__unstable-large",value:h.PerformanceStats.Period,prefix:(0,p.jsx)(f.__experimentalInputControlPrefixWrapper,{children:(0,_.__)("Period: ","wp-parsely")}),onChange:function(e){D(e,y)&&(v({PerformanceStats:Kt(Kt({},h.PerformanceStats),{Period:e})}),P.trackEvent("editor_sidebar_performance_period_changed",{period:e}))},children:Object.values(y).map((function(e){return(0,p.jsx)("option",{value:e,children:F(e)},e)}))})})}),o?o.Message():(0,p.jsxs)(p.Fragment,{children:[en(h,"overview")&&(0,p.jsx)(Gt,{data:c,isLoading:r}),en(h,"categories")&&(0,p.jsx)(Et,{data:c,isLoading:r}),en(h,"referrers")&&(0,p.jsx)(Ht,{data:c,isLoading:r})]}),window.wpParselyPostUrl&&(0,p.jsx)(f.Button,{className:"wp-parsely-view-post",variant:"primary",onClick:function(){P.trackEvent("editor_sidebar_view_post_pressed")},href:window.wpParselyPostUrl,rel:"noopener",target:"_blank",children:(0,_.__)("View this in Parse.ly","wp-parsely")})]})},rn=function(e){var t=e.period;return(0,p.jsx)(f.Panel,{children:(0,p.jsx)(J,{children:(0,p.jsx)(nn,{period:t})})})},sn=function(e,t){var n={};for(var r in e)Object.prototype.hasOwnProperty.call(e,r)&&t.indexOf(r)<0&&(n[r]=e[r]);if(null!=e&&"function"==typeof Object.getOwnPropertySymbols){var i=0;for(r=Object.getOwnPropertySymbols(e);i=1&&(0,p.jsx)(f.__experimentalToggleGroupControlOption,{value:w.Tag,label:(0,_.__)("Tag","wp-parsely")}),r.categories.length>=1&&(0,p.jsx)(f.__experimentalToggleGroupControlOption,{value:w.Section,label:(0,_.__)("Section","wp-parsely")}),r.authors.length>=1&&(0,p.jsx)(f.__experimentalToggleGroupControlOption,{value:w.Author,label:(0,_.__)("Author","wp-parsely")})]})})},an=function(e){var t=e.filter,n=e.label,r=e.postData,i=sn(e,["filter","label","postData"]);return(0,p.jsx)("div",{className:"related-posts-filter-values",children:(0,p.jsx)(f.ComboboxControl,{__next40pxDefaultSize:!0,allowReset:!0,label:n,onChange:function(e){return i.onFilterValueChange(e)},options:w.Tag===t.type?r.tags.map((function(e){return{value:e,label:e}})):w.Section===t.type?r.categories.map((function(e){return{value:e,label:e}})):w.Author===t.type?r.authors.map((function(e){return{value:e,label:e}})):[],value:t.value})})},ln=function(e){var t=e.filter,n=e.postData,r=e.label,i=sn(e,["filter","postData","label"]),s=function(){return n.authors.length>0&&n.categories.length>0||n.authors.length>0&&n.tags.length>0||n.tags.length>0&&n.categories.length>0},o=function(){return w.Tag===t.type&&n.tags.length>1||w.Section===t.type&&n.categories.length>1||w.Author===t.type&&n.authors.length>1};return s()||o()?(0,p.jsxs)("div",{className:"related-posts-filter-settings",children:[s()&&(0,p.jsx)(on,{filter:t,label:r,onFilterTypeChange:i.onFilterTypeChange,postData:n}),o()&&(0,p.jsx)(an,{filter:t,label:s()?void 0:r,onFilterValueChange:i.onFilterValueChange,postData:n})]}):null},cn=(0,p.jsx)(x.SVG,{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 24 24",children:(0,p.jsx)(x.Path,{d:"M10 17.389H8.444A5.194 5.194 0 1 1 8.444 7H10v1.5H8.444a3.694 3.694 0 0 0 0 7.389H10v1.5ZM14 7h1.556a5.194 5.194 0 0 1 0 10.39H14v-1.5h1.556a3.694 3.694 0 0 0 0-7.39H14V7Zm-4.5 6h5v-1.5h-5V13Z"})}),un=(0,p.jsx)(x.SVG,{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 24 24",children:(0,p.jsx)(x.Path,{fillRule:"evenodd",clipRule:"evenodd",d:"M5.625 5.5h9.75c.069 0 .125.056.125.125v9.75a.125.125 0 0 1-.125.125h-9.75a.125.125 0 0 1-.125-.125v-9.75c0-.069.056-.125.125-.125ZM4 5.625C4 4.728 4.728 4 5.625 4h9.75C16.273 4 17 4.728 17 5.625v9.75c0 .898-.727 1.625-1.625 1.625h-9.75A1.625 1.625 0 0 1 4 15.375v-9.75Zm14.5 11.656v-9H20v9C20 18.8 18.77 20 17.251 20H6.25v-1.5h11.001c.69 0 1.249-.528 1.249-1.219Z"})}),dn=function(){return(0,p.jsx)(f.SVG,{xmlns:"http://www.w3.org/2000/svg",width:"1",height:"40",viewBox:"0 0 1 40",fill:"none",children:(0,p.jsx)(f.Rect,{width:"1",height:"40",fill:"#cccccc"})})};function pn(e){var t=e.metric,n=e.post,r=e.avgEngagedIcon,i=e.viewsIcon;return"views"===t?(0,p.jsxs)("span",{className:"parsely-post-metric-data",children:[(0,p.jsx)("span",{className:"screen-reader-text",children:(0,_.__)("Number of Views","wp-parsely")}),i,Lt(n.views.toString())]}):"avg_engaged"===t?(0,p.jsxs)("span",{className:"parsely-post-metric-data",children:[(0,p.jsx)("span",{className:"screen-reader-text",children:(0,_.__)("Average Time","wp-parsely")}),r,n.avgEngaged]}):(0,p.jsx)("span",{className:"parsely-post-metric-data",children:"-"})}var fn,hn=function(e){var t,n,r=e.metric,i=e.post,s=e.postContent,o=(0,h.useDispatch)("core/notices").createNotice,a=s&&(t=s,n=q(i.rawUrl),new RegExp("]*href=[\"'](http://|https://)?.*".concat(n,".*[\"'][^>]*>"),"i").test(t));return(0,p.jsxs)("div",{className:"related-post-single","data-testid":"related-post-single",children:[(0,p.jsx)("div",{className:"related-post-title",children:(0,p.jsxs)("a",{href:i.url,target:"_blank",rel:"noreferrer",children:[(0,p.jsx)("span",{className:"screen-reader-text",children:(0,_.__)("View on website (opens new tab)","wp-parsely")}),i.title]})}),(0,p.jsx)("div",{className:"related-post-actions",children:(0,p.jsxs)("div",{className:"related-post-info",children:[(0,p.jsxs)("div",{children:[(0,p.jsx)("div",{className:"related-post-metric",children:(0,p.jsx)(pn,{metric:r,post:i,viewsIcon:(0,p.jsx)(X,{icon:Nt}),avgEngagedIcon:(0,p.jsx)(f.Dashicon,{icon:"clock",size:24})})}),a&&(0,p.jsx)("div",{className:"related-post-linked",children:(0,p.jsx)(f.Tooltip,{text:(0,_.__)("This post is linked in the content","wp-parsely"),children:(0,p.jsx)(X,{icon:cn,size:24})})})]}),(0,p.jsx)(dn,{}),(0,p.jsxs)("div",{children:[(0,p.jsx)(f.Button,{icon:un,iconSize:24,onClick:function(){navigator.clipboard.writeText(i.rawUrl).then((function(){o("success",(0,_.__)("URL copied to clipboard","wp-parsely"),{type:"snackbar"})}))},label:(0,_.__)("Copy URL to clipboard","wp-parsely")}),(0,p.jsx)(f.Button,{icon:(0,p.jsx)(T,{}),iconSize:18,href:i.dashUrl,target:"_blank",label:(0,_.__)("View in Parse.ly","wp-parsely")})]})]})})]})},vn=window.wp.coreData,gn=function(){var e=function(t,n){return e=Object.setPrototypeOf||{__proto__:[]}instanceof Array&&function(e,t){e.__proto__=t}||function(e,t){for(var n in t)Object.prototype.hasOwnProperty.call(t,n)&&(e[n]=t[n])},e(t,n)};return function(t,n){if("function"!=typeof n&&null!==n)throw new TypeError("Class extends value "+String(n)+" is not a constructor or null");function __(){this.constructor=t}e(t,n),t.prototype=null===n?Object.create(n):(__.prototype=n.prototype,new __)}}(),yn=function(){return yn=Object.assign||function(e){for(var t,n=1,r=arguments.length;n0&&i[i.length-1])||6!==a[0]&&2!==a[0])){o=0;continue}if(3===a[0]&&(!i||a[1]>i[0]&&a[1]>( { - path: addQueryArgs( '/wp-parsely/v1/related', { query } ), + path: addQueryArgs( '/wp-parsely/v2/stats/related', query ), } ); } catch ( wpError ) { error = wpError; diff --git a/src/content-helper/dashboard-widget/provider.ts b/src/content-helper/dashboard-widget/provider.ts index 23bc4673e0..0338e490ac 100644 --- a/src/content-helper/dashboard-widget/provider.ts +++ b/src/content-helper/dashboard-widget/provider.ts @@ -83,7 +83,7 @@ export class DashboardWidgetProvider extends BaseProvider { settings: TopPostsSettings, page: number ): Promise { const response = this.fetch( { - path: addQueryArgs( '/wp-parsely/v1/stats/posts/', { + path: addQueryArgs( '/wp-parsely/v2/stats/posts/', { limit: TOP_POSTS_DEFAULT_LIMIT, ...getApiPeriodParams( settings.Period ), sort: settings.Metric, diff --git a/src/content-helper/editor-sidebar/performance-stats/provider.ts b/src/content-helper/editor-sidebar/performance-stats/provider.ts index 5bc524cd65..98ad0681b8 100644 --- a/src/content-helper/editor-sidebar/performance-stats/provider.ts +++ b/src/content-helper/editor-sidebar/performance-stats/provider.ts @@ -68,13 +68,13 @@ export class PerformanceStatsProvider extends BaseProvider { ); } - // Get post URL. - const postUrl = editor.getPermalink(); + // Get post ID. + const postID = editor.getCurrentPostId(); - if ( null === postUrl ) { + if ( null === postID ) { return Promise.reject( new ContentHelperError( __( - "The post's URL returned null.", + "The post's ID returned null.", 'wp-parsely' ), ContentHelperErrorCode.PostIsNotPublished ) ); @@ -84,10 +84,10 @@ export class PerformanceStatsProvider extends BaseProvider { let performanceData, referrerData; try { performanceData = await this.fetchPerformanceDataFromWpEndpoint( - period, postUrl + period, postID ); referrerData = await this.fetchReferrerDataFromWpEndpoint( - period, postUrl, performanceData.views + period, postID, performanceData.views ); } catch ( contentHelperError ) { return Promise.reject( contentHelperError ); @@ -100,20 +100,19 @@ export class PerformanceStatsProvider extends BaseProvider { * Fetches the performance data for the current post from the WordPress REST * API. * - * @param {Period} period The period for which to fetch data. - * @param {string} postUrl + * @param {Period} period The period for which to fetch data. + * @param {number} postId The post's ID. * * @return {Promise } The current post's details. */ private async fetchPerformanceDataFromWpEndpoint( - period: Period, postUrl: string + period: Period, postId: number ): Promise { const response = await this.fetch( { path: addQueryArgs( - '/wp-parsely/v1/stats/post/detail', { + `/wp-parsely/v2/stats/post/${ postId }/details`, { ...getApiPeriodParams( period ), itm_source: this.itmSource, - url: postUrl, } ), } ); @@ -122,8 +121,8 @@ export class PerformanceStatsProvider extends BaseProvider { return Promise.reject( new ContentHelperError( sprintf( /* translators: URL of the published post */ - __( 'The post %s has 0 views, or the Parse.ly API returned no data.', - 'wp-parsely' ), postUrl + __( 'The post %d has 0 views, or the Parse.ly API returned no data.', + 'wp-parsely' ), postId ), ContentHelperErrorCode.ParselyApiReturnedNoData, '' ) ); } @@ -133,8 +132,8 @@ export class PerformanceStatsProvider extends BaseProvider { return Promise.reject( new ContentHelperError( sprintf( /* translators: URL of the published post */ - __( 'Multiple results were returned for the post %s by the Parse.ly API.', - 'wp-parsely' ), postUrl + __( 'Multiple results were returned for the post %d by the Parse.ly API.', + 'wp-parsely' ), postId ), ContentHelperErrorCode.ParselyApiReturnedTooManyResults ) ); } @@ -146,21 +145,20 @@ export class PerformanceStatsProvider extends BaseProvider { * Fetches referrer data for the current post from the WordPress REST API. * * @param {Period} period The period for which to fetch data. - * @param {string} postUrl The post's URL. + * @param {number} postId The post's ID. * @param {string} totalViews Total post views (including direct views). * * @return {Promise} The post's referrer data. */ private async fetchReferrerDataFromWpEndpoint( - period: Period, postUrl: string, totalViews: string + period: Period, postId: string, totalViews: string ): Promise { const response = await this.fetch( { path: addQueryArgs( - '/wp-parsely/v1/referrers/post/detail', { + `/wp-parsely/v2/stats/post/${ postId }/referrers`, { ...getApiPeriodParams( period ), itm_source: this.itmSource, total_views: totalViews, // Needed to calculate direct views. - url: postUrl, } ), } ); diff --git a/src/content-helper/editor-sidebar/related-posts/provider.ts b/src/content-helper/editor-sidebar/related-posts/provider.ts index 6345f4f193..6051c4ca84 100644 --- a/src/content-helper/editor-sidebar/related-posts/provider.ts +++ b/src/content-helper/editor-sidebar/related-posts/provider.ts @@ -146,7 +146,7 @@ export class RelatedPostsProvider extends BaseProvider { */ private async fetchRelatedPostsFromWpEndpoint( query: RelatedPostsApiQuery ): Promise { const response = this.fetch( { - path: addQueryArgs( '/wp-parsely/v1/stats/posts', { + path: addQueryArgs( '/wp-parsely/v2/stats/posts', { ...query.query, itm_source: 'wp-parsely-content-helper', } ), From e14f28c2cb0615494119afb3cf00be8ebdf71745 Mon Sep 17 00:00:00 2001 From: Henrique Mouta Date: Thu, 29 Aug 2024 13:08:26 +0100 Subject: [PATCH 18/49] Delete the old endpoints --- .../class-analytics-post-detail-api-proxy.php | 48 --- .../class-analytics-posts-api-proxy.php | 48 --- .../class-referrers-post-detail-api-proxy.php | 259 --------------- src/Endpoints/class-related-api-proxy.php | 61 ---- .../Proxy/AnalyticsPostsProxyEndpointTest.php | 264 --------------- .../ReferrersPostDetailProxyEndpointTest.php | 305 ------------------ .../Proxy/RelatedProxyEndpointTest.php | 162 ---------- .../StatsPostDetailProxyEndpointTest.php | 206 ------------ wp-parsely.php | 27 +- 9 files changed, 1 insertion(+), 1379 deletions(-) delete mode 100644 src/Endpoints/class-analytics-post-detail-api-proxy.php delete mode 100644 src/Endpoints/class-analytics-posts-api-proxy.php delete mode 100644 src/Endpoints/class-referrers-post-detail-api-proxy.php delete mode 100644 src/Endpoints/class-related-api-proxy.php delete mode 100644 tests/Integration/Endpoints/Proxy/AnalyticsPostsProxyEndpointTest.php delete mode 100644 tests/Integration/Endpoints/Proxy/ReferrersPostDetailProxyEndpointTest.php delete mode 100644 tests/Integration/Endpoints/Proxy/RelatedProxyEndpointTest.php delete mode 100644 tests/Integration/Endpoints/Proxy/StatsPostDetailProxyEndpointTest.php diff --git a/src/Endpoints/class-analytics-post-detail-api-proxy.php b/src/Endpoints/class-analytics-post-detail-api-proxy.php deleted file mode 100644 index 787ae0e50e..0000000000 --- a/src/Endpoints/class-analytics-post-detail-api-proxy.php +++ /dev/null @@ -1,48 +0,0 @@ -register_endpoint( '/stats/post/detail' ); - } - - /** - * Cached "proxy" to the Parse.ly `/analytics/post/detail` API endpoint. - * - * @param WP_REST_Request $request The request object. - * @return stdClass|WP_Error stdClass containing the data or a WP_Error object on failure. - */ - public function get_items( WP_REST_Request $request ) { - return $this->get_data( $request ); - } - - /** - * Generates the final data from the passed response. - * - * @param array $response The response received by the proxy. - * @return array The generated data. - */ - protected function generate_data( $response ): array { - return $this->generate_post_data( $response ); - } -} diff --git a/src/Endpoints/class-analytics-posts-api-proxy.php b/src/Endpoints/class-analytics-posts-api-proxy.php deleted file mode 100644 index 3ee862caf8..0000000000 --- a/src/Endpoints/class-analytics-posts-api-proxy.php +++ /dev/null @@ -1,48 +0,0 @@ -register_endpoint( '/stats/posts' ); - } - - /** - * Cached "proxy" to the Parse.ly `/analytics/posts` API endpoint. - * - * @param WP_REST_Request $request The request object. - * @return stdClass|WP_Error stdClass containing the data or a WP_Error object on failure. - */ - public function get_items( WP_REST_Request $request ) { - return $this->get_data( $request ); - } - - /** - * Generates the final data from the passed response. - * - * @param array $response The response received by the proxy. - * @return array The generated data. - */ - protected function generate_data( $response ): array { - return $this->generate_post_data( $response ); - } -} diff --git a/src/Endpoints/class-referrers-post-detail-api-proxy.php b/src/Endpoints/class-referrers-post-detail-api-proxy.php deleted file mode 100644 index 812a9ca81f..0000000000 --- a/src/Endpoints/class-referrers-post-detail-api-proxy.php +++ /dev/null @@ -1,259 +0,0 @@ -register_endpoint( '/referrers/post/detail' ); - } - - /** - * Cached "proxy" to the Parse.ly `/referrers/post/detail` API endpoint. - * - * @since 3.6.0 - * - * @param WP_REST_Request $request The request object. - * @return stdClass|WP_Error stdClass containing the data or a WP_Error object on failure. - */ - public function get_items( WP_REST_Request $request ) { - $total_views = $request->get_param( 'total_views' ) ?? '0'; - - if ( ! is_string( $total_views ) ) { - $total_views = '0'; - } - - $this->total_views = Utils::convert_to_positive_integer( $total_views ); - $request->offsetUnset( 'total_views' ); // Remove param from request. - return $this->get_data( $request ); - } - - /** - * Generates the final data from the passed response. - * - * @since 3.6.0 - * - * @param array $response The response received by the proxy. - * @return array The generated data. - */ - protected function generate_data( $response ): array { - $referrers_types = $this->generate_referrer_types_data( $response ); - $direct_views = Utils::convert_to_positive_integer( - $referrers_types->direct->views ?? '0' - ); - $referrers_top = $this->generate_referrers_data( 5, $response, $direct_views ); - - return array( - 'top' => $referrers_top, - 'types' => $referrers_types, - ); - } - - /** - * Generates the referrer types data. - * - * Referrer types are: - * - `social`: Views coming from social media. - * - `search`: Views coming from search engines. - * - `other`: Views coming from other referrers, like external websites. - * - `internal`: Views coming from linking pages of the same website. - * - * Returned object properties: - * - `views`: The number of views. - * - `viewPercentage`: The number of views as a percentage, compared to the - * total views of all referrer types. - * - * @since 3.6.0 - * - * @param array $response The response received by the proxy. - * @return stdClass The generated data. - */ - private function generate_referrer_types_data( array $response ): stdClass { - $result = new stdClass(); - $total_referrer_views = 0; // Views from all referrer types combined. - - // Set referrer type order as it is displayed in the Parse.ly dashboard. - $referrer_type_keys = array( 'social', 'search', 'other', 'internal', 'direct' ); - foreach ( $referrer_type_keys as $key ) { - $result->$key = (object) array( 'views' => 0 ); - } - - // Set views and views totals. - foreach ( $response as $referrer_data ) { - /** - * Variable. - * - * @var int - */ - $current_views = $referrer_data->metrics->referrers_views ?? 0; - $total_referrer_views += $current_views; - - /** - * Variable. - * - * @var string - */ - $current_key = $referrer_data->type ?? ''; - if ( '' !== $current_key ) { - if ( ! isset( $result->$current_key->views ) ) { - $result->$current_key = (object) array( 'views' => 0 ); - } - - $result->$current_key->views += $current_views; - } - } - - // Add direct and total views to the object. - $result->direct->views = $this->total_views - $total_referrer_views; - $result->totals = (object) array( 'views' => $this->total_views ); - - // Remove referrer types without views. - foreach ( $referrer_type_keys as $key ) { - if ( 0 === $result->$key->views ) { - unset( $result->$key ); - } - } - - // Set percentage values and format numbers. - // @phpstan-ignore-next-line. - foreach ( $result as $key => $value ) { - // Set and format percentage values. - $result->{ $key }->viewsPercentage = $this->get_i18n_percentage( - absint( $value->views ), - $this->total_views - ); - - // Format views values. - $result->{ $key }->views = number_format_i18n( $result->{ $key }->views ); - } - - return $result; - } - - /** - * Generates the top referrers data. - * - * Returned object properties: - * - `views`: The number of views. - * - `viewPercentage`: The number of views as a percentage, compared to the - * total views of all referrer types. - * - `datasetViewsPercentage: The number of views as a percentage, compared - * to the total views of the current dataset. - * - * @since 3.6.0 - * - * @param int $limit The limit of returned referrers. - * @param array $response The response received by the proxy. - * @param int $direct_views The count of direct views. - * @return stdClass The generated data. - */ - private function generate_referrers_data( - int $limit, - array $response, - int $direct_views - ): stdClass { - $temp_views = array(); - $totals = 0; - $referrer_count = count( $response ); - - // Set views and views totals. - $loop_count = $referrer_count > $limit ? $limit : $referrer_count; - for ( $i = 0; $i < $loop_count; $i++ ) { - $data = $response[ $i ]; - - /** - * Variable. - * - * @var int - */ - $referrer_views = $data->metrics->referrers_views ?? 0; - $totals += $referrer_views; - if ( isset( $data->name ) ) { - $temp_views[ $data->name ] = $referrer_views; - } - } - - // If applicable, add the direct views. - if ( isset( $referrer_views ) && $direct_views >= $referrer_views ) { - $temp_views['direct'] = $direct_views; - $totals += $direct_views; - arsort( $temp_views ); - if ( count( $temp_views ) > $limit ) { - $totals -= array_pop( $temp_views ); - } - } - - // Convert temporary array to result object and add totals. - $result = new stdClass(); - foreach ( $temp_views as $key => $value ) { - $result->$key = (object) array( 'views' => $value ); - } - $result->totals = (object) array( 'views' => $totals ); - - // Set percentages values and format numbers. - // @phpstan-ignore-next-line. - foreach ( $result as $key => $value ) { - // Percentage against all referrer views, even those not included - // in the dataset due to the $limit argument. - $result->{ $key }->viewsPercentage = $this - ->get_i18n_percentage( absint( $value->views ), $this->total_views ); - - // Percentage against the current dataset that is limited due to the - // $limit argument. - $result->{ $key }->datasetViewsPercentage = $this - ->get_i18n_percentage( absint( $value->views ), $totals ); - - // Format views values. - $result->{ $key }->views = number_format_i18n( $result->{ $key }->views ); - } - - return $result; - } - - /** - * Returns the passed number compared to the passed total, in an - * internationalized percentage format. - * - * @since 3.6.0 - * - * @param int $number The number to be calculated as a percentage. - * @param int $total The total number to compare against. - * @return string|false The internationalized percentage or false on error. - */ - private function get_i18n_percentage( int $number, int $total ) { - if ( 0 === $total ) { - return false; - } - - return number_format_i18n( $number / $total * 100, 2 ); - } -} diff --git a/src/Endpoints/class-related-api-proxy.php b/src/Endpoints/class-related-api-proxy.php deleted file mode 100644 index 9835363625..0000000000 --- a/src/Endpoints/class-related-api-proxy.php +++ /dev/null @@ -1,61 +0,0 @@ -register_endpoint( '/related' ); - } - - /** - * Cached "proxy" to the Parse.ly `/related` API endpoint. - * - * @param WP_REST_Request $request The request object. - * @return stdClass|WP_Error stdClass containing the data or a WP_Error object on failure. - */ - public function get_items( WP_REST_Request $request ) { - return $this->get_data( $request, false, 'query' ); - } - - /** - * Generates the final data from the passed response. - * - * @param array $response The response received by the proxy. - * @return array The generated data. - */ - protected function generate_data( $response ): array { - $itm_source = $this->itm_source; - - return array_map( - static function ( stdClass $item ) use ( $itm_source ) { - return (object) array( - 'image_url' => $item->image_url, - 'thumb_url_medium' => $item->thumb_url_medium, - 'title' => $item->title, - 'url' => Parsely::get_url_with_itm_source( $item->url, $itm_source ), - ); - }, - $response - ); - } -} diff --git a/tests/Integration/Endpoints/Proxy/AnalyticsPostsProxyEndpointTest.php b/tests/Integration/Endpoints/Proxy/AnalyticsPostsProxyEndpointTest.php deleted file mode 100644 index b0a7c366bf..0000000000 --- a/tests/Integration/Endpoints/Proxy/AnalyticsPostsProxyEndpointTest.php +++ /dev/null @@ -1,264 +0,0 @@ -dispatch( - new WP_REST_Request( 'GET', self::$route ) - ); - /** - * Variable. - * - * @var WP_Error - */ - $error = $response->as_error(); - - self::assertSame( 401, $response->get_status() ); - self::assertSame( 'rest_forbidden', $error->get_error_code() ); - self::assertSame( - 'Sorry, you are not allowed to do that.', - $error->get_error_message() - ); - } - - /** - * Verifies that calling `GET /wp-parsely/v1/stats/posts` returns an - * error and does not perform a remote call when the Site ID is not populated - * in site options. - * - * @covers \Parsely\Endpoints\Analytics_Posts_API_Proxy::get_items - * @uses \Parsely\Endpoints\Analytics_Posts_API_Proxy::__construct - * @uses \Parsely\Endpoints\Analytics_Posts_API_Proxy::is_available_to_current_user - * @uses \Parsely\Endpoints\Analytics_Posts_API_Proxy::run - * @uses \Parsely\Parsely::site_id_is_missing - * @uses \Parsely\Parsely::site_id_is_set - * @uses \Parsely\Parsely::get_options - * @uses \Parsely\Endpoints\Base_Endpoint::__construct - * @uses \Parsely\Endpoints\Base_API_Proxy::get_data - * @uses \Parsely\Endpoints\Base_API_Proxy::register_endpoint - */ - public function test_get_items_fails_when_site_id_is_not_set(): void { - $this->set_current_user_to_admin(); - parent::run_test_get_items_fails_without_site_id_set(); - } - - /** - * Verifies that calling `GET /wp-parsely/v1/stats/posts` returns an - * error and does not perform a remote call when the API Secret is not - * populated in site options. - * - * @covers \Parsely\Endpoints\Analytics_Posts_API_Proxy::get_items - * @uses \Parsely\Endpoints\Analytics_Posts_API_Proxy::__construct - * @uses \Parsely\Endpoints\Analytics_Posts_API_Proxy::is_available_to_current_user - * @uses \Parsely\Endpoints\Analytics_Posts_API_Proxy::run - * @uses \Parsely\Parsely::site_id_is_missing - * @uses \Parsely\Parsely::site_id_is_set - * @uses \Parsely\Parsely::api_secret_is_set - * @uses \Parsely\Parsely::get_options - * @uses \Parsely\Endpoints\Base_Endpoint::__construct - * @uses \Parsely\Endpoints\Base_API_Proxy::get_data - * @uses \Parsely\Endpoints\Base_API_Proxy::register_endpoint - */ - public function test_get_items_fails_when_api_secret_is_not_set(): void { - $this->set_current_user_to_admin(); - parent::run_test_get_items_fails_without_api_secret_set(); - } - - /** - * Verifies default user capability filter. - * - * @covers \Parsely\Endpoints\Analytics_Posts_API_Proxy::is_available_to_current_user - * @uses \Parsely\Endpoints\Base_Endpoint::__construct - * @uses \Parsely\RemoteAPI\Analytics_Posts_API::is_available_to_current_user - */ - public function test_user_is_allowed_to_make_proxy_api_call_if_default_user_capability_is_changed(): void { - parent::run_test_user_is_allowed_to_make_proxy_api_call_if_default_user_capability_is_changed(); - } - - /** - * Verifies endpoint specific user capability filter. - * - * @covers \Parsely\Endpoints\Analytics_Posts_API_Proxy::is_available_to_current_user - * @uses \Parsely\Endpoints\Base_Endpoint::__construct - * @uses \Parsely\RemoteAPI\Analytics_Posts_API::is_available_to_current_user - */ - public function test_user_is_allowed_to_make_proxy_api_call_if_endpoint_specific_user_capability_is_changed(): void { - parent::run_test_user_is_allowed_to_make_proxy_api_call_if_endpoint_specific_user_capability_is_changed( 'analytics_posts' ); - } - - /** - * Verifies that calls to `GET /wp-parsely/v1/stats/posts` return - * results in the expected format. - * - * @covers \Parsely\Endpoints\Analytics_Posts_API_Proxy::get_items - * @uses \Parsely\Endpoints\Analytics_Posts_API_Proxy::__construct - * @uses \Parsely\Endpoints\Analytics_Posts_API_Proxy::generate_data - * @uses \Parsely\Endpoints\Analytics_Posts_API_Proxy::is_available_to_current_user - * @uses \Parsely\Endpoints\Analytics_Posts_API_Proxy::run - * @uses \Parsely\Endpoints\Base_API_Proxy::get_data - * @uses \Parsely\Endpoints\Base_API_Proxy::register_endpoint - * @uses \Parsely\Parsely::site_id_is_missing - * @uses \Parsely\Parsely::site_id_is_set - * @uses \Parsely\Parsely::api_secret_is_set - * @uses \Parsely\Parsely::get_site_id - * @uses \Parsely\Parsely::get_api_secret - * @uses \Parsely\Parsely::get_options - * @uses \Parsely\Endpoints\Base_Endpoint::__construct - * @uses \Parsely\RemoteAPI\Base_Endpoint_Remote::get_api_url - * @uses \Parsely\RemoteAPI\Base_Endpoint_Remote::get_items - * @uses \Parsely\RemoteAPI\Base_Endpoint_Remote::get_request_options - */ - public function test_get_items(): void { - $this->set_current_user_to_admin(); - TestCase::set_options( - array( - 'apikey' => 'example.com', - 'api_secret' => 'test', - ) - ); - - $dispatched = 0; - $date_format = Utils::get_date_format(); - - add_filter( - 'pre_http_request', - function () use ( &$dispatched ): array { - $dispatched++; - return array( - 'body' => '{"data":[ - { - "author": "Aakash Shah", - "metrics": {"views": 142}, - "pub_date": "2020-04-06T13:30:58", - "thumb_url_medium": "https://images.parsely.com/XCmTXuOf8yVbUYTxj2abQ4RSDkM=/85x85/smart/https%3A//blog.parse.ly/wp-content/uploads/2021/06/Web-Analytics-Tool.png%3Fw%3D150%26h%3D150%26crop%3D1", - "title": "9 Types of Web Analytics Tools \u2014 And How to Know Which Ones You Really Need", - "url": "https://blog.parse.ly/web-analytics-software-tools/?itm_source=parsely-api" - }, - { - "author": "Stephanie Schwartz and Andrew Butler", - "metrics": {"views": 40}, - "pub_date": "2021-04-30T20:30:24", - "thumb_url_medium": "https://images.parsely.com/ap3YSufqxnLpz6zzQshoks3snXI=/85x85/smart/https%3A//blog.parse.ly/wp-content/uploads/2021/05/pexels-brett-jordan-998501-1024x768-2.jpeg%3Fw%3D150%26h%3D150%26crop%3D1", - "title": "5 Tagging Best Practices For Getting the Most Out of Your Content Strategy", - "url": "https://blog.parse.ly/5-tagging-best-practices-content-strategy/?itm_source=parsely-api" - } - ]}', - ); - } - ); - - $rest_request = new WP_REST_Request( 'GET', '/wp-parsely/v1/stats/posts' ); - $rest_request->set_param( 'itm_source', 'wp-parsely-content-helper' ); - - $response = rest_get_server()->dispatch( $rest_request ); - - self::assertSame( 1, $dispatched ); - self::assertSame( 200, $response->get_status() ); - self::assertEquals( - (object) array( - 'data' => array( - (object) array( - 'author' => 'Aakash Shah', - 'date' => wp_date( $date_format, strtotime( '2020-04-06T13:30:58' ) ), - 'id' => 'https://blog.parse.ly/web-analytics-software-tools/', - 'dashUrl' => PARSELY::DASHBOARD_BASE_URL . '/example.com/find?url=https%3A%2F%2Fblog.parse.ly%2Fweb-analytics-software-tools%2F', - 'thumbnailUrl' => 'https://images.parsely.com/XCmTXuOf8yVbUYTxj2abQ4RSDkM=/85x85/smart/https%3A//blog.parse.ly/wp-content/uploads/2021/06/Web-Analytics-Tool.png%3Fw%3D150%26h%3D150%26crop%3D1', - 'title' => '9 Types of Web Analytics Tools — And How to Know Which Ones You Really Need', - 'url' => 'https://blog.parse.ly/web-analytics-software-tools/?itm_source=wp-parsely-content-helper', - 'views' => '142', - 'postId' => 0, - 'rawUrl' => 'https://blog.parse.ly/web-analytics-software-tools/', - ), - (object) array( - 'author' => 'Stephanie Schwartz and Andrew Butler', - 'date' => wp_date( $date_format, strtotime( '2021-04-30T20:30:24' ) ), - 'id' => 'https://blog.parse.ly/5-tagging-best-practices-content-strategy/', - 'dashUrl' => PARSELY::DASHBOARD_BASE_URL . '/example.com/find?url=https%3A%2F%2Fblog.parse.ly%2F5-tagging-best-practices-content-strategy%2F', - 'thumbnailUrl' => 'https://images.parsely.com/ap3YSufqxnLpz6zzQshoks3snXI=/85x85/smart/https%3A//blog.parse.ly/wp-content/uploads/2021/05/pexels-brett-jordan-998501-1024x768-2.jpeg%3Fw%3D150%26h%3D150%26crop%3D1', - 'title' => '5 Tagging Best Practices For Getting the Most Out of Your Content Strategy', - 'url' => 'https://blog.parse.ly/5-tagging-best-practices-content-strategy/?itm_source=wp-parsely-content-helper', - 'views' => '40', - 'postId' => 0, - 'rawUrl' => 'https://blog.parse.ly/5-tagging-best-practices-content-strategy/', - ), - ), - ), - $response->get_data() - ); - } -} diff --git a/tests/Integration/Endpoints/Proxy/ReferrersPostDetailProxyEndpointTest.php b/tests/Integration/Endpoints/Proxy/ReferrersPostDetailProxyEndpointTest.php deleted file mode 100644 index 38ec499a46..0000000000 --- a/tests/Integration/Endpoints/Proxy/ReferrersPostDetailProxyEndpointTest.php +++ /dev/null @@ -1,305 +0,0 @@ -set_current_user_to_admin(); - parent::run_test_get_items_fails_without_site_id_set(); - } - - /** - * Verifies that calling `GET /wp-parsely/v1/referrers/post/detail` returns - * an error and does not perform a remote call when the Site ID is not - * populated in site options. - * - * @covers \Parsely\Endpoints\Referrers_Post_Detail_API_Proxy::get_items - * @uses \Parsely\Endpoints\Base_API_Proxy::get_data - * @uses \Parsely\Endpoints\Base_API_Proxy::register_endpoint - * @uses \Parsely\Endpoints\Referrers_Post_Detail_API_Proxy::__construct - * @uses \Parsely\Endpoints\Referrers_Post_Detail_API_Proxy::is_available_to_current_user - * @uses \Parsely\Endpoints\Referrers_Post_Detail_API_Proxy::run - * @uses \Parsely\Parsely::site_id_is_missing - * @uses \Parsely\Parsely::site_id_is_set - * @uses \Parsely\Parsely::api_secret_is_set - * @uses \Parsely\Parsely::get_options - * @uses \Parsely\Endpoints\Base_Endpoint::__construct - */ - public function test_get_items_fails_when_api_secret_is_not_set(): void { - $this->set_current_user_to_admin(); - parent::run_test_get_items_fails_without_api_secret_set(); - } - - /** - * Verifies default user capability filter. - * - * @covers \Parsely\Endpoints\Referrers_Post_Detail_API_Proxy::is_available_to_current_user - * @uses \Parsely\Endpoints\Base_Endpoint::__construct - * @uses \Parsely\RemoteAPI\Referrers_Post_Detail_API::is_available_to_current_user - */ - public function test_user_is_allowed_to_make_proxy_api_call_if_default_user_capability_is_changed(): void { - parent::run_test_user_is_allowed_to_make_proxy_api_call_if_default_user_capability_is_changed(); - } - - /** - * Verifies endpoint specific user capability filter. - * - * @covers \Parsely\Endpoints\Referrers_Post_Detail_API_Proxy::is_available_to_current_user - * @uses \Parsely\Endpoints\Base_Endpoint::__construct - * @uses \Parsely\RemoteAPI\Referrers_Post_Detail_API::is_available_to_current_user - */ - public function test_user_is_allowed_to_make_proxy_api_call_if_endpoint_specific_user_capability_is_changed(): void { - parent::run_test_user_is_allowed_to_make_proxy_api_call_if_endpoint_specific_user_capability_is_changed(); - } - - /** - * Verifies that calls to `GET /wp-parsely/v1/referrers/post/detail` return - * results in the expected format. - * - * @covers \Parsely\Endpoints\Referrers_Post_Detail_API_Proxy::get_items - * @uses \Parsely\Endpoints\Base_API_Proxy::get_data - * @uses \Parsely\Endpoints\Base_API_Proxy::register_endpoint - * @uses \Parsely\Endpoints\Referrers_Post_Detail_API_Proxy::__construct - * @uses \Parsely\Endpoints\Referrers_Post_Detail_API_Proxy::generate_data - * @uses \Parsely\Endpoints\Referrers_Post_Detail_API_Proxy::is_available_to_current_user - * @uses \Parsely\Endpoints\Referrers_Post_Detail_API_Proxy::run - * @uses \Parsely\Parsely::site_id_is_missing - * @uses \Parsely\Parsely::site_id_is_set - * @uses \Parsely\Parsely::api_secret_is_set - * @uses \Parsely\Parsely::get_site_id - * @uses \Parsely\Parsely::get_options - * @uses \Parsely\Endpoints\Base_Endpoint::__construct - * @uses \Parsely\RemoteAPI\Base_Endpoint_Remote::get_api_url - * @uses \Parsely\RemoteAPI\Base_Endpoint_Remote::get_items - * @uses \Parsely\RemoteAPI\Base_Endpoint_Remote::get_request_options - */ - public function test_get_items(): void { - $this->set_current_user_to_admin(); - TestCase::set_options( - array( - 'apikey' => 'example.com', - 'api_secret' => 'test', - ) - ); - - $dispatched = 0; - - add_filter( - 'pre_http_request', - function () use ( &$dispatched ): array { - $dispatched++; - return array( - 'body' => '{"data":[ - { - "metrics": {"referrers_views": 1500}, - "name": "google", - "type": "search" - }, - { - "metrics": {"referrers_views": 100}, - "name": "blog.parse.ly", - "type": "internal" - }, - { - "metrics": {"referrers_views": 50}, - "name": "bing", - "type": "search" - }, - { - "metrics": {"referrers_views": 30}, - "name": "facebook.com", - "type": "social" - }, - { - "metrics": {"referrers_views": 10}, - "name": "okt.to", - "type": "other" - }, - { - "metrics": {"referrers_views": 10}, - "name": "yandex", - "type": "search" - }, - { - "metrics": {"referrers_views": 10}, - "name": "parse.ly", - "type": "internal" - }, - { - "metrics": {"referrers_views": 10}, - "name": "yahoo!", - "type": "search" - }, - { - "metrics": {"referrers_views": 5}, - "name": "site1.com", - "type": "other" - }, - { - "metrics": {"referrers_views": 5}, - "name": "link.site2.com", - "type": "other" - } - ]}', - ); - } - ); - - $expected_top = (object) array( - 'direct' => (object) array( - 'views' => '770', - 'viewsPercentage' => '30.80', - 'datasetViewsPercentage' => '31.43', - ), - 'google' => (object) array( - 'views' => '1,500', - 'viewsPercentage' => '60.00', - 'datasetViewsPercentage' => '61.22', - ), - 'blog.parse.ly' => (object) array( - 'views' => '100', - 'viewsPercentage' => '4.00', - 'datasetViewsPercentage' => '4.08', - ), - 'bing' => (object) array( - 'views' => '50', - 'viewsPercentage' => '2.00', - 'datasetViewsPercentage' => '2.04', - ), - 'facebook.com' => (object) array( - 'views' => '30', - 'viewsPercentage' => '1.20', - 'datasetViewsPercentage' => '1.22', - ), - 'totals' => (object) array( - 'views' => '2,450', - 'viewsPercentage' => '98.00', - 'datasetViewsPercentage' => '100.00', - ), - ); - - $expected_types = (object) array( - 'social' => (object) array( - 'views' => '30', - 'viewsPercentage' => '1.20', - ), - 'search' => (object) array( - 'views' => '1,570', - 'viewsPercentage' => '62.80', - ), - 'other' => (object) array( - 'views' => '20', - 'viewsPercentage' => '0.80', - ), - 'internal' => (object) array( - 'views' => '110', - 'viewsPercentage' => '4.40', - ), - 'direct' => (object) array( - 'views' => '770', - 'viewsPercentage' => '30.80', - ), - 'totals' => (object) array( - 'views' => '2,500', - 'viewsPercentage' => '100.00', - ), - ); - - $request = new WP_REST_Request( 'GET', self::$route ); - $request->set_param( 'total_views', '2,500' ); - - $response = rest_get_server()->dispatch( $request ); - - self::assertSame( 1, $dispatched ); - self::assertSame( 200, $response->get_status() ); - self::assertEquals( - (object) array( - 'data' => array( - 'top' => $expected_top, - 'types' => $expected_types, - ), - ), - $response->get_data() - ); - } -} diff --git a/tests/Integration/Endpoints/Proxy/RelatedProxyEndpointTest.php b/tests/Integration/Endpoints/Proxy/RelatedProxyEndpointTest.php deleted file mode 100644 index 7584fa597d..0000000000 --- a/tests/Integration/Endpoints/Proxy/RelatedProxyEndpointTest.php +++ /dev/null @@ -1,162 +0,0 @@ - 'example.com' ) ); - - $dispatched = 0; - - add_filter( - 'pre_http_request', - function () use ( &$dispatched ): array { - $dispatched++; - return array( - 'body' => '{"data":[ - { - "image_url":"https:\/\/example.com\/img.png", - "thumb_url_medium":"https:\/\/example.com\/thumb.png", - "title":"something", - "url":"https:\/\/example.com" - }, - { - "image_url":"https:\/\/example.com\/img2.png", - "thumb_url_medium":"https:\/\/example.com\/thumb2.png", - "title":"something2", - "url":"https:\/\/example.com\/2" - } - ]}', - ); - } - ); - - $response = rest_get_server()->dispatch( new WP_REST_Request( 'GET', '/wp-parsely/v1/related' ) ); - - self::assertSame( 1, $dispatched ); - self::assertSame( 200, $response->get_status() ); - self::assertEquals( - (object) array( - 'data' => array( - (object) array( - 'image_url' => 'https://example.com/img.png', - 'thumb_url_medium' => 'https://example.com/thumb.png', - 'title' => 'something', - 'url' => 'https://example.com', - ), - (object) array( - 'image_url' => 'https://example.com/img2.png', - 'thumb_url_medium' => 'https://example.com/thumb2.png', - 'title' => 'something2', - 'url' => 'https://example.com/2', - ), - ), - ), - $response->get_data() - ); - } -} diff --git a/tests/Integration/Endpoints/Proxy/StatsPostDetailProxyEndpointTest.php b/tests/Integration/Endpoints/Proxy/StatsPostDetailProxyEndpointTest.php deleted file mode 100644 index c442d098a2..0000000000 --- a/tests/Integration/Endpoints/Proxy/StatsPostDetailProxyEndpointTest.php +++ /dev/null @@ -1,206 +0,0 @@ -set_current_user_to_admin(); - parent::run_test_get_items_fails_without_site_id_set(); - } - - /** - * Verifies that calling `GET /wp-parsely/v1/analytics/post/detail` returns - * an error and does not perform a remote call when the Site ID is not - * populated in site options. - * - * @covers \Parsely\Endpoints\Analytics_Post_Detail_API_Proxy::get_items - * @uses \Parsely\Endpoints\Base_API_Proxy::get_data - * @uses \Parsely\Endpoints\Base_API_Proxy::register_endpoint - * @uses \Parsely\Endpoints\Analytics_Post_Detail_API_Proxy::__construct - * @uses \Parsely\Endpoints\Analytics_Post_Detail_API_Proxy::is_available_to_current_user - * @uses \Parsely\Endpoints\Analytics_Post_Detail_API_Proxy::run - * @uses \Parsely\Parsely::site_id_is_missing - * @uses \Parsely\Parsely::site_id_is_set - * @uses \Parsely\Parsely::api_secret_is_set - * @uses \Parsely\Parsely::get_options - * @uses \Parsely\Endpoints\Base_Endpoint::__construct - */ - public function test_get_items_fails_when_api_secret_is_not_set(): void { - $this->set_current_user_to_admin(); - parent::run_test_get_items_fails_without_api_secret_set(); - } - - /** - * Verifies default user capability filter. - * - * @covers \Parsely\Endpoints\Analytics_Post_Detail_API_Proxy::is_available_to_current_user - * @uses \Parsely\Endpoints\Base_Endpoint::__construct - */ - public function test_user_is_allowed_to_make_proxy_api_call_if_default_user_capability_is_changed(): void { - parent::run_test_user_is_allowed_to_make_proxy_api_call_if_default_user_capability_is_changed(); - } - - /** - * Verifies endpoint specific user capability filter. - * - * @covers \Parsely\Endpoints\Analytics_Post_Detail_API_Proxy::is_available_to_current_user - * @uses \Parsely\Endpoints\Base_Endpoint::__construct - */ - public function test_user_is_allowed_to_make_proxy_api_call_if_endpoint_specific_user_capability_is_changed(): void { - parent::run_test_user_is_allowed_to_make_proxy_api_call_if_endpoint_specific_user_capability_is_changed( 'analytics_post_detail' ); - } - - /** - * Verifies that calls to `GET /wp-parsely/v1/analytics/post/detail` return - * results in the expected format. - * - * @covers \Parsely\Endpoints\Analytics_Post_Detail_API_Proxy::get_items - * @uses \Parsely\Endpoints\Base_API_Proxy::get_data - * @uses \Parsely\Endpoints\Base_API_Proxy::register_endpoint - * @uses \Parsely\Endpoints\Analytics_Post_Detail_API_Proxy::__construct - * @uses \Parsely\Endpoints\Analytics_Post_Detail_API_Proxy::generate_data - * @uses \Parsely\Endpoints\Analytics_Post_Detail_API_Proxy::is_available_to_current_user - * @uses \Parsely\Endpoints\Analytics_Post_Detail_API_Proxy::run - * @uses \Parsely\Parsely::site_id_is_missing - * @uses \Parsely\Parsely::site_id_is_set - * @uses \Parsely\Parsely::api_secret_is_set - * @uses \Parsely\Parsely::get_site_id - * @uses \Parsely\Parsely::get_options - * @uses \Parsely\Endpoints\Base_Endpoint::__construct - * @uses \Parsely\RemoteAPI\Base_Endpoint_Remote::get_api_url - * @uses \Parsely\RemoteAPI\Base_Endpoint_Remote::get_items - * @uses \Parsely\RemoteAPI\Base_Endpoint_Remote::get_request_options - */ - public function test_get_items(): void { - $this->set_current_user_to_admin(); - TestCase::set_options( - array( - 'apikey' => 'example.com', - 'api_secret' => 'test', - ) - ); - - $dispatched = 0; - - add_filter( - 'pre_http_request', - function () use ( &$dispatched ): array { - $dispatched++; - return array( - 'body' => ' - {"data":[{ - "avg_engaged": 1.911, - "metrics": { - "views": 2158, - "visitors": 1537 - }, - "url": "https://example.com" - }]} - ', - ); - } - ); - - $response = rest_get_server()->dispatch( new WP_REST_Request( 'GET', '/wp-parsely/v1/stats/post/detail' ) ); - - self::assertSame( 1, $dispatched ); - self::assertSame( 200, $response->get_status() ); - self::assertEquals( - (object) array( - 'data' => array( - (object) array( - 'avgEngaged' => '1:55', - 'dashUrl' => Parsely::DASHBOARD_BASE_URL . '/example.com/find?url=https%3A%2F%2Fexample.com', - 'id' => 'https://example.com', - 'postId' => 0, - 'url' => 'https://example.com', - 'views' => '2,158', - 'visitors' => '1,537', - 'rawUrl' => 'https://example.com', - ), - ), - ), - $response->get_data() - ); - } -} diff --git a/wp-parsely.php b/wp-parsely.php index ae8e313315..cf5012bef6 100644 --- a/wp-parsely.php +++ b/wp-parsely.php @@ -132,37 +132,12 @@ function parsely_wp_admin_early_register(): void { * @since 3.2.0 */ function parsely_rest_api_init(): void { - $wp_cache = new WordPress_Cache(); - $rest = new Rest_Metadata( $GLOBALS['parsely'] ); + $rest = new Rest_Metadata( $GLOBALS['parsely'] ); $rest->run(); // Content Helper settings endpoints. ( new Dashboard_Widget_Settings_Endpoint( $GLOBALS['parsely'] ) )->run(); ( new Editor_Sidebar_Settings_Endpoint( $GLOBALS['parsely'] ) )->run(); - - parsely_run_rest_api_endpoint( - Related_API::class, - Related_API_Proxy::class, - $wp_cache - ); - - parsely_run_rest_api_endpoint( - Analytics_Posts_API::class, - Analytics_Posts_API_Proxy::class, - $wp_cache - ); - - parsely_run_rest_api_endpoint( - Analytics_Post_Detail_API::class, - Analytics_Post_Detail_API_Proxy::class, - $wp_cache - ); - - parsely_run_rest_api_endpoint( - Referrers_Post_Detail_API::class, - Referrers_Post_Detail_API_Proxy::class, - $wp_cache - ); } add_action( 'init', __NAMESPACE__ . '\\init_recommendations_block' ); From 5978a36710fef5ab54ca76770aaff2860589ad45 Mon Sep 17 00:00:00 2001 From: Henrique Mouta Date: Thu, 29 Aug 2024 13:08:50 +0100 Subject: [PATCH 19/49] Add tests --- src/rest-api/stats/class-endpoint-related.php | 6 +- src/rest-api/stats/trait-post-data.php | 7 +- .../Integration/RestAPI/BaseEndpointTest.php | 12 + .../ContentHelperControllerTest.php | 27 +- .../ContentHelperFeatureTestTrait.php | 11 +- .../EndpointExcerptGeneratorTest.php | 23 +- .../EndpointSmartLinkingTest.php | 43 +- .../EndpointTitleSuggestionsTest.php | 11 +- .../RestAPI/Stats/EndpointPostTest.php | 674 ++++++++++++++++++ .../RestAPI/Stats/EndpointPostsTest.php | 347 +++++++++ .../RestAPI/Stats/EndpointRelatedTest.php | 316 ++++++++ .../RestAPI/Stats/StatsControllerTest.php | 60 ++ 12 files changed, 1483 insertions(+), 54 deletions(-) create mode 100644 tests/Integration/RestAPI/Stats/EndpointPostTest.php create mode 100644 tests/Integration/RestAPI/Stats/EndpointPostsTest.php create mode 100644 tests/Integration/RestAPI/Stats/EndpointRelatedTest.php create mode 100644 tests/Integration/RestAPI/Stats/StatsControllerTest.php diff --git a/src/rest-api/stats/class-endpoint-related.php b/src/rest-api/stats/class-endpoint-related.php index d675e9e923..d636957b8f 100644 --- a/src/rest-api/stats/class-endpoint-related.php +++ b/src/rest-api/stats/class-endpoint-related.php @@ -104,9 +104,9 @@ public function get_related_posts( WP_REST_Request $request ) { * @since 3.17.0 * * @param WP_REST_Request|null $request The request object. - * @return bool + * @return bool|WP_Error */ - public function is_available_to_current_user( ?WP_REST_Request $request = null ): bool { - return true; + public function is_available_to_current_user( ?WP_REST_Request $request = null ) { + return $this->validate_site_id_and_secret( false ); } } diff --git a/src/rest-api/stats/trait-post-data.php b/src/rest-api/stats/trait-post-data.php index 4698d2a9a4..0ccb0a087e 100644 --- a/src/rest-api/stats/trait-post-data.php +++ b/src/rest-api/stats/trait-post-data.php @@ -66,10 +66,9 @@ private function set_itm_source_from_request( WP_REST_Request $request ): void { private function get_itm_source_param_args(): array { return array( 'itm_source' => array( - 'description' => __( 'The source of the item.', 'wp-parsely' ), - 'type' => 'string', - 'required' => false, - 'validate_callback' => array( $this, 'validate_itm_source' ), + 'description' => __( 'The source of the item.', 'wp-parsely' ), + 'type' => 'string', + 'required' => false, ), ); } diff --git a/tests/Integration/RestAPI/BaseEndpointTest.php b/tests/Integration/RestAPI/BaseEndpointTest.php index f437531842..00f2f7bb83 100644 --- a/tests/Integration/RestAPI/BaseEndpointTest.php +++ b/tests/Integration/RestAPI/BaseEndpointTest.php @@ -238,11 +238,19 @@ public function test_route_is_registered(): void { * @uses \Parsely\REST_API\Base_API_Controller::__construct * @uses \Parsely\REST_API\Base_API_Controller::get_full_namespace * @uses \Parsely\REST_API\Base_API_Controller::prefix_route + * @uses \Parsely\REST_API\Base_API_Controller::get_parsely + * @uses \Parsely\REST_API\REST_API_Controller::get_namespace + * @uses \Parsely\REST_API\REST_API_Controller::get_version + * @uses \Parsely\REST_API\Base_API_Controller::get_route_prefix * @uses \Parsely\REST_API\Base_Endpoint::__construct * @uses \Parsely\REST_API\Base_Endpoint::get_endpoint_name * @uses \Parsely\REST_API\Base_Endpoint::is_available_to_current_user * @uses \Parsely\REST_API\Base_Endpoint::register_rest_route * @uses \Parsely\REST_API\Base_Endpoint::validate_site_id_and_secret + * @uses \Parsely\REST_API\Base_Endpoint::apply_capability_filters + * @uses \Parsely\REST_API\Base_Endpoint::get_default_access_capability + * @uses \Parsely\REST_API\Content_Helper\Content_Helper_Controller::get_route_prefix + * @uses \Parsely\REST_API\Stats\Stats_Controller::get_route_prefix * @uses \Parsely\Utils\Utils::convert_endpoint_to_filter_key */ public function test_endpoint_is_registered_based_on_filter(): void { @@ -285,6 +293,7 @@ public function test_endpoint_is_registered_based_on_filter(): void { * @covers \Parsely\REST_API\Base_Endpoint::is_available_to_current_user * @covers \Parsely\REST_API\Base_Endpoint::validate_site_id_and_secret * @uses \Parsely\REST_API\Base_API_Controller::__construct + * @uses \Parsely\REST_API\Base_API_Controller::get_parsely * @uses \Parsely\Parsely::__construct * @uses \Parsely\Parsely::allow_parsely_remote_requests * @uses \Parsely\Parsely::are_credentials_managed @@ -324,6 +333,7 @@ public function test_is_available_to_current_user_returns_error_site_id_not_set( * @covers \Parsely\REST_API\Base_Endpoint::is_available_to_current_user * @covers \Parsely\REST_API\Base_Endpoint::validate_site_id_and_secret * @uses \Parsely\REST_API\Base_API_Controller::__construct + * @uses \Parsely\REST_API\Base_API_Controller::get_parsely * @uses \Parsely\Parsely::__construct * @uses \Parsely\Parsely::allow_parsely_remote_requests * @uses \Parsely\Parsely::api_secret_is_set @@ -361,6 +371,7 @@ public function test_is_available_to_current_user_returns_error_api_secret_not_s * * @covers \Parsely\REST_API\Base_Endpoint::apply_capability_filters * @uses \Parsely\REST_API\Base_API_Controller::__construct + * @uses \Parsely\REST_API\Base_API_Controller::get_parsely * @uses \Parsely\Utils\Utils::convert_endpoint_to_filter_key * @uses \Parsely\Parsely::__construct * @uses \Parsely\Parsely::allow_parsely_remote_requests @@ -387,6 +398,7 @@ function () { * * @covers \Parsely\REST_API\Base_Endpoint::validate_site_id_and_secret * @uses \Parsely\REST_API\Base_API_Controller::__construct + * @uses \Parsely\REST_API\Base_API_Controller::get_parsely * @uses \Parsely\Parsely::__construct * @uses \Parsely\Parsely::allow_parsely_remote_requests * @uses \Parsely\Parsely::api_secret_is_set diff --git a/tests/Integration/RestAPI/ContentHelper/ContentHelperControllerTest.php b/tests/Integration/RestAPI/ContentHelper/ContentHelperControllerTest.php index 5b22320e87..944dd6b814 100644 --- a/tests/Integration/RestAPI/ContentHelper/ContentHelperControllerTest.php +++ b/tests/Integration/RestAPI/ContentHelper/ContentHelperControllerTest.php @@ -55,6 +55,8 @@ public function set_up(): void { * * @covers \Parsely\REST_API\Content_Helper\Content_Helper_Controller::__construct * @uses \Parsely\REST_API\Content_Helper\Content_Helper_Controller::get_full_namespace + * @uses \Parsely\REST_API\REST_API_Controller::get_namespace + * @uses \Parsely\REST_API\REST_API_Controller::get_version */ public function test_constructor_sets_up_namespace_and_version(): void { self::assertEquals( 'wp-parsely/v2', $this->content_helper_controller->get_full_namespace() ); @@ -77,16 +79,21 @@ public function test_route_prefix(): void { * @since 3.17.0 * * @covers \Parsely\REST_API\Content_Helper\Content_Helper_Controller::init - * @uses \Parsely\REST_API\Content_Helper\Content_Helper_Controller::register_endpoints - * @uses Parsely\Endpoints\Base_Endpoint::__construct - * @uses Parsely\REST_API\Base_API_Controller::__construct - * @uses Parsely\REST_API\Base_API_Controller::register_endpoint - * @uses Parsely\REST_API\Base_Endpoint::__construct - * @uses Parsely\REST_API\Base_Endpoint::init - * @uses Parsely\REST_API\Content_Helper\Excerpt_Generator_Endpoint::__construct - * @uses Parsely\REST_API\Content_Helper\Smart_Linking_Endpoint::__construct - * @uses Parsely\REST_API\Content_Helper\Title_Suggestions_Endpoint::__construct - * @uses Parsely\Utils\Utils::convert_endpoint_to_filter_key + * @uses \Parsely\Endpoints\Base_Endpoint::__construct + * @uses \Parsely\REST_API\Base_API_Controller::__construct + * @uses \Parsely\REST_API\Base_API_Controller::get_endpoints + * @uses \Parsely\REST_API\Base_API_Controller::get_parsely + * @uses \Parsely\REST_API\Base_API_Controller::register_endpoint + * @uses \Parsely\REST_API\Base_API_Controller::register_endpoints + * @uses \Parsely\REST_API\Base_Endpoint::__construct + * @uses \Parsely\REST_API\Base_Endpoint::init + * @uses \Parsely\REST_API\Content_Helper\Endpoint_Excerpt_Generator::__construct + * @uses \Parsely\REST_API\Content_Helper\Endpoint_Excerpt_Generator::get_endpoint_name + * @uses \Parsely\REST_API\Content_Helper\Endpoint_Smart_Linking::__construct + * @uses \Parsely\REST_API\Content_Helper\Endpoint_Smart_Linking::get_endpoint_name + * @uses \Parsely\REST_API\Content_Helper\Endpoint_Title_Suggestions::__construct + * @uses \Parsely\REST_API\Content_Helper\Endpoint_Title_Suggestions::get_endpoint_name + * @uses \Parsely\Utils\Utils::convert_endpoint_to_filter_key */ public function test_init_registers_endpoints(): void { $this->content_helper_controller->init(); diff --git a/tests/Integration/RestAPI/ContentHelper/ContentHelperFeatureTestTrait.php b/tests/Integration/RestAPI/ContentHelper/ContentHelperFeatureTestTrait.php index 32e859dbb7..878cbdd44d 100644 --- a/tests/Integration/RestAPI/ContentHelper/ContentHelperFeatureTestTrait.php +++ b/tests/Integration/RestAPI/ContentHelper/ContentHelperFeatureTestTrait.php @@ -190,18 +190,21 @@ public function test_is_available_to_current_user_returns_error_if_no_permission * * @covers \Parsely\REST_API\Content_Helper\Content_Helper_Feature::is_available_to_current_user * @uses \Parsely\Endpoints\Base_Endpoint::__construct - * @uses \Parsely\Parsely::__construct - * @uses \Parsely\Parsely::allow_parsely_remote_requests - * @uses \Parsely\Parsely::are_credentials_managed + * @uses \Parsely\Parsely::api_secret_is_set * @uses \Parsely\Parsely::get_managed_credentials * @uses \Parsely\Parsely::get_options * @uses \Parsely\Parsely::set_default_full_metadata_in_non_posts - * @uses \Parsely\Parsely::set_managed_options + * @uses \Parsely\Parsely::site_id_is_set * @uses \Parsely\Permissions::current_user_can_use_pch_feature * @uses \Parsely\Permissions::get_user_roles_with_edit_posts_cap * @uses \Parsely\REST_API\Base_API_Controller::__construct + * @uses \Parsely\REST_API\Base_API_Controller::get_parsely * @uses \Parsely\REST_API\Base_Endpoint::__construct + * @uses \Parsely\REST_API\Base_Endpoint::apply_capability_filters + * @uses \Parsely\REST_API\Base_Endpoint::get_default_access_capability * @uses \Parsely\REST_API\Base_Endpoint::init + * @uses \Parsely\REST_API\Base_Endpoint::is_available_to_current_user + * @uses \Parsely\REST_API\Base_Endpoint::validate_site_id_and_secret * @uses \Parsely\Utils\Utils::convert_endpoint_to_filter_key */ public function test_is_available_to_current_user_returns_error_if_no_user(): void { diff --git a/tests/Integration/RestAPI/ContentHelper/EndpointExcerptGeneratorTest.php b/tests/Integration/RestAPI/ContentHelper/EndpointExcerptGeneratorTest.php index a7eb36d773..08f07e046c 100644 --- a/tests/Integration/RestAPI/ContentHelper/EndpointExcerptGeneratorTest.php +++ b/tests/Integration/RestAPI/ContentHelper/EndpointExcerptGeneratorTest.php @@ -68,29 +68,28 @@ public function get_endpoint(): \Parsely\REST_API\Base_Endpoint { * * @covers \Parsely\REST_API\Content_Helper\Endpoint_Excerpt_Generator::register_routes * @uses \Parsely\Endpoints\Base_Endpoint::__construct - * @uses \Parsely\Parsely::__construct - * @uses \Parsely\Parsely::allow_parsely_remote_requests * @uses \Parsely\Parsely::api_secret_is_set - * @uses \Parsely\Parsely::are_credentials_managed * @uses \Parsely\Parsely::get_managed_credentials * @uses \Parsely\Parsely::get_options * @uses \Parsely\Parsely::set_default_content_helper_settings_values * @uses \Parsely\Parsely::set_default_full_metadata_in_non_posts - * @uses \Parsely\Parsely::set_managed_options * @uses \Parsely\Parsely::site_id_is_set * @uses \Parsely\Permissions::build_pch_permissions_settings_array * @uses \Parsely\Permissions::current_user_can_use_pch_feature * @uses \Parsely\Permissions::get_user_roles_with_edit_posts_cap * @uses \Parsely\REST_API\Base_API_Controller::__construct * @uses \Parsely\REST_API\Base_API_Controller::get_full_namespace + * @uses \Parsely\REST_API\Base_API_Controller::get_parsely * @uses \Parsely\REST_API\Base_API_Controller::prefix_route * @uses \Parsely\REST_API\Base_Endpoint::__construct - * @uses \Parsely\REST_API\Base_Endpoint::get_endpoint_name * @uses \Parsely\REST_API\Base_Endpoint::get_full_endpoint * @uses \Parsely\REST_API\Base_Endpoint::init * @uses \Parsely\REST_API\Base_Endpoint::is_available_to_current_user * @uses \Parsely\REST_API\Base_Endpoint::register_rest_route * @uses \Parsely\REST_API\Base_Endpoint::validate_site_id_and_secret + * @uses \Parsely\REST_API\Content_Helper\Content_Helper_Controller::get_route_prefix + * @uses \Parsely\REST_API\REST_API_Controller::get_namespace + * @uses \Parsely\REST_API\REST_API_Controller::get_version * @uses \Parsely\Utils\Utils::convert_endpoint_to_filter_key */ public function test_route_is_registered(): void { @@ -114,12 +113,11 @@ public function test_route_is_registered(): void { * * @covers \Parsely\REST_API\Content_Helper\Endpoint_Excerpt_Generator::generate_excerpt * @uses \Parsely\Endpoints\Base_Endpoint::__construct - * @uses \Parsely\Parsely::__construct - * @uses \Parsely\Parsely::allow_parsely_remote_requests - * @uses \Parsely\Parsely::are_credentials_managed - * @uses \Parsely\Parsely::set_managed_options * @uses \Parsely\REST_API\Base_API_Controller::__construct + * @uses \Parsely\REST_API\Base_API_Controller::get_parsely * @uses \Parsely\REST_API\Base_Endpoint::__construct + * @uses \Parsely\REST_API\Base_Endpoint::init + * @uses \Parsely\Utils\Utils::convert_endpoint_to_filter_key */ public function test_generate_excerpt_returns_valid_response(): void { // Mock the Suggest_Brief_API to control the response. @@ -160,12 +158,11 @@ public function test_generate_excerpt_returns_valid_response(): void { * * @covers \Parsely\REST_API\Content_Helper\Endpoint_Excerpt_Generator::generate_excerpt * @uses \Parsely\Endpoints\Base_Endpoint::__construct - * @uses \Parsely\Parsely::__construct - * @uses \Parsely\Parsely::allow_parsely_remote_requests - * @uses \Parsely\Parsely::are_credentials_managed - * @uses \Parsely\Parsely::set_managed_options * @uses \Parsely\REST_API\Base_API_Controller::__construct + * @uses \Parsely\REST_API\Base_API_Controller::get_parsely * @uses \Parsely\REST_API\Base_Endpoint::__construct + * @uses \Parsely\REST_API\Base_Endpoint::init + * @uses \Parsely\Utils\Utils::convert_endpoint_to_filter_key */ public function test_generate_excerpt_returns_error_on_failure(): void { // Mock the Suggest_Brief_API to simulate a failure. diff --git a/tests/Integration/RestAPI/ContentHelper/EndpointSmartLinkingTest.php b/tests/Integration/RestAPI/ContentHelper/EndpointSmartLinkingTest.php index 99b9c5c103..9fd983fefa 100644 --- a/tests/Integration/RestAPI/ContentHelper/EndpointSmartLinkingTest.php +++ b/tests/Integration/RestAPI/ContentHelper/EndpointSmartLinkingTest.php @@ -79,29 +79,30 @@ public function get_endpoint(): \Parsely\REST_API\Base_Endpoint { * * @covers \Parsely\REST_API\Content_Helper\Endpoint_Smart_Linking::register_routes * @uses \Parsely\Endpoints\Base_Endpoint::__construct - * @uses \Parsely\Parsely::__construct - * @uses \Parsely\Parsely::allow_parsely_remote_requests * @uses \Parsely\Parsely::api_secret_is_set - * @uses \Parsely\Parsely::are_credentials_managed * @uses \Parsely\Parsely::get_managed_credentials * @uses \Parsely\Parsely::get_options * @uses \Parsely\Parsely::set_default_content_helper_settings_values * @uses \Parsely\Parsely::set_default_full_metadata_in_non_posts - * @uses \Parsely\Parsely::set_managed_options * @uses \Parsely\Parsely::site_id_is_set * @uses \Parsely\Permissions::build_pch_permissions_settings_array * @uses \Parsely\Permissions::current_user_can_use_pch_feature * @uses \Parsely\Permissions::get_user_roles_with_edit_posts_cap * @uses \Parsely\REST_API\Base_API_Controller::__construct * @uses \Parsely\REST_API\Base_API_Controller::get_full_namespace + * @uses \Parsely\REST_API\Base_API_Controller::get_parsely * @uses \Parsely\REST_API\Base_API_Controller::prefix_route * @uses \Parsely\REST_API\Base_Endpoint::__construct - * @uses \Parsely\REST_API\Base_Endpoint::get_endpoint_name + * @uses \Parsely\REST_API\Base_Endpoint::apply_capability_filters + * @uses \Parsely\REST_API\Base_Endpoint::get_default_access_capability * @uses \Parsely\REST_API\Base_Endpoint::get_full_endpoint * @uses \Parsely\REST_API\Base_Endpoint::init * @uses \Parsely\REST_API\Base_Endpoint::is_available_to_current_user * @uses \Parsely\REST_API\Base_Endpoint::register_rest_route * @uses \Parsely\REST_API\Base_Endpoint::validate_site_id_and_secret + * @uses \Parsely\REST_API\Content_Helper\Content_Helper_Controller::get_route_prefix + * @uses \Parsely\REST_API\REST_API_Controller::get_namespace + * @uses \Parsely\REST_API\REST_API_Controller::get_version * @uses \Parsely\Utils\Utils::convert_endpoint_to_filter_key */ public function test_route_is_registered(): void { @@ -125,12 +126,17 @@ public function test_route_is_registered(): void { * * @covers \Parsely\REST_API\Content_Helper\Endpoint_Smart_Linking::generate_smart_links * @uses \Parsely\Endpoints\Base_Endpoint::__construct - * @uses \Parsely\Parsely::__construct - * @uses \Parsely\Parsely::allow_parsely_remote_requests - * @uses \Parsely\Parsely::are_credentials_managed - * @uses \Parsely\Parsely::set_managed_options + * @uses \Parsely\Models\Base_Model::__construct + * @uses \Parsely\Models\Smart_Link::__construct + * @uses \Parsely\Models\Smart_Link::generate_uid + * @uses \Parsely\Models\Smart_Link::get_post_id_by_url + * @uses \Parsely\Models\Smart_Link::set_href + * @uses \Parsely\Models\Smart_Link::to_array * @uses \Parsely\REST_API\Base_API_Controller::__construct + * @uses \Parsely\REST_API\Base_API_Controller::get_parsely * @uses \Parsely\REST_API\Base_Endpoint::__construct + * @uses \Parsely\REST_API\Base_Endpoint::init + * @uses \Parsely\Utils\Utils::convert_endpoint_to_filter_key */ public function test_generate_smart_links_returns_valid_response(): void { // Mock the Suggest_Linked_Reference_API to control the response. @@ -175,12 +181,11 @@ public function test_generate_smart_links_returns_valid_response(): void { * * @covers \Parsely\REST_API\Content_Helper\Endpoint_Smart_Linking::generate_smart_links * @uses \Parsely\Endpoints\Base_Endpoint::__construct - * @uses \Parsely\Parsely::__construct - * @uses \Parsely\Parsely::allow_parsely_remote_requests - * @uses \Parsely\Parsely::are_credentials_managed - * @uses \Parsely\Parsely::set_managed_options * @uses \Parsely\REST_API\Base_API_Controller::__construct + * @uses \Parsely\REST_API\Base_API_Controller::get_parsely * @uses \Parsely\REST_API\Base_Endpoint::__construct + * @uses \Parsely\REST_API\Base_Endpoint::init + * @uses \Parsely\Utils\Utils::convert_endpoint_to_filter_key */ public function test_generate_smart_links_returns_error_on_failure(): void { // Mock the Suggest_Linked_Reference_API to simulate a failure. @@ -238,15 +243,19 @@ public function test_generate_smart_links_returns_error_on_failure(): void { * @uses \Parsely\Permissions::get_user_roles_with_edit_posts_cap * @uses \Parsely\REST_API\Base_API_Controller::__construct * @uses \Parsely\REST_API\Base_API_Controller::get_full_namespace + * @uses \Parsely\REST_API\Base_API_Controller::get_parsely * @uses \Parsely\REST_API\Base_API_Controller::prefix_route * @uses \Parsely\REST_API\Base_Endpoint::__construct * @uses \Parsely\REST_API\Base_Endpoint::apply_capability_filters - * @uses \Parsely\REST_API\Base_Endpoint::get_endpoint_name + * @uses \Parsely\REST_API\Base_Endpoint::get_default_access_capability * @uses \Parsely\REST_API\Base_Endpoint::get_full_endpoint * @uses \Parsely\REST_API\Base_Endpoint::init * @uses \Parsely\REST_API\Base_Endpoint::is_available_to_current_user * @uses \Parsely\REST_API\Base_Endpoint::register_rest_route * @uses \Parsely\REST_API\Base_Endpoint::validate_site_id_and_secret + * @uses \Parsely\REST_API\Content_Helper\Content_Helper_Controller::get_route_prefix + * @uses \Parsely\REST_API\REST_API_Controller::get_namespace + * @uses \Parsely\REST_API\REST_API_Controller::get_version * @uses \Parsely\Utils\Utils::convert_endpoint_to_filter_key */ public function test_add_smart_link_returns_valid_response(): void { @@ -334,15 +343,19 @@ public function test_add_smart_link_returns_valid_response(): void { * @uses \Parsely\Permissions::get_user_roles_with_edit_posts_cap * @uses \Parsely\REST_API\Base_API_Controller::__construct * @uses \Parsely\REST_API\Base_API_Controller::get_full_namespace + * @uses \Parsely\REST_API\Base_API_Controller::get_parsely * @uses \Parsely\REST_API\Base_API_Controller::prefix_route * @uses \Parsely\REST_API\Base_Endpoint::__construct * @uses \Parsely\REST_API\Base_Endpoint::apply_capability_filters - * @uses \Parsely\REST_API\Base_Endpoint::get_endpoint_name + * @uses \Parsely\REST_API\Base_Endpoint::get_default_access_capability * @uses \Parsely\REST_API\Base_Endpoint::get_full_endpoint * @uses \Parsely\REST_API\Base_Endpoint::init * @uses \Parsely\REST_API\Base_Endpoint::is_available_to_current_user * @uses \Parsely\REST_API\Base_Endpoint::register_rest_route * @uses \Parsely\REST_API\Base_Endpoint::validate_site_id_and_secret + * @uses \Parsely\REST_API\Content_Helper\Content_Helper_Controller::get_route_prefix + * @uses \Parsely\REST_API\REST_API_Controller::get_namespace + * @uses \Parsely\REST_API\REST_API_Controller::get_version * @uses \Parsely\Utils\Utils::convert_endpoint_to_filter_key */ public function test_add_multiple_smart_links_returns_valid_response(): void { diff --git a/tests/Integration/RestAPI/ContentHelper/EndpointTitleSuggestionsTest.php b/tests/Integration/RestAPI/ContentHelper/EndpointTitleSuggestionsTest.php index 15fa9e622f..508a1fbabe 100644 --- a/tests/Integration/RestAPI/ContentHelper/EndpointTitleSuggestionsTest.php +++ b/tests/Integration/RestAPI/ContentHelper/EndpointTitleSuggestionsTest.php @@ -67,29 +67,28 @@ public function get_endpoint(): \Parsely\REST_API\Base_Endpoint { * * @covers \Parsely\REST_API\Content_Helper\Endpoint_Title_Suggestions::register_routes * @uses \Parsely\Endpoints\Base_Endpoint::__construct - * @uses \Parsely\Parsely::__construct - * @uses \Parsely\Parsely::allow_parsely_remote_requests * @uses \Parsely\Parsely::api_secret_is_set - * @uses \Parsely\Parsely::are_credentials_managed * @uses \Parsely\Parsely::get_managed_credentials * @uses \Parsely\Parsely::get_options * @uses \Parsely\Parsely::set_default_content_helper_settings_values * @uses \Parsely\Parsely::set_default_full_metadata_in_non_posts - * @uses \Parsely\Parsely::set_managed_options * @uses \Parsely\Parsely::site_id_is_set * @uses \Parsely\Permissions::build_pch_permissions_settings_array * @uses \Parsely\Permissions::current_user_can_use_pch_feature * @uses \Parsely\Permissions::get_user_roles_with_edit_posts_cap * @uses \Parsely\REST_API\Base_API_Controller::__construct * @uses \Parsely\REST_API\Base_API_Controller::get_full_namespace + * @uses \Parsely\REST_API\Base_API_Controller::get_parsely * @uses \Parsely\REST_API\Base_API_Controller::prefix_route * @uses \Parsely\REST_API\Base_Endpoint::__construct - * @uses \Parsely\REST_API\Base_Endpoint::get_endpoint_name * @uses \Parsely\REST_API\Base_Endpoint::get_full_endpoint * @uses \Parsely\REST_API\Base_Endpoint::init * @uses \Parsely\REST_API\Base_Endpoint::is_available_to_current_user * @uses \Parsely\REST_API\Base_Endpoint::register_rest_route * @uses \Parsely\REST_API\Base_Endpoint::validate_site_id_and_secret + * @uses \Parsely\REST_API\Content_Helper\Content_Helper_Controller::get_route_prefix + * @uses \Parsely\REST_API\REST_API_Controller::get_namespace + * @uses \Parsely\REST_API\REST_API_Controller::get_version * @uses \Parsely\Utils\Utils::convert_endpoint_to_filter_key */ public function test_route_is_registered(): void { @@ -116,6 +115,7 @@ public function test_route_is_registered(): void { * @uses \Parsely\Parsely::are_credentials_managed * @uses \Parsely\Parsely::set_managed_options * @uses \Parsely\REST_API\Base_API_Controller::__construct + * @uses \Parsely\REST_API\Base_API_Controller::get_parsely * @uses \Parsely\REST_API\Base_Endpoint::__construct * @uses \Parsely\REST_API\Base_Endpoint::init * @uses \Parsely\Utils\Utils::convert_endpoint_to_filter_key @@ -164,6 +164,7 @@ public function test_generate_titles_returns_valid_response(): void { * @uses \Parsely\Parsely::are_credentials_managed * @uses \Parsely\Parsely::set_managed_options * @uses \Parsely\REST_API\Base_API_Controller::__construct + * @uses \Parsely\REST_API\Base_API_Controller::get_parsely * @uses \Parsely\REST_API\Base_Endpoint::__construct * @uses \Parsely\REST_API\Base_Endpoint::init * @uses \Parsely\Utils\Utils::convert_endpoint_to_filter_key diff --git a/tests/Integration/RestAPI/Stats/EndpointPostTest.php b/tests/Integration/RestAPI/Stats/EndpointPostTest.php new file mode 100644 index 0000000000..acba94a64b --- /dev/null +++ b/tests/Integration/RestAPI/Stats/EndpointPostTest.php @@ -0,0 +1,674 @@ +api_controller = new Stats_Controller( $this->parsely ); + $this->endpoint = new Endpoint_Post( $this->api_controller ); + + parent::set_up(); + } + + /** + * Get the test endpoint instance. + * + * @since 3.17.0 + * + * @return Endpoint_Post + */ + public function get_endpoint(): \Parsely\REST_API\Base_Endpoint { + return $this->endpoint; + } + + /** + * Test that the route is registered. + * + * @since 3.17.0 + * + * @covers \Parsely\REST_API\Stats\Endpoint_Post::register_routes + * @uses \Parsely\Endpoints\Base_Endpoint::__construct + * @uses \Parsely\Parsely::api_secret_is_set + * @uses \Parsely\Parsely::get_managed_credentials + * @uses \Parsely\Parsely::get_options + * @uses \Parsely\Parsely::set_default_content_helper_settings_values + * @uses \Parsely\Parsely::set_default_full_metadata_in_non_posts + * @uses \Parsely\Parsely::site_id_is_set + * @uses \Parsely\Permissions::build_pch_permissions_settings_array + * @uses \Parsely\Permissions::get_user_roles_with_edit_posts_cap + * @uses \Parsely\REST_API\Base_API_Controller::__construct + * @uses \Parsely\REST_API\Base_API_Controller::get_full_namespace + * @uses \Parsely\REST_API\Base_API_Controller::get_parsely + * @uses \Parsely\REST_API\Base_API_Controller::prefix_route + * @uses \Parsely\REST_API\Base_Endpoint::__construct + * @uses \Parsely\REST_API\Base_Endpoint::get_full_endpoint + * @uses \Parsely\REST_API\Base_Endpoint::get_registered_routes + * @uses \Parsely\REST_API\Base_Endpoint::init + * @uses \Parsely\REST_API\Base_Endpoint::is_available_to_current_user + * @uses \Parsely\REST_API\Base_Endpoint::register_rest_route + * @uses \Parsely\REST_API\Base_Endpoint::validate_site_id_and_secret + * @uses \Parsely\REST_API\REST_API_Controller::get_namespace + * @uses \Parsely\REST_API\REST_API_Controller::get_version + * @uses \Parsely\REST_API\Stats\Endpoint_Post::__construct + * @uses \Parsely\REST_API\Stats\Endpoint_Post::get_endpoint_name + * @uses \Parsely\REST_API\Stats\Post_Data_Trait::get_itm_source_param_args + * @uses \Parsely\REST_API\Stats\Related_Posts_Trait::get_related_posts_param_args + * @uses \Parsely\REST_API\Stats\Stats_Controller::get_route_prefix + * @uses \Parsely\REST_API\Use_Post_ID_Parameter_Trait::register_rest_route_with_post_id + * @uses \Parsely\Utils\Utils::convert_endpoint_to_filter_key + */ + public function test_route_is_registered(): void { + $routes = rest_get_server()->get_routes(); + $registered_routes = $this->get_endpoint()->get_registered_routes(); + + // Assert that the routes are registered when the filter returns true. + foreach ( $registered_routes as $route ) { + $expected_route = $this->get_endpoint()->get_full_endpoint( $route ); + $route_data = $routes[ $expected_route ]; + self::assertArrayHasKey( $expected_route, $routes ); + + // Check that the route is associated with the GET method, since all + // the routes in this endpoint are GET routes. + self::assertArrayHasKey( 'GET', $route_data[0]['methods'] ); + } + } + + /** + * Test that the endpoint is not available if the API key is not set. + * + * @covers \Parsely\REST_API\Stats\Endpoint_Post::is_available_to_current_user + * @uses \Parsely\Endpoints\Base_Endpoint::__construct + * @uses \Parsely\Parsely::api_secret_is_set + * @uses \Parsely\Parsely::get_managed_credentials + * @uses \Parsely\Parsely::get_options + * @uses \Parsely\Parsely::set_default_content_helper_settings_values + * @uses \Parsely\Parsely::set_default_full_metadata_in_non_posts + * @uses \Parsely\Parsely::site_id_is_set + * @uses \Parsely\Permissions::build_pch_permissions_settings_array + * @uses \Parsely\Permissions::get_user_roles_with_edit_posts_cap + * @uses \Parsely\REST_API\Base_API_Controller::__construct + * @uses \Parsely\REST_API\Base_API_Controller::get_full_namespace + * @uses \Parsely\REST_API\Base_API_Controller::get_parsely + * @uses \Parsely\REST_API\Base_API_Controller::prefix_route + * @uses \Parsely\REST_API\Base_Endpoint::__construct + * @uses \Parsely\REST_API\Base_Endpoint::get_full_endpoint + * @uses \Parsely\REST_API\Base_Endpoint::init + * @uses \Parsely\REST_API\Base_Endpoint::register_rest_route + * @uses \Parsely\REST_API\Base_Endpoint::validate_site_id_and_secret + * @uses \Parsely\REST_API\REST_API_Controller::get_namespace + * @uses \Parsely\REST_API\REST_API_Controller::get_version + * @uses \Parsely\REST_API\Stats\Endpoint_Post::__construct + * @uses \Parsely\REST_API\Stats\Endpoint_Post::get_endpoint_name + * @uses \Parsely\REST_API\Stats\Endpoint_Post::register_routes + * @uses \Parsely\REST_API\Stats\Post_Data_Trait::get_itm_source_param_args + * @uses \Parsely\REST_API\Stats\Related_Posts_Trait::get_related_posts_param_args + * @uses \Parsely\REST_API\Stats\Stats_Controller::get_route_prefix + * @uses \Parsely\REST_API\Use_Post_ID_Parameter_Trait::register_rest_route_with_post_id + * @uses \Parsely\REST_API\Use_Post_ID_Parameter_Trait::validate_post_id + * @uses \Parsely\Utils\Utils::convert_endpoint_to_filter_key + */ + public function test_access_error_if_api_secret_is_not_set(): void { + $test_post_id = $this->create_test_post(); + TestCase::set_options( + array( + 'apikey' => 'test', + ) + ); + $route = $this->get_endpoint()->get_full_endpoint( '/' . $test_post_id . '/details' ); + $response = rest_get_server()->dispatch( + new WP_REST_Request( 'GET', $route ) + ); + + $error = $response->as_error(); + self::assertNotNull( $error ); + self::assertSame( 403, $response->get_status() ); + self::assertSame( 'parsely_api_secret_not_set', $error->get_error_code() ); + self::assertSame( + 'A Parse.ly API Secret must be set in site options to use this endpoint', + $error->get_error_message() + ); + } + + + /** + * Verifies forbidden error when current user doesn't have proper + * capabilities. + * + * @covers \Parsely\REST_API\Stats\Endpoint_Post::is_available_to_current_user + * @uses \Parsely\Endpoints\Base_Endpoint::__construct + * @uses \Parsely\Parsely::api_secret_is_set + * @uses \Parsely\Parsely::get_managed_credentials + * @uses \Parsely\Parsely::get_options + * @uses \Parsely\Parsely::set_default_content_helper_settings_values + * @uses \Parsely\Parsely::set_default_full_metadata_in_non_posts + * @uses \Parsely\Parsely::site_id_is_set + * @uses \Parsely\Permissions::build_pch_permissions_settings_array + * @uses \Parsely\Permissions::get_user_roles_with_edit_posts_cap + * @uses \Parsely\REST_API\Base_API_Controller::__construct + * @uses \Parsely\REST_API\Base_API_Controller::get_full_namespace + * @uses \Parsely\REST_API\Base_API_Controller::get_parsely + * @uses \Parsely\REST_API\Base_API_Controller::prefix_route + * @uses \Parsely\REST_API\Base_Endpoint::__construct + * @uses \Parsely\REST_API\Base_Endpoint::apply_capability_filters + * @uses \Parsely\REST_API\Base_Endpoint::get_default_access_capability + * @uses \Parsely\REST_API\Base_Endpoint::get_full_endpoint + * @uses \Parsely\REST_API\Base_Endpoint::init + * @uses \Parsely\REST_API\Base_Endpoint::register_rest_route + * @uses \Parsely\REST_API\Base_Endpoint::validate_site_id_and_secret + * @uses \Parsely\REST_API\REST_API_Controller::get_namespace + * @uses \Parsely\REST_API\REST_API_Controller::get_version + * @uses \Parsely\REST_API\Stats\Endpoint_Post::__construct + * @uses \Parsely\REST_API\Stats\Endpoint_Post::get_endpoint_name + * @uses \Parsely\REST_API\Stats\Endpoint_Post::register_routes + * @uses \Parsely\REST_API\Stats\Post_Data_Trait::get_itm_source_param_args + * @uses \Parsely\REST_API\Stats\Related_Posts_Trait::get_related_posts_param_args + * @uses \Parsely\REST_API\Stats\Stats_Controller::get_route_prefix + * @uses \Parsely\REST_API\Use_Post_ID_Parameter_Trait::register_rest_route_with_post_id + * @uses \Parsely\REST_API\Use_Post_ID_Parameter_Trait::validate_post_id + * @uses \Parsely\Utils\Utils::convert_endpoint_to_filter_key + */ + public function test_access_of_stats_post_endpoint_is_forbidden(): void { + $test_post_id = $this->create_test_post(); + TestCase::set_options( + array( + 'apikey' => 'test-api-key', + 'api_secret' => 'test-secret', + ) + ); + $this->set_current_user_to_contributor(); + + $route = $this->get_endpoint()->get_full_endpoint( '/' . $test_post_id . '/details' ); + $response = rest_get_server()->dispatch( + new WP_REST_Request( 'GET', $route ) + ); + /** + * Variable. + * + * @var WP_Error $error + */ + $error = $response->as_error(); + + self::assertSame( 403, $response->get_status() ); + self::assertSame( 'rest_forbidden', $error->get_error_code() ); + self::assertSame( + 'Sorry, you are not allowed to do that.', + $error->get_error_message() + ); + } + + /** + * Verifies that calls to the `stats/{post_id}/details` return the expected data, in the + * expected format. + * + * @since 3.17.0 + * + * @covers \Parsely\REST_API\Stats\Endpoint_Post::get_post_details + * @uses \Parsely\Endpoints\Base_Endpoint::__construct + * @uses \Parsely\Parsely::api_secret_is_set + * @uses \Parsely\Parsely::get_api_secret + * @uses \Parsely\Parsely::get_dash_url + * @uses \Parsely\Parsely::get_managed_credentials + * @uses \Parsely\Parsely::get_options + * @uses \Parsely\Parsely::get_site_id + * @uses \Parsely\Parsely::get_url_with_itm_source + * @uses \Parsely\Parsely::set_default_content_helper_settings_values + * @uses \Parsely\Parsely::set_default_full_metadata_in_non_posts + * @uses \Parsely\Parsely::site_id_is_set + * @uses \Parsely\Permissions::build_pch_permissions_settings_array + * @uses \Parsely\Permissions::get_user_roles_with_edit_posts_cap + * @uses \Parsely\REST_API\Base_API_Controller::__construct + * @uses \Parsely\REST_API\Base_API_Controller::get_full_namespace + * @uses \Parsely\REST_API\Base_API_Controller::get_parsely + * @uses \Parsely\REST_API\Base_API_Controller::prefix_route + * @uses \Parsely\REST_API\Base_Endpoint::__construct + * @uses \Parsely\REST_API\Base_Endpoint::apply_capability_filters + * @uses \Parsely\REST_API\Base_Endpoint::get_default_access_capability + * @uses \Parsely\REST_API\Base_Endpoint::get_full_endpoint + * @uses \Parsely\REST_API\Base_Endpoint::init + * @uses \Parsely\REST_API\Base_Endpoint::is_available_to_current_user + * @uses \Parsely\REST_API\Base_Endpoint::register_rest_route + * @uses \Parsely\REST_API\Base_Endpoint::validate_site_id_and_secret + * @uses \Parsely\REST_API\REST_API_Controller::get_namespace + * @uses \Parsely\REST_API\REST_API_Controller::get_version + * @uses \Parsely\REST_API\Stats\Endpoint_Post::__construct + * @uses \Parsely\REST_API\Stats\Endpoint_Post::get_endpoint_name + * @uses \Parsely\REST_API\Stats\Endpoint_Post::register_routes + * @uses \Parsely\REST_API\Stats\Post_Data_Trait::extract_post_data + * @uses \Parsely\REST_API\Stats\Post_Data_Trait::get_itm_source_param_args + * @uses \Parsely\REST_API\Stats\Post_Data_Trait::set_itm_source_from_request + * @uses \Parsely\REST_API\Stats\Related_Posts_Trait::get_related_posts_param_args + * @uses \Parsely\REST_API\Stats\Stats_Controller::get_route_prefix + * @uses \Parsely\REST_API\Use_Post_ID_Parameter_Trait::register_rest_route_with_post_id + * @uses \Parsely\REST_API\Use_Post_ID_Parameter_Trait::validate_post_id + * @uses \Parsely\RemoteAPI\Base_Endpoint_Remote::get_api_url + * @uses \Parsely\RemoteAPI\Base_Endpoint_Remote::get_items + * @uses \Parsely\RemoteAPI\Base_Endpoint_Remote::get_request_options + * @uses \Parsely\RemoteAPI\Base_Endpoint_Remote::validate_required_constraints + * @uses \Parsely\Utils\Utils::convert_endpoint_to_filter_key + * @uses \Parsely\Utils\Utils::get_formatted_duration + * @uses \Parsely\Utils\Utils::parsely_is_https_supported + */ + public function test_get_details(): void { + $test_post_id = $this->create_test_post(); + $route = $this->get_endpoint()->get_full_endpoint( '/' . $test_post_id . '/details' ); + + TestCase::set_options( + array( + 'apikey' => 'example.com', + 'api_secret' => 'test-secret', + ) + ); + $this->set_current_user_to_admin(); + + $dispatched = 0; + + add_filter( + 'pre_http_request', + function () use ( &$dispatched ): array { + $dispatched++; + return array( + 'body' => ' + {"data":[{ + "avg_engaged": 1.911, + "metrics": { + "views": 2158, + "visitors": 1537 + }, + "url": "https://example.com" + }]} + ', + ); + } + ); + + $response = rest_get_server()->dispatch( new WP_REST_Request( 'GET', $route ) ); + + /** + * The response data. + * + * @var array $response_data + */ + $response_data = $response->get_data(); + + self::assertSame( 1, $dispatched ); + self::assertSame( 200, $response->get_status() ); + self::assertEquals( + array( + array( + 'avgEngaged' => '1:55', + 'dashUrl' => Parsely::DASHBOARD_BASE_URL . '/example.com/find?url=https%3A%2F%2Fexample.com', + 'id' => 'https://example.com', + 'postId' => 0, + 'url' => 'https://example.com', + 'views' => '2,158', + 'visitors' => '1,537', + 'rawUrl' => 'https://example.com', + ), + ), + $response_data['data'] + ); + } + + /** + * Verifies that calls to the `stats/{post_id}/referrers` return the expected data, in the + * expected format. + * + * @since 3.17.0 + * + * @covers \Parsely\REST_API\Stats\Endpoint_Post::get_post_referrers + * @uses \Parsely\Endpoints\Base_Endpoint::__construct + * @uses \Parsely\Parsely::api_secret_is_set + * @uses \Parsely\Parsely::get_api_secret + * @uses \Parsely\Parsely::get_managed_credentials + * @uses \Parsely\Parsely::get_options + * @uses \Parsely\Parsely::get_site_id + * @uses \Parsely\Parsely::set_default_content_helper_settings_values + * @uses \Parsely\Parsely::set_default_full_metadata_in_non_posts + * @uses \Parsely\Parsely::site_id_is_set + * @uses \Parsely\Permissions::build_pch_permissions_settings_array + * @uses \Parsely\Permissions::get_user_roles_with_edit_posts_cap + * @uses \Parsely\REST_API\Base_API_Controller::__construct + * @uses \Parsely\REST_API\Base_API_Controller::get_full_namespace + * @uses \Parsely\REST_API\Base_API_Controller::get_parsely + * @uses \Parsely\REST_API\Base_API_Controller::prefix_route + * @uses \Parsely\REST_API\Base_Endpoint::__construct + * @uses \Parsely\REST_API\Base_Endpoint::apply_capability_filters + * @uses \Parsely\REST_API\Base_Endpoint::get_default_access_capability + * @uses \Parsely\REST_API\Base_Endpoint::get_full_endpoint + * @uses \Parsely\REST_API\Base_Endpoint::init + * @uses \Parsely\REST_API\Base_Endpoint::is_available_to_current_user + * @uses \Parsely\REST_API\Base_Endpoint::register_rest_route + * @uses \Parsely\REST_API\Base_Endpoint::validate_site_id_and_secret + * @uses \Parsely\REST_API\REST_API_Controller::get_namespace + * @uses \Parsely\REST_API\REST_API_Controller::get_version + * @uses \Parsely\REST_API\Stats\Endpoint_Post::__construct + * @uses \Parsely\REST_API\Stats\Endpoint_Post::generate_referrer_types_data + * @uses \Parsely\REST_API\Stats\Endpoint_Post::generate_referrers_data + * @uses \Parsely\REST_API\Stats\Endpoint_Post::get_endpoint_name + * @uses \Parsely\REST_API\Stats\Endpoint_Post::get_i18n_percentage + * @uses \Parsely\REST_API\Stats\Endpoint_Post::register_routes + * @uses \Parsely\REST_API\Stats\Post_Data_Trait::get_itm_source_param_args + * @uses \Parsely\REST_API\Stats\Post_Data_Trait::set_itm_source_from_request + * @uses \Parsely\REST_API\Stats\Related_Posts_Trait::get_related_posts_param_args + * @uses \Parsely\REST_API\Stats\Stats_Controller::get_route_prefix + * @uses \Parsely\REST_API\Use_Post_ID_Parameter_Trait::register_rest_route_with_post_id + * @uses \Parsely\REST_API\Use_Post_ID_Parameter_Trait::validate_post_id + * @uses \Parsely\RemoteAPI\Base_Endpoint_Remote::get_api_url + * @uses \Parsely\RemoteAPI\Base_Endpoint_Remote::get_items + * @uses \Parsely\RemoteAPI\Base_Endpoint_Remote::get_request_options + * @uses \Parsely\RemoteAPI\Base_Endpoint_Remote::validate_required_constraints + * @uses \Parsely\Utils\Utils::convert_endpoint_to_filter_key + * @uses \Parsely\Utils\Utils::convert_to_positive_integer + */ + public function test_get_referrers(): void { + $test_post_id = $this->create_test_post(); + $route = $this->get_endpoint()->get_full_endpoint( '/' . $test_post_id . '/referrers' ); + + TestCase::set_options( + array( + 'apikey' => 'example.com', + 'api_secret' => 'test-secret', + ) + ); + $this->set_current_user_to_admin(); + + $dispatched = 0; + + add_filter( + 'pre_http_request', + function () use ( &$dispatched ): array { + $dispatched++; + return array( + 'body' => '{"data":[ + { + "metrics": {"referrers_views": 1500}, + "name": "google", + "type": "search" + }, + { + "metrics": {"referrers_views": 100}, + "name": "blog.parse.ly", + "type": "internal" + }, + { + "metrics": {"referrers_views": 50}, + "name": "bing", + "type": "search" + }, + { + "metrics": {"referrers_views": 30}, + "name": "facebook.com", + "type": "social" + }, + { + "metrics": {"referrers_views": 10}, + "name": "okt.to", + "type": "other" + }, + { + "metrics": {"referrers_views": 10}, + "name": "yandex", + "type": "search" + }, + { + "metrics": {"referrers_views": 10}, + "name": "parse.ly", + "type": "internal" + }, + { + "metrics": {"referrers_views": 10}, + "name": "yahoo!", + "type": "search" + }, + { + "metrics": {"referrers_views": 5}, + "name": "site1.com", + "type": "other" + }, + { + "metrics": {"referrers_views": 5}, + "name": "link.site2.com", + "type": "other" + } + ]}', + ); + } + ); + + $expected_top = (object) array( + 'direct' => (object) array( + 'views' => '770', + 'viewsPercentage' => '30.80', + 'datasetViewsPercentage' => '31.43', + ), + 'google' => (object) array( + 'views' => '1,500', + 'viewsPercentage' => '60.00', + 'datasetViewsPercentage' => '61.22', + ), + 'blog.parse.ly' => (object) array( + 'views' => '100', + 'viewsPercentage' => '4.00', + 'datasetViewsPercentage' => '4.08', + ), + 'bing' => (object) array( + 'views' => '50', + 'viewsPercentage' => '2.00', + 'datasetViewsPercentage' => '2.04', + ), + 'facebook.com' => (object) array( + 'views' => '30', + 'viewsPercentage' => '1.20', + 'datasetViewsPercentage' => '1.22', + ), + 'totals' => (object) array( + 'views' => '2,450', + 'viewsPercentage' => '98.00', + 'datasetViewsPercentage' => '100.00', + ), + ); + + $expected_types = (object) array( + 'social' => (object) array( + 'views' => '30', + 'viewsPercentage' => '1.20', + ), + 'search' => (object) array( + 'views' => '1,570', + 'viewsPercentage' => '62.80', + ), + 'other' => (object) array( + 'views' => '20', + 'viewsPercentage' => '0.80', + ), + 'internal' => (object) array( + 'views' => '110', + 'viewsPercentage' => '4.40', + ), + 'direct' => (object) array( + 'views' => '770', + 'viewsPercentage' => '30.80', + ), + 'totals' => (object) array( + 'views' => '2,500', + 'viewsPercentage' => '100.00', + ), + ); + + $request = new WP_REST_Request( 'GET', $route ); + $request->set_param( 'total_views', '2,500' ); + + $response = rest_get_server()->dispatch( $request ); + + /** + * The response data. + * + * @var array $response_data + */ + $response_data = $response->get_data(); + + self::assertSame( 1, $dispatched ); + self::assertSame( 200, $response->get_status() ); + self::assertEquals( + array( + 'top' => $expected_top, + 'types' => $expected_types, + ), + $response_data['data'] + ); + } + + + /** + * Verifies that calls to the `stats/{post_id}/related` return the expected data, in the + * expected format. + * + * @since 3.17.0 + * + * @covers \Parsely\REST_API\Stats\Endpoint_Post::get_related_posts + * @uses \Parsely\Endpoints\Base_Endpoint::__construct + * @uses \Parsely\Parsely::api_secret_is_set + * @uses \Parsely\Parsely::get_api_secret + * @uses \Parsely\Parsely::get_managed_credentials + * @uses \Parsely\Parsely::get_options + * @uses \Parsely\Parsely::get_site_id + * @uses \Parsely\Parsely::get_url_with_itm_source + * @uses \Parsely\Parsely::set_default_content_helper_settings_values + * @uses \Parsely\Parsely::set_default_full_metadata_in_non_posts + * @uses \Parsely\Parsely::site_id_is_set + * @uses \Parsely\Permissions::build_pch_permissions_settings_array + * @uses \Parsely\Permissions::get_user_roles_with_edit_posts_cap + * @uses \Parsely\REST_API\Base_API_Controller::__construct + * @uses \Parsely\REST_API\Base_API_Controller::get_full_namespace + * @uses \Parsely\REST_API\Base_API_Controller::get_parsely + * @uses \Parsely\REST_API\Base_API_Controller::prefix_route + * @uses \Parsely\REST_API\Base_Endpoint::__construct + * @uses \Parsely\REST_API\Base_Endpoint::apply_capability_filters + * @uses \Parsely\REST_API\Base_Endpoint::get_default_access_capability + * @uses \Parsely\REST_API\Base_Endpoint::get_full_endpoint + * @uses \Parsely\REST_API\Base_Endpoint::init + * @uses \Parsely\REST_API\Base_Endpoint::is_available_to_current_user + * @uses \Parsely\REST_API\Base_Endpoint::register_rest_route + * @uses \Parsely\REST_API\Base_Endpoint::validate_site_id_and_secret + * @uses \Parsely\REST_API\REST_API_Controller::get_namespace + * @uses \Parsely\REST_API\REST_API_Controller::get_version + * @uses \Parsely\REST_API\Stats\Endpoint_Post::__construct + * @uses \Parsely\REST_API\Stats\Endpoint_Post::get_endpoint_name + * @uses \Parsely\REST_API\Stats\Endpoint_Post::register_routes + * @uses \Parsely\REST_API\Stats\Post_Data_Trait::get_itm_source_param_args + * @uses \Parsely\REST_API\Stats\Post_Data_Trait::set_itm_source_from_request + * @uses \Parsely\REST_API\Stats\Related_Posts_Trait::get_related_posts_of_url + * @uses \Parsely\REST_API\Stats\Related_Posts_Trait::get_related_posts_param_args + * @uses \Parsely\REST_API\Stats\Stats_Controller::get_route_prefix + * @uses \Parsely\REST_API\Use_Post_ID_Parameter_Trait::register_rest_route_with_post_id + * @uses \Parsely\REST_API\Use_Post_ID_Parameter_Trait::validate_post_id + * @uses \Parsely\RemoteAPI\Base_Endpoint_Remote::get_api_url + * @uses \Parsely\RemoteAPI\Base_Endpoint_Remote::get_items + * @uses \Parsely\RemoteAPI\Base_Endpoint_Remote::get_request_options + * @uses \Parsely\RemoteAPI\Base_Endpoint_Remote::validate_required_constraints + * @uses \Parsely\Utils\Utils::convert_endpoint_to_filter_key + */ + public function test_get_related_posts(): void { + $test_post_id = $this->create_test_post(); + $route = $this->get_endpoint()->get_full_endpoint( '/' . $test_post_id . '/related' ); + + TestCase::set_options( + array( + 'apikey' => 'example.com', + 'api_secret' => 'test-secret', + ) + ); + $this->set_current_user_to_admin(); + + $dispatched = 0; + + add_filter( + 'pre_http_request', + function () use ( &$dispatched ): array { + $dispatched++; + return array( + 'body' => '{"data":[ + { + "image_url":"https:\/\/example.com\/img.png", + "thumb_url_medium":"https:\/\/example.com\/thumb.png", + "title":"something", + "url":"https:\/\/example.com" + }, + { + "image_url":"https:\/\/example.com\/img2.png", + "thumb_url_medium":"https:\/\/example.com\/thumb2.png", + "title":"something2", + "url":"https:\/\/example.com\/2" + } + ]}', + ); + } + ); + + $response = rest_get_server()->dispatch( new WP_REST_Request( 'GET', $route ) ); + /** + * The response data. + * + * @var array $response_data + */ + $response_data = $response->get_data(); + + self::assertSame( 1, $dispatched ); + self::assertSame( 200, $response->get_status() ); + self::assertEquals( + array( + (object) array( + 'image_url' => 'https://example.com/img.png', + 'thumb_url_medium' => 'https://example.com/thumb.png', + 'title' => 'something', + 'url' => 'https://example.com', + ), + (object) array( + 'image_url' => 'https://example.com/img2.png', + 'thumb_url_medium' => 'https://example.com/thumb2.png', + 'title' => 'something2', + 'url' => 'https://example.com/2', + ), + ), + $response_data['data'] + ); + } +} diff --git a/tests/Integration/RestAPI/Stats/EndpointPostsTest.php b/tests/Integration/RestAPI/Stats/EndpointPostsTest.php new file mode 100644 index 0000000000..0cecdd1cb5 --- /dev/null +++ b/tests/Integration/RestAPI/Stats/EndpointPostsTest.php @@ -0,0 +1,347 @@ +api_controller = new Stats_Controller( $this->parsely ); + $this->endpoint = new Endpoint_Posts( $this->api_controller ); + + parent::set_up(); + } + + /** + * Get the test endpoint instance. + * + * @since 3.17.0 + * + * @return Endpoint_Posts + */ + public function get_endpoint(): \Parsely\REST_API\Base_Endpoint { + return $this->endpoint; + } + + /** + * Test the route is registered. + * + * @since 3.17.0 + * + * @covers \Parsely\REST_API\Stats\Endpoint_Posts::register_routes + * @uses \Parsely\Endpoints\Base_Endpoint::__construct + * @uses \Parsely\Parsely::api_secret_is_set + * @uses \Parsely\Parsely::get_managed_credentials + * @uses \Parsely\Parsely::get_options + * @uses \Parsely\Parsely::set_default_content_helper_settings_values + * @uses \Parsely\Parsely::set_default_full_metadata_in_non_posts + * @uses \Parsely\Parsely::site_id_is_set + * @uses \Parsely\Permissions::build_pch_permissions_settings_array + * @uses \Parsely\Permissions::get_user_roles_with_edit_posts_cap + * @uses \Parsely\REST_API\Base_API_Controller::__construct + * @uses \Parsely\REST_API\Base_API_Controller::get_full_namespace + * @uses \Parsely\REST_API\Base_API_Controller::get_parsely + * @uses \Parsely\REST_API\Base_API_Controller::prefix_route + * @uses \Parsely\REST_API\Base_Endpoint::__construct + * @uses \Parsely\REST_API\Base_Endpoint::get_full_endpoint + * @uses \Parsely\REST_API\Base_Endpoint::init + * @uses \Parsely\REST_API\Base_Endpoint::is_available_to_current_user + * @uses \Parsely\REST_API\Base_Endpoint::register_rest_route + * @uses \Parsely\REST_API\Base_Endpoint::validate_site_id_and_secret + * @uses \Parsely\REST_API\REST_API_Controller::get_namespace + * @uses \Parsely\REST_API\REST_API_Controller::get_version + * @uses \Parsely\REST_API\Stats\Stats_Controller::get_route_prefix + * @uses \Parsely\Utils\Utils::convert_endpoint_to_filter_key + */ + public function test_route_is_registered(): void { + $routes = rest_get_server()->get_routes(); + + // Check that the excerpt-generator/generate route is registered. + $expected_route = $this->get_endpoint()->get_full_endpoint( '/' ); + self::assertArrayHasKey( $expected_route, $routes ); + + // Check that the route is associated with the POST method. + $route_data = $routes[ $expected_route ]; + self::assertArrayHasKey( 'GET', $route_data[0]['methods'] ); + } + + /** + * Test that the endpoint is not available if the API key is not set. + * + * @covers \Parsely\REST_API\Stats\Endpoint_Posts::is_available_to_current_user + * @uses \Parsely\Endpoints\Base_Endpoint::__construct + * @uses \Parsely\Parsely::api_secret_is_set + * @uses \Parsely\Parsely::get_managed_credentials + * @uses \Parsely\Parsely::get_options + * @uses \Parsely\Parsely::set_default_content_helper_settings_values + * @uses \Parsely\Parsely::set_default_full_metadata_in_non_posts + * @uses \Parsely\Parsely::site_id_is_set + * @uses \Parsely\Permissions::build_pch_permissions_settings_array + * @uses \Parsely\Permissions::get_user_roles_with_edit_posts_cap + * @uses \Parsely\REST_API\Base_API_Controller::__construct + * @uses \Parsely\REST_API\Base_API_Controller::get_full_namespace + * @uses \Parsely\REST_API\Base_API_Controller::get_parsely + * @uses \Parsely\REST_API\Base_API_Controller::prefix_route + * @uses \Parsely\REST_API\Base_Endpoint::__construct + * @uses \Parsely\REST_API\Base_Endpoint::get_full_endpoint + * @uses \Parsely\REST_API\Base_Endpoint::init + * @uses \Parsely\REST_API\Base_Endpoint::register_rest_route + * @uses \Parsely\REST_API\Base_Endpoint::validate_site_id_and_secret + * @uses \Parsely\REST_API\REST_API_Controller::get_namespace + * @uses \Parsely\REST_API\REST_API_Controller::get_version + * @uses \Parsely\REST_API\Stats\Stats_Controller::get_route_prefix + * @uses \Parsely\Utils\Utils::convert_endpoint_to_filter_key + */ + public function test_access_error_if_api_secret_is_not_set(): void { + TestCase::set_options( + array( + 'apikey' => 'test', + ) + ); + $route = $this->get_endpoint()->get_full_endpoint( '/' ); + $response = rest_get_server()->dispatch( + new WP_REST_Request( 'GET', $route ) + ); + + $error = $response->as_error(); + self::assertNotNull( $error ); + self::assertSame( 403, $response->get_status() ); + self::assertSame( 'parsely_api_secret_not_set', $error->get_error_code() ); + self::assertSame( + 'A Parse.ly API Secret must be set in site options to use this endpoint', + $error->get_error_message() + ); + } + + + /** + * Verifies forbidden error when current user doesn't have proper + * capabilities. + * + * @covers \Parsely\REST_API\Stats\Endpoint_Posts::is_available_to_current_user + * @uses \Parsely\Endpoints\Base_Endpoint::__construct + * @uses \Parsely\Parsely::api_secret_is_set + * @uses \Parsely\Parsely::get_managed_credentials + * @uses \Parsely\Parsely::get_options + * @uses \Parsely\Parsely::set_default_content_helper_settings_values + * @uses \Parsely\Parsely::set_default_full_metadata_in_non_posts + * @uses \Parsely\Parsely::site_id_is_set + * @uses \Parsely\Permissions::build_pch_permissions_settings_array + * @uses \Parsely\Permissions::get_user_roles_with_edit_posts_cap + * @uses \Parsely\REST_API\Base_API_Controller::__construct + * @uses \Parsely\REST_API\Base_API_Controller::get_full_namespace + * @uses \Parsely\REST_API\Base_API_Controller::get_parsely + * @uses \Parsely\REST_API\Base_API_Controller::prefix_route + * @uses \Parsely\REST_API\Base_Endpoint::__construct + * @uses \Parsely\REST_API\Base_Endpoint::apply_capability_filters + * @uses \Parsely\REST_API\Base_Endpoint::get_default_access_capability + * @uses \Parsely\REST_API\Base_Endpoint::get_full_endpoint + * @uses \Parsely\REST_API\Base_Endpoint::init + * @uses \Parsely\REST_API\Base_Endpoint::register_rest_route + * @uses \Parsely\REST_API\Base_Endpoint::validate_site_id_and_secret + * @uses \Parsely\REST_API\REST_API_Controller::get_namespace + * @uses \Parsely\REST_API\REST_API_Controller::get_version + * @uses \Parsely\REST_API\Stats\Stats_Controller::get_route_prefix + * @uses \Parsely\Utils\Utils::convert_endpoint_to_filter_key + */ + public function test_access_of_stats_posts_endpoint_is_forbidden(): void { + TestCase::set_options( + array( + 'apikey' => 'test-api-key', + 'api_secret' => 'test-secret', + ) + ); + $this->set_current_user_to_contributor(); + + $route = $this->get_endpoint()->get_full_endpoint( '/' ); + $response = rest_get_server()->dispatch( + new WP_REST_Request( 'GET', $route ) + ); + /** + * Variable. + * + * @var WP_Error $error + */ + $error = $response->as_error(); + + self::assertSame( 403, $response->get_status() ); + self::assertSame( 'rest_forbidden', $error->get_error_code() ); + self::assertSame( + 'Sorry, you are not allowed to do that.', + $error->get_error_message() + ); + } + + /** + * Verifies that calls to the endpoint return the expected data, in the + * expected format. + * + * @covers \Parsely\REST_API\Stats\Endpoint_Posts::get_posts + * @uses \Parsely\Endpoints\Base_Endpoint::__construct + * @uses \Parsely\Parsely::api_secret_is_set + * @uses \Parsely\Parsely::get_api_secret + * @uses \Parsely\Parsely::get_dash_url + * @uses \Parsely\Parsely::get_managed_credentials + * @uses \Parsely\Parsely::get_options + * @uses \Parsely\Parsely::get_site_id + * @uses \Parsely\Parsely::get_url_with_itm_source + * @uses \Parsely\Parsely::set_default_content_helper_settings_values + * @uses \Parsely\Parsely::set_default_full_metadata_in_non_posts + * @uses \Parsely\Parsely::site_id_is_set + * @uses \Parsely\Permissions::build_pch_permissions_settings_array + * @uses \Parsely\Permissions::get_user_roles_with_edit_posts_cap + * @uses \Parsely\REST_API\Base_API_Controller::__construct + * @uses \Parsely\REST_API\Base_API_Controller::get_full_namespace + * @uses \Parsely\REST_API\Base_API_Controller::get_parsely + * @uses \Parsely\REST_API\Base_API_Controller::prefix_route + * @uses \Parsely\REST_API\Base_Endpoint::__construct + * @uses \Parsely\REST_API\Base_Endpoint::apply_capability_filters + * @uses \Parsely\REST_API\Base_Endpoint::get_default_access_capability + * @uses \Parsely\REST_API\Base_Endpoint::init + * @uses \Parsely\REST_API\Base_Endpoint::is_available_to_current_user + * @uses \Parsely\REST_API\Base_Endpoint::register_rest_route + * @uses \Parsely\REST_API\Base_Endpoint::validate_site_id_and_secret + * @uses \Parsely\REST_API\REST_API_Controller::get_namespace + * @uses \Parsely\REST_API\REST_API_Controller::get_version + * @uses \Parsely\REST_API\Stats\Stats_Controller::get_route_prefix + * @uses \Parsely\RemoteAPI\Analytics_Posts_API::get_request_options + * @uses \Parsely\RemoteAPI\Base_Endpoint_Remote::get_api_url + * @uses \Parsely\RemoteAPI\Base_Endpoint_Remote::get_items + * @uses \Parsely\RemoteAPI\Base_Endpoint_Remote::validate_required_constraints + * @uses \Parsely\Utils\Utils::convert_endpoint_to_filter_key + * @uses \Parsely\Utils\Utils::get_date_format + * @uses \Parsely\Utils\Utils::parsely_is_https_supported + */ + public function test_get_posts(): void { + TestCase::set_options( + array( + 'apikey' => 'example.com', + 'api_secret' => 'test-secret', + ) + ); + $this->set_current_user_to_admin(); + + $dispatched = 0; + $date_format = Utils::get_date_format(); + + add_filter( + 'pre_http_request', + function () use ( &$dispatched ): array { + $dispatched++; + + return array( + 'body' => '{"data":[ + { + "author": "Aakash Shah", + "metrics": {"views": 142}, + "pub_date": "2020-04-06T13:30:58", + "thumb_url_medium": "https://images.parsely.com/XCmTXuOf8yVbUYTxj2abQ4RSDkM=/85x85/smart/https%3A//blog.parse.ly/wp-content/uploads/2021/06/Web-Analytics-Tool.png%3Fw%3D150%26h%3D150%26crop%3D1", + "title": "9 Types of Web Analytics Tools \u2014 And How to Know Which Ones You Really Need", + "url": "https://blog.parse.ly/web-analytics-software-tools/?itm_source=parsely-api" + }, + { + "author": "Stephanie Schwartz and Andrew Butler", + "metrics": {"views": 40}, + "pub_date": "2021-04-30T20:30:24", + "thumb_url_medium": "https://images.parsely.com/ap3YSufqxnLpz6zzQshoks3snXI=/85x85/smart/https%3A//blog.parse.ly/wp-content/uploads/2021/05/pexels-brett-jordan-998501-1024x768-2.jpeg%3Fw%3D150%26h%3D150%26crop%3D1", + "title": "5 Tagging Best Practices For Getting the Most Out of Your Content Strategy", + "url": "https://blog.parse.ly/5-tagging-best-practices-content-strategy/?itm_source=parsely-api" + } + ]}', + ); + } + ); + + $rest_request = new WP_REST_Request( 'GET', '/wp-parsely/v2/stats/posts' ); + $rest_request->set_param( 'itm_source', 'wp-parsely-content-helper' ); + + $response = rest_get_server()->dispatch( $rest_request ); + + /** + * The response data. + * + * @var array $response_data + */ + $response_data = $response->get_data(); + + self::assertSame( 1, $dispatched ); + self::assertSame( 200, $response->get_status() ); + self::assertEquals( + array( + array( + 'author' => 'Aakash Shah', + 'date' => wp_date( $date_format, strtotime( '2020-04-06T13:30:58' ) ), + 'id' => 'https://blog.parse.ly/web-analytics-software-tools/', + 'dashUrl' => PARSELY::DASHBOARD_BASE_URL . '/example.com/find?url=https%3A%2F%2Fblog.parse.ly%2Fweb-analytics-software-tools%2F', + 'thumbnailUrl' => 'https://images.parsely.com/XCmTXuOf8yVbUYTxj2abQ4RSDkM=/85x85/smart/https%3A//blog.parse.ly/wp-content/uploads/2021/06/Web-Analytics-Tool.png%3Fw%3D150%26h%3D150%26crop%3D1', + 'title' => '9 Types of Web Analytics Tools — And How to Know Which Ones You Really Need', + 'url' => 'https://blog.parse.ly/web-analytics-software-tools/?itm_source=wp-parsely-content-helper', + 'views' => '142', + 'postId' => 0, + 'rawUrl' => 'https://blog.parse.ly/web-analytics-software-tools/', + ), + array( + 'author' => 'Stephanie Schwartz and Andrew Butler', + 'date' => wp_date( $date_format, strtotime( '2021-04-30T20:30:24' ) ), + 'id' => 'https://blog.parse.ly/5-tagging-best-practices-content-strategy/', + 'dashUrl' => PARSELY::DASHBOARD_BASE_URL . '/example.com/find?url=https%3A%2F%2Fblog.parse.ly%2F5-tagging-best-practices-content-strategy%2F', + 'thumbnailUrl' => 'https://images.parsely.com/ap3YSufqxnLpz6zzQshoks3snXI=/85x85/smart/https%3A//blog.parse.ly/wp-content/uploads/2021/05/pexels-brett-jordan-998501-1024x768-2.jpeg%3Fw%3D150%26h%3D150%26crop%3D1', + 'title' => '5 Tagging Best Practices For Getting the Most Out of Your Content Strategy', + 'url' => 'https://blog.parse.ly/5-tagging-best-practices-content-strategy/?itm_source=wp-parsely-content-helper', + 'views' => '40', + 'postId' => 0, + 'rawUrl' => 'https://blog.parse.ly/5-tagging-best-practices-content-strategy/', + ), + ), + $response_data['data'] + ); + + self::assertEquals( + array( + 'limit' => 5, + 'sort' => 'views', + 'page' => 1, + 'itm_source' => 'wp-parsely-content-helper', + ), + $response_data['params'] + ); + } +} diff --git a/tests/Integration/RestAPI/Stats/EndpointRelatedTest.php b/tests/Integration/RestAPI/Stats/EndpointRelatedTest.php new file mode 100644 index 0000000000..463886c852 --- /dev/null +++ b/tests/Integration/RestAPI/Stats/EndpointRelatedTest.php @@ -0,0 +1,316 @@ +api_controller = new Stats_Controller( $this->parsely ); + $this->endpoint = new Endpoint_Related( $this->api_controller ); + + parent::set_up(); + } + + + /** + * Get the test endpoint instance. + * + * @since 3.17.0 + * + * @return Endpoint_Related + */ + public function get_endpoint(): \Parsely\REST_API\Base_Endpoint { + return $this->endpoint; + } + + /** + * Test the route is registered. + * + * @since 3.17.0 + * + * @covers \Parsely\REST_API\Stats\Endpoint_Related::register_routes + * @uses \Parsely\Endpoints\Base_Endpoint::__construct + * @uses \Parsely\REST_API\Base_API_Controller::__construct + * @uses \Parsely\REST_API\Base_API_Controller::get_full_namespace + * @uses \Parsely\REST_API\Base_API_Controller::get_parsely + * @uses \Parsely\REST_API\Base_API_Controller::prefix_route + * @uses \Parsely\REST_API\Base_Endpoint::__construct + * @uses \Parsely\REST_API\Base_Endpoint::get_full_endpoint + * @uses \Parsely\REST_API\Base_Endpoint::init + * @uses \Parsely\REST_API\Base_Endpoint::register_rest_route + * @uses \Parsely\REST_API\REST_API_Controller::get_namespace + * @uses \Parsely\REST_API\REST_API_Controller::get_version + * @uses \Parsely\REST_API\Stats\Endpoint_Related::__construct + * @uses \Parsely\REST_API\Stats\Endpoint_Related::get_endpoint_name + * @uses \Parsely\REST_API\Stats\Endpoint_Related::is_available_to_current_user + * @uses \Parsely\REST_API\Stats\Post_Data_Trait::get_itm_source_param_args + * @uses \Parsely\REST_API\Stats\Related_Posts_Trait::get_related_posts_param_args + * @uses \Parsely\REST_API\Stats\Stats_Controller::get_route_prefix + * @uses \Parsely\Utils\Utils::convert_endpoint_to_filter_key + */ + public function test_route_is_registered(): void { + $routes = rest_get_server()->get_routes(); + + // Check that the excerpt-generator/generate route is registered. + $expected_route = $this->get_endpoint()->get_full_endpoint( '/' ); + self::assertArrayHasKey( $expected_route, $routes ); + + // Check that the route is associated with the POST method. + $route_data = $routes[ $expected_route ]; + self::assertArrayHasKey( 'GET', $route_data[0]['methods'] ); + } + + /** + * Test that the endpoint is available to everyone, even if they are not logged in. + * + * @since 3.17.0 + * + * @covers \Parsely\REST_API\Stats\Endpoint_Related::is_available_to_current_user + * @uses \Parsely\Endpoints\Base_Endpoint::__construct + * @uses \Parsely\Parsely::api_secret_is_set + * @uses \Parsely\Parsely::get_api_secret + * @uses \Parsely\Parsely::get_managed_credentials + * @uses \Parsely\Parsely::get_options + * @uses \Parsely\Parsely::get_site_id + * @uses \Parsely\Parsely::get_url_with_itm_source + * @uses \Parsely\Parsely::set_default_content_helper_settings_values + * @uses \Parsely\Parsely::set_default_full_metadata_in_non_posts + * @uses \Parsely\Parsely::site_id_is_set + * @uses \Parsely\Permissions::build_pch_permissions_settings_array + * @uses \Parsely\Permissions::get_user_roles_with_edit_posts_cap + * @uses \Parsely\REST_API\Base_API_Controller::__construct + * @uses \Parsely\REST_API\Base_API_Controller::get_full_namespace + * @uses \Parsely\REST_API\Base_API_Controller::get_parsely + * @uses \Parsely\REST_API\Base_API_Controller::prefix_route + * @uses \Parsely\REST_API\Base_Endpoint::__construct + * @uses \Parsely\REST_API\Base_Endpoint::get_full_endpoint + * @uses \Parsely\REST_API\Base_Endpoint::init + * @uses \Parsely\REST_API\Base_Endpoint::register_rest_route + * @uses \Parsely\REST_API\REST_API_Controller::get_namespace + * @uses \Parsely\REST_API\REST_API_Controller::get_version + * @uses \Parsely\REST_API\Stats\Endpoint_Related::__construct + * @uses \Parsely\REST_API\Stats\Endpoint_Related::get_endpoint_name + * @uses \Parsely\REST_API\Stats\Endpoint_Related::get_related_posts + * @uses \Parsely\REST_API\Stats\Endpoint_Related::register_routes + * @uses \Parsely\REST_API\Stats\Post_Data_Trait::get_itm_source_param_args + * @uses \Parsely\REST_API\Stats\Post_Data_Trait::set_itm_source_from_request + * @uses \Parsely\REST_API\Stats\Related_Posts_Trait::get_related_posts_of_url + * @uses \Parsely\REST_API\Stats\Related_Posts_Trait::get_related_posts_param_args + * @uses \Parsely\REST_API\Stats\Stats_Controller::get_route_prefix + * @uses \Parsely\RemoteAPI\Base_Endpoint_Remote::get_api_url + * @uses \Parsely\RemoteAPI\Base_Endpoint_Remote::get_items + * @uses \Parsely\RemoteAPI\Base_Endpoint_Remote::get_request_options + * @uses \Parsely\RemoteAPI\Base_Endpoint_Remote::validate_required_constraints + * @uses \Parsely\Utils\Utils::convert_endpoint_to_filter_key + */ + public function test_access_of_related_posts_is_available_to_everyone(): void { + TestCase::set_options( + array( + 'apikey' => 'test-api-key', + 'api_secret' => 'test-secret', + ) + ); + wp_set_current_user( 0 ); + + $dispatched = 0; + $this->mock_api_response( $dispatched ); + + $route = $this->get_endpoint()->get_full_endpoint( '/' ); + $request = new WP_REST_Request( 'GET', $route ); + $request->set_param( 'url', 'https://example.com/a-post' ); + $response = rest_get_server()->dispatch( $request ); + + self::assertEquals( 1, $dispatched ); + self::assertSame( 200, $response->get_status() ); + } + + /** + * Mock the API response of the Parse.ly API. + * + * @since 3.17.0 + * + * @param int &$dispatched The number of times the API was dispatched. + */ + private function mock_api_response( int &$dispatched ): void { + add_filter( + 'pre_http_request', + function () use ( &$dispatched ): array { + $dispatched++; + return array( + 'body' => '{"data":[ + { + "image_url":"https:\/\/example.com\/img.png", + "thumb_url_medium":"https:\/\/example.com\/thumb.png", + "title":"something", + "url":"https:\/\/example.com" + }, + { + "image_url":"https:\/\/example.com\/img2.png", + "thumb_url_medium":"https:\/\/example.com\/thumb2.png", + "title":"something2", + "url":"https:\/\/example.com\/2" + } + ]}', + ); + } + ); + } + + /** + * Verifies that calls to the `stats/related` return the expected data, in the + * expected format, despite the user being unauthenticated. + * + * @since 3.17.0 + * + * @covers \Parsely\REST_API\Stats\Endpoint_Related::get_related_posts + * @uses \Parsely\Endpoints\Base_Endpoint::__construct + * @uses \Parsely\Parsely::api_secret_is_set + * @uses \Parsely\Parsely::get_api_secret + * @uses \Parsely\Parsely::get_managed_credentials + * @uses \Parsely\Parsely::get_options + * @uses \Parsely\Parsely::get_site_id + * @uses \Parsely\Parsely::get_url_with_itm_source + * @uses \Parsely\Parsely::set_default_content_helper_settings_values + * @uses \Parsely\Parsely::set_default_full_metadata_in_non_posts + * @uses \Parsely\Parsely::site_id_is_set + * @uses \Parsely\Permissions::build_pch_permissions_settings_array + * @uses \Parsely\Permissions::get_user_roles_with_edit_posts_cap + * @uses \Parsely\REST_API\Base_API_Controller::__construct + * @uses \Parsely\REST_API\Base_API_Controller::get_full_namespace + * @uses \Parsely\REST_API\Base_API_Controller::get_parsely + * @uses \Parsely\REST_API\Base_API_Controller::prefix_route + * @uses \Parsely\REST_API\Base_Endpoint::__construct + * @uses \Parsely\REST_API\Base_Endpoint::get_full_endpoint + * @uses \Parsely\REST_API\Base_Endpoint::init + * @uses \Parsely\REST_API\Base_Endpoint::register_rest_route + * @uses \Parsely\REST_API\REST_API_Controller::get_namespace + * @uses \Parsely\REST_API\REST_API_Controller::get_version + * @uses \Parsely\REST_API\Stats\Endpoint_Related::__construct + * @uses \Parsely\REST_API\Stats\Endpoint_Related::get_endpoint_name + * @uses \Parsely\REST_API\Stats\Endpoint_Related::is_available_to_current_user + * @uses \Parsely\REST_API\Stats\Endpoint_Related::register_routes + * @uses \Parsely\REST_API\Stats\Post_Data_Trait::get_itm_source_param_args + * @uses \Parsely\REST_API\Stats\Post_Data_Trait::set_itm_source_from_request + * @uses \Parsely\REST_API\Stats\Related_Posts_Trait::get_related_posts_of_url + * @uses \Parsely\REST_API\Stats\Related_Posts_Trait::get_related_posts_param_args + * @uses \Parsely\REST_API\Stats\Stats_Controller::get_route_prefix + * @uses \Parsely\RemoteAPI\Base_Endpoint_Remote::get_api_url + * @uses \Parsely\RemoteAPI\Base_Endpoint_Remote::get_items + * @uses \Parsely\RemoteAPI\Base_Endpoint_Remote::get_request_options + * @uses \Parsely\RemoteAPI\Base_Endpoint_Remote::validate_required_constraints + * @uses \Parsely\Utils\Utils::convert_endpoint_to_filter_key + */ + public function test_get_related_posts(): void { + $route = $this->get_endpoint()->get_full_endpoint( '/' ); + + TestCase::set_options( + array( + 'apikey' => 'example.com', + 'api_secret' => 'test-secret', + ) + ); + + $dispatched = 0; + + add_filter( + 'pre_http_request', + function () use ( &$dispatched ): array { + $dispatched++; + return array( + 'body' => '{"data":[ + { + "image_url":"https:\/\/example.com\/img.png", + "thumb_url_medium":"https:\/\/example.com\/thumb.png", + "title":"something", + "url":"https:\/\/example.com" + }, + { + "image_url":"https:\/\/example.com\/img2.png", + "thumb_url_medium":"https:\/\/example.com\/thumb2.png", + "title":"something2", + "url":"https:\/\/example.com\/2" + } + ]}', + ); + } + ); + + $request = new WP_REST_Request( 'GET', $route ); + $request->set_param( 'url', 'https://example.com/a-post' ); + $response = rest_get_server()->dispatch( $request ); + + /** + * The response data. + * + * @var array $response_data + */ + $response_data = $response->get_data(); + + self::assertSame( 1, $dispatched ); + self::assertSame( 200, $response->get_status() ); + self::assertEquals( + array( + (object) array( + 'image_url' => 'https://example.com/img.png', + 'thumb_url_medium' => 'https://example.com/thumb.png', + 'title' => 'something', + 'url' => 'https://example.com', + ), + (object) array( + 'image_url' => 'https://example.com/img2.png', + 'thumb_url_medium' => 'https://example.com/thumb2.png', + 'title' => 'something2', + 'url' => 'https://example.com/2', + ), + ), + $response_data['data'] + ); + } + + /** + * Test that the endpoint is not available if the API secret is not set. + * This test should be disabled since the endpoint does not requires the API secret. + * + * @since 3.17.0 + * @coversNothing + */ + public function test_is_available_to_current_user_returns_error_api_secret_not_set(): void { + // This test is disabled since the endpoint does not requires the API secret. + self::assertTrue( true ); + } +} diff --git a/tests/Integration/RestAPI/Stats/StatsControllerTest.php b/tests/Integration/RestAPI/Stats/StatsControllerTest.php new file mode 100644 index 0000000000..0635125f95 --- /dev/null +++ b/tests/Integration/RestAPI/Stats/StatsControllerTest.php @@ -0,0 +1,60 @@ +stats_controller = new Stats_Controller( $parsely ); + } + + /** + * Test the constructor sets up the correct namespace and version. + * + * @since 3.17.0 + * + * @covers \Parsely\REST_API\Stats\Stats_Controller::__construct + * @uses \Parsely\REST_API\Stats\Stats_Controller::get_full_namespace + * @uses \Parsely\REST_API\REST_API_Controller::get_namespace + * @uses \Parsely\REST_API\REST_API_Controller::get_version + */ + public function test_constructor_sets_up_namespace_and_version(): void { + self::assertEquals( 'wp-parsely/v2', $this->stats_controller->get_full_namespace() ); + } +} From fa5ee007a942085084e26168a3e40cb3c3766a6f Mon Sep 17 00:00:00 2001 From: Henrique Mouta Date: Thu, 29 Aug 2024 16:20:48 +0100 Subject: [PATCH 20/49] Fix E2E test --- build/blocks/recommendations/edit.asset.php | 2 +- build/blocks/recommendations/edit.js | 2 +- build/blocks/recommendations/view.asset.php | 2 +- build/blocks/recommendations/view.js | 2 +- .../recommendations/components/parsely-recommendations.tsx | 2 +- src/blocks/recommendations/recommendations-store.tsx | 4 +++- 6 files changed, 8 insertions(+), 6 deletions(-) diff --git a/build/blocks/recommendations/edit.asset.php b/build/blocks/recommendations/edit.asset.php index ce1f1eb731..3216423575 100644 --- a/build/blocks/recommendations/edit.asset.php +++ b/build/blocks/recommendations/edit.asset.php @@ -1 +1 @@ - array('react', 'wp-api-fetch', 'wp-block-editor', 'wp-blocks', 'wp-components', 'wp-compose', 'wp-element', 'wp-i18n', 'wp-url'), 'version' => 'f1121e53d4c8acf98db5'); + array('react', 'wp-api-fetch', 'wp-block-editor', 'wp-blocks', 'wp-components', 'wp-compose', 'wp-element', 'wp-i18n', 'wp-url'), 'version' => '1752d46c4755b2406c94'); diff --git a/build/blocks/recommendations/edit.js b/build/blocks/recommendations/edit.js index c923405cf3..96036490f1 100644 --- a/build/blocks/recommendations/edit.js +++ b/build/blocks/recommendations/edit.js @@ -1 +1 @@ -!function(){"use strict";var e,n={271:function(e,n,r){var t,o,a=r(848),i=window.wp.blockEditor,l=window.wp.blocks,s=window.wp.i18n,c=window.wp.components,u=JSON.parse('{"UU":"wp-parsely/recommendations","uK":{"imagestyle":{"type":"string","default":"original"},"limit":{"type":"number","default":3},"openlinksinnewtab":{"type":"boolean","default":false},"showimages":{"type":"boolean","default":true},"sort":{"type":"string","default":"score"},"title":{"type":"string","default":"Related Content"}}}'),d=window.wp.element;(o=t||(t={}))[o.Error=0]="Error",o[o.Loaded=1]="Loaded",o[o.Recommendations=2]="Recommendations";var p=function(){return p=Object.assign||function(e){for(var n,r=1,t=arguments.length;r0&&o[o.length-1])||6!==l[0]&&2!==l[0])){i=0;continue}if(3===l[0]&&(!o||l[1]>o[0]&&l[1]=a)&&Object.keys(t.O).every((function(e){return t.O[e](r[s])}))?r.splice(s--,1):(l=!1,a0&&e[u-1][2]>a;u--)e[u]=e[u-1];e[u]=[r,o,a]},t.n=function(e){var n=e&&e.__esModule?function(){return e.default}:function(){return e};return t.d(n,{a:n}),n},t.d=function(e,n){for(var r in n)t.o(n,r)&&!t.o(e,r)&&Object.defineProperty(e,r,{enumerable:!0,get:n[r]})},t.o=function(e,n){return Object.prototype.hasOwnProperty.call(e,n)},function(){var e={335:0,203:0};t.O.j=function(n){return 0===e[n]};var n=function(n,r){var o,a,i=r[0],l=r[1],s=r[2],c=0;if(i.some((function(n){return 0!==e[n]}))){for(o in l)t.o(l,o)&&(t.m[o]=l[o]);if(s)var u=s(t)}for(n&&n(r);c0&&o[o.length-1])||6!==l[0]&&2!==l[0])){i=0;continue}if(3===l[0]&&(!o||l[1]>o[0]&&l[1]=a)&&Object.keys(t.O).every((function(e){return t.O[e](r[s])}))?r.splice(s--,1):(l=!1,a0&&e[u-1][2]>a;u--)e[u]=e[u-1];e[u]=[r,o,a]},t.n=function(e){var n=e&&e.__esModule?function(){return e.default}:function(){return e};return t.d(n,{a:n}),n},t.d=function(e,n){for(var r in n)t.o(n,r)&&!t.o(e,r)&&Object.defineProperty(e,r,{enumerable:!0,get:n[r]})},t.o=function(e,n){return Object.prototype.hasOwnProperty.call(e,n)},function(){var e={335:0,203:0};t.O.j=function(n){return 0===e[n]};var n=function(n,r){var o,a,i=r[0],l=r[1],s=r[2],c=0;if(i.some((function(n){return 0!==e[n]}))){for(o in l)t.o(l,o)&&(t.m[o]=l[o]);if(s)var u=s(t)}for(n&&n(r);c array('react', 'wp-api-fetch', 'wp-components', 'wp-compose', 'wp-dom-ready', 'wp-element', 'wp-i18n', 'wp-url'), 'version' => 'af6eb0946975f32d53b1'); + array('react', 'wp-api-fetch', 'wp-components', 'wp-compose', 'wp-dom-ready', 'wp-element', 'wp-i18n', 'wp-url'), 'version' => 'dd57327c2b8d1346aff3'); diff --git a/build/blocks/recommendations/view.js b/build/blocks/recommendations/view.js index d8f4b45779..053f899c34 100644 --- a/build/blocks/recommendations/view.js +++ b/build/blocks/recommendations/view.js @@ -1 +1 @@ -!function(){"use strict";var e={20:function(e,r,n){var t=n(609),o=Symbol.for("react.element"),a=Symbol.for("react.fragment"),i=Object.prototype.hasOwnProperty,s=t.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED.ReactCurrentOwner,l={key:!0,ref:!0,__self:!0,__source:!0};function u(e,r,n){var t,a={},u=null,c=null;for(t in void 0!==n&&(u=""+n),void 0!==r.key&&(u=""+r.key),void 0!==r.ref&&(c=r.ref),r)i.call(r,t)&&!l.hasOwnProperty(t)&&(a[t]=r[t]);if(e&&e.defaultProps)for(t in r=e.defaultProps)void 0===a[t]&&(a[t]=r[t]);return{$$typeof:o,type:e,key:u,ref:c,props:a,_owner:s.current}}r.Fragment=a,r.jsx=u,r.jsxs=u},848:function(e,r,n){e.exports=n(20)},609:function(e){e.exports=window.React}},r={};function n(t){var o=r[t];if(void 0!==o)return o.exports;var a=r[t]={exports:{}};return e[t](a,a.exports,n),a.exports}n.n=function(e){var r=e&&e.__esModule?function(){return e.default}:function(){return e};return n.d(r,{a:r}),r},n.d=function(e,r){for(var t in r)n.o(r,t)&&!n.o(e,t)&&Object.defineProperty(e,t,{enumerable:!0,get:r[t]})},n.o=function(e,r){return Object.prototype.hasOwnProperty.call(e,r)},function(){var e,r,t=n(848),o=n(609),a=window.wp.domReady,i=n.n(a),s=window.wp.element,l=window.wp.i18n;(r=e||(e={}))[r.Error=0]="Error",r[r.Loaded=1]="Loaded",r[r.Recommendations=2]="Recommendations";var u=function(){return u=Object.assign||function(e){for(var r,n=1,t=arguments.length;n0&&o[o.length-1])||6!==s[0]&&2!==s[0])){i=0;continue}if(3===s[0]&&(!o||s[1]>o[0]&&s[1]0&&o[o.length-1])||6!==s[0]&&2!==s[0])){i=0;continue}if(3===s[0]&&(!o||s[1]>o[0]&&s[1] Date: Fri, 30 Aug 2024 10:52:36 +0100 Subject: [PATCH 21/49] Add `settings` endpoints --- src/rest-api/class-rest-api-controller.php | 2 + .../settings/class-base-settings-endpoint.php | 415 ++++++++++++++++++ ...ass-endpoint-dashboard-widget-settings.php | 66 +++ ...class-endpoint-editor-sidebar-settings.php | 114 +++++ .../settings/class-settings-controller.php | 47 ++ 5 files changed, 644 insertions(+) create mode 100644 src/rest-api/settings/class-base-settings-endpoint.php create mode 100644 src/rest-api/settings/class-endpoint-dashboard-widget-settings.php create mode 100644 src/rest-api/settings/class-endpoint-editor-sidebar-settings.php create mode 100644 src/rest-api/settings/class-settings-controller.php diff --git a/src/rest-api/class-rest-api-controller.php b/src/rest-api/class-rest-api-controller.php index 5ce4e76fac..007c49e54e 100644 --- a/src/rest-api/class-rest-api-controller.php +++ b/src/rest-api/class-rest-api-controller.php @@ -11,6 +11,7 @@ namespace Parsely\REST_API; use Parsely\REST_API\Content_Helper\Content_Helper_Controller; +use Parsely\REST_API\Settings\Settings_Controller; use Parsely\REST_API\Stats\Stats_Controller; /** @@ -62,6 +63,7 @@ public function init(): void { $controllers = array( new Content_Helper_Controller( $this->get_parsely() ), new Stats_Controller( $this->get_parsely() ), + new Settings_Controller( $this->get_parsely() ), ); // Initialize the controllers. diff --git a/src/rest-api/settings/class-base-settings-endpoint.php b/src/rest-api/settings/class-base-settings-endpoint.php new file mode 100644 index 0000000000..3a4b59b3b3 --- /dev/null +++ b/src/rest-api/settings/class-base-settings-endpoint.php @@ -0,0 +1,415 @@ +, default: mixed} + */ +abstract class Base_Settings_Endpoint extends Base_Endpoint { + /** + * The meta entry's default value. Initialized in the constructor. + * + * @since 3.13.0 + * @since 3.17.0 Moved from Base_Endpoint_User_Meta. + * + * @var array + */ + protected $default_value = array(); + + /** + * The valid values that can be used for each subvalue. Initialized in the + * constructor. + * + * @since 3.13.0 + * @since 3.17.0 Moved from Base_Endpoint_User_Meta. + * + * @var array> + */ + protected $valid_subvalues = array(); + + /** + * The current user's ID. + * + * @since 3.14.0 + * @since 3.17.0 Moved from Base_Endpoint_User_Meta. + * + * @var int + */ + protected $current_user_id = 0; + + /** + * Returns the meta entry's key. + * + * @since 3.13.0 + * @since 3.17.0 Moved from Base_Endpoint_User_Meta. + * + * @return string The meta entry's key. + */ + abstract protected function get_meta_key(): string; + + /** + * Returns the endpoint's subvalues specifications. + * + * @since 3.13.0 + * @since 3.17.0 Moved from Base_Endpoint_User_Meta. + * + * @return array + */ + abstract protected function get_subvalues_specs(): array; + + /** + * Constructor. + * + * @since 3.13.0 + * @since 3.17.0 Moved from Base_Endpoint_User_Meta. + * + * @param Base_API_Controller $controller Parsely instance. + */ + public function __construct( Base_API_Controller $controller ) { + parent::__construct( $controller ); + + $subvalues_specs = $this->get_subvalues_specs(); + + foreach ( $subvalues_specs as $key => $value ) { + $this->default_value[ $key ] = $value['default']; + $this->valid_subvalues[ $key ] = $value['values']; + } + } + + /** + * Initializes the endpoint and sets the current user ID. + * + * @since 3.17.0 + */ + public function init(): void { + parent::init(); + $this->current_user_id = get_current_user_id(); + } + + /** + * Registers the routes for the endpoint. + * + * @since 3.17.0 + */ + public function register_routes(): void { + /** + * GET settings/{endpoint}/get + * Retrieves the settings for the current user. + */ + $this->register_rest_route( + '/get', + array( 'GET' ), + array( $this, 'get_settings' ) + ); + + /** + * PUT settings/{endpoint}/set + * Updates the settings for the current user. + */ + $this->register_rest_route( + '/set', + array( 'PUT' ), + array( $this, 'set_settings' ) + ); + + /** + * GET|PUT settings/{endpoint} + * Handles direct requests to the endpoint. + */ + $this->register_rest_route( + '/', + array( 'GET', 'PUT' ), + array( $this, 'process_request' ) + ); + } + + /** + * API Endpoint: GET|PUT settings/{endpoint}/ + * + * Processes the requests sent directly to the main endpoint. + * + * @since 3.17.0 + * + * @param WP_REST_Request $request The request sent to the endpoint. + * @return WP_REST_Response|WP_Error The response object. + */ + public function process_request( WP_REST_Request $request ) { + $request_method = $request->get_method(); + + // Update the meta entry's value if the request method is PUT. + if ( 'PUT' === $request_method ) { + return $this->set_settings( $request ); + } + + return $this->get_settings(); + } + + /** + * API Endpoint: GET settings/{endpoint}/get + * + * Retrieves the settings for the current user. + * + * @since 3.17.0 + * + * @return WP_REST_Response The response object. + */ + public function get_settings(): WP_REST_Response { + $meta_key = $this->get_meta_key(); + $settings = get_user_meta( $this->current_user_id, $meta_key, true ); + + if ( ! is_array( $settings ) || 0 === count( $settings ) ) { + $settings = $this->default_value; + } + + return new WP_REST_Response( $settings, 200 ); + } + + /** + * API Endpoint: PUT settings/{endpoint}/set + * + * Updates the settings for the current user. + * + * @since 3.17.0 + * + * @param WP_REST_Request $request The request object. + * @return WP_REST_Response|WP_Error The response object. + */ + public function set_settings( WP_REST_Request $request ) { + $meta_value = $request->get_json_params(); + + // Validates the settings format. + if ( ! is_array( $meta_value ) ) { // @phpstan-ignore-line + return new WP_Error( + 'ch_settings_invalid_format', + __( 'Settings must be an valid JSON array', 'wp-parsely' ) + ); + } + + $sanitized_value = $this->sanitize_value( $meta_value ); + + // If the current settings are the same as the new settings, return early. + $current_settings = $this->get_settings(); + if ( $current_settings->get_data() === $sanitized_value ) { + return $current_settings; + } + + $update_meta = update_user_meta( + $this->current_user_id, + $this->get_meta_key(), + $sanitized_value + ); + + if ( false === $update_meta ) { + return new WP_Error( + 'ch_settings_update_failed', + __( 'Failed to update settings', 'wp-parsely' ) + ); + } + + return new WP_REST_Response( $sanitized_value, 200 ); + } + + /** + * Returns whether the endpoint is available for access by the current + * user. + * + * @since 3.14.0 + * @since 3.16.0 Added the `$request` parameter. + * @since 3.17.0 Moved from Base_Endpoint_User_Meta. + * + * @param WP_REST_Request|null $request The request object. + * @return bool + */ + public function is_available_to_current_user( ?WP_REST_Request $request = null ): bool { + return current_user_can( 'edit_user', $this->current_user_id ); + } + + /** + * Sanitizes the passed meta value. + * + * @since 3.13.0 + * @since 3.14.0 Added support for nested arrays. + * @since 3.17.0 Moved from Base_Endpoint_User_Meta. + * + * @param array $meta_value The meta value to sanitize. + * @param string $parent_key The parent key for the current level of the meta. + * @return array The sanitized meta as an array of subvalues. + */ + protected function sanitize_value( array $meta_value, string $parent_key = '' ): array { + $sanitized_value = array(); + + // Determine the current level's specifications based on the parent key. + /** + * Current level's specifications. + * + * @var array $current_specs + */ + $current_specs = ( '' === $parent_key ) ? $this->get_subvalues_specs() : $this->get_nested_specs( $parent_key ); + + foreach ( $current_specs as $key => $spec ) { + $composite_key = '' === $parent_key ? $key : $parent_key . '.' . $key; + + // Check if the key exists in the input meta value array. + if ( array_key_exists( $key, $meta_value ) ) { + $value = $meta_value[ $key ]; + } else { + // Key is missing in the input, use the default value from the specifications. + $value = $this->get_default( explode( '.', $composite_key ) ); + } + + /** + * Spec for the current key. + * + * @var array{default: mixed, values?: array} $spec + */ + if ( is_array( $value ) && isset( $spec['values'] ) ) { + // Recursively handle nested arrays if 'values' spec exists for this key. + $sanitized_value[ $key ] = $this->sanitize_value( $value, $composite_key ); + } else { + // Directly sanitize non-array values or non-nested specs. + $sanitized_value[ $key ] = $this->sanitize_subvalue( $composite_key, $value ); + } + } + + return $sanitized_value; + } + + /** + * Sanitizes the passed subvalue. + * + * @since 3.13.0 + * @since 3.14.0 Added support for nested arrays. + * @since 3.17.0 Moved from Base_Endpoint_User_Meta. + * + * @param string $composite_key The subvalue's key. + * @param mixed $value The value to sanitize. + * @return mixed The sanitized subvalue. + */ + protected function sanitize_subvalue( string $composite_key, $value ) { + $keys = explode( '.', $composite_key ); + $valid_values = $this->get_valid_values( $keys ); + + if ( is_array( $value ) ) { + // Check if $value elements are inside $valid_values + // If not, the value should be the default value. + $valid_value = array(); + foreach ( $value as $key => $val ) { + if ( in_array( $val, $valid_values, true ) ) { + $valid_value[ $key ] = $val; + } + } + return $valid_value; + } + + if ( is_string( $value ) ) { + $value = sanitize_text_field( $value ); + } + + if ( count( $valid_values ) === 0 ) { + return $value; + } + + if ( ! in_array( $value, $valid_values, true ) ) { + return $this->get_default( $keys ); + } + + return $value; + } + + /** + * Gets the valid values for a given setting path. + * + * @since 3.14.3 + * @since 3.17.0 Moved from Base_Endpoint_User_Meta. + * + * @param array $keys The path to the setting. + * @return array The valid values for the setting path. + */ + protected function get_valid_values( array $keys ): array { + $current = $this->valid_subvalues; + + foreach ( $keys as $key ) { + if ( ! is_array( $current ) || ! isset( $current[ $key ] ) ) { + return array(); // No valid values for invalid key path. + } + if ( isset( $current[ $key ]['values'] ) ) { + $current = $current[ $key ]['values']; + } else { + $current = $current[ $key ]; + } + } + + return is_array( $current ) ? $current : array(); + } + + /** + * Gets the default value for a given setting path. + * + * @since 3.14.3 + * @since 3.17.0 Moved from Base_Endpoint_User_Meta. + * + * @param array $keys The path to the setting. + * @return mixed|array|null The default value for the setting path. + */ + protected function get_default( array $keys ) { + $current = $this->default_value; + + foreach ( $keys as $key ) { + if ( ! is_array( $current ) || ! isset( $current[ $key ] ) ) { + return null; // No default value for invalid key path. + } + if ( isset( $current[ $key ]['default'] ) ) { + $current = $current[ $key ]['default']; + } else { + $current = $current[ $key ]; + } + } + + return $current; // Return default value for valid key path. + } + + + /** + * Gets the specifications for nested settings based on a composite key. + * + * @since 3.14.3 + * @since 3.17.0 Moved from Base_Endpoint_User_Meta. + * + * @param string $composite_key The composite key representing the nested path. + * @return array The specifications for the nested path. + */ + protected function get_nested_specs( string $composite_key ): array { + $keys = explode( '.', $composite_key ); + $specs = $this->get_subvalues_specs(); + + foreach ( $keys as $key ) { + if ( is_array( $specs[ $key ] ) && array_key_exists( 'values', $specs[ $key ] ) ) { + $specs = $specs[ $key ]['values']; + } else { + break; + } + } + + return $specs; + } +} diff --git a/src/rest-api/settings/class-endpoint-dashboard-widget-settings.php b/src/rest-api/settings/class-endpoint-dashboard-widget-settings.php new file mode 100644 index 0000000000..bf7d429fee --- /dev/null +++ b/src/rest-api/settings/class-endpoint-dashboard-widget-settings.php @@ -0,0 +1,66 @@ + + */ + protected function get_subvalues_specs(): array { + return array( + 'Metric' => array( + 'values' => array( 'views', 'avg_engaged' ), + 'default' => 'views', + ), + 'Period' => array( + 'values' => array( '10m', '1h', '2h', '4h', '24h', '7d', '30d' ), + 'default' => '7d', + ), + ); + } +} diff --git a/src/rest-api/settings/class-endpoint-editor-sidebar-settings.php b/src/rest-api/settings/class-endpoint-editor-sidebar-settings.php new file mode 100644 index 0000000000..ccbd33c825 --- /dev/null +++ b/src/rest-api/settings/class-endpoint-editor-sidebar-settings.php @@ -0,0 +1,114 @@ + + */ + protected function get_subvalues_specs(): array { + return array( + 'InitialTabName' => array( + 'values' => array( 'tools', 'performance' ), + 'default' => 'tools', + ), + 'PerformanceStats' => array( + 'values' => array( + 'Period' => array( '10m', '1h', '2h', '4h', '24h', '7d', '30d' ), + 'VisibleDataPoints' => array( 'views', 'visitors', 'avgEngaged', 'recirculation' ), + 'VisiblePanels' => array( 'overview', 'categories', 'referrers' ), + ), + 'default' => array( + 'Period' => '7d', + 'VisibleDataPoints' => array( 'views', 'visitors', 'avgEngaged', 'recirculation' ), + 'VisiblePanels' => array( 'overview', 'categories', 'referrers' ), + ), + ), + 'RelatedPosts' => array( + 'values' => array( + 'FilterBy' => array( 'unavailable', 'tag', 'section', 'author' ), + 'FilterValue' => array(), + 'Metric' => array( 'views', 'avg_engaged' ), + 'Open' => array( true, false ), + 'Period' => array( '10m', '1h', '2h', '4h', '24h', '7d', '30d' ), + ), + 'default' => array( + 'FilterBy' => 'unavailable', + 'FilterValue' => '', + 'Metric' => 'views', + 'Open' => false, + 'Period' => '7d', + ), + ), + 'SmartLinking' => array( + 'values' => array( + 'MaxLinks' => array(), + 'MaxLinkWords' => array(), + 'Open' => array( true, false ), + ), + 'default' => array( + 'MaxLinks' => 10, + 'MaxLinkWords' => 4, + 'Open' => false, + ), + ), + 'TitleSuggestions' => array( + 'values' => array( + 'Open' => array( true, false ), + 'Persona' => array(), + 'Tone' => array(), + ), + 'default' => array( + 'Open' => false, + 'Persona' => 'journalist', + 'Tone' => 'neutral', + ), + ), + ); + } +} diff --git a/src/rest-api/settings/class-settings-controller.php b/src/rest-api/settings/class-settings-controller.php new file mode 100644 index 0000000000..30760c938f --- /dev/null +++ b/src/rest-api/settings/class-settings-controller.php @@ -0,0 +1,47 @@ +register_endpoints( $endpoints ); + } +} From 36ade72fc116d9ed2e2f05e1016f226dbeeb8745 Mon Sep 17 00:00:00 2001 From: Henrique Mouta Date: Fri, 30 Aug 2024 10:57:46 +0100 Subject: [PATCH 22/49] Update UI to use the new settings API --- build/content-helper/dashboard-widget.asset.php | 2 +- build/content-helper/dashboard-widget.js | 2 +- build/content-helper/editor-sidebar.asset.php | 2 +- build/content-helper/editor-sidebar.js | 6 +++--- src/content-helper/common/settings/provider.tsx | 3 ++- src/content-helper/dashboard-widget/dashboard-widget.tsx | 2 +- src/content-helper/editor-sidebar/editor-sidebar.tsx | 4 ++-- .../editor-sidebar/smart-linking/smart-linking.tsx | 2 +- 8 files changed, 12 insertions(+), 11 deletions(-) diff --git a/build/content-helper/dashboard-widget.asset.php b/build/content-helper/dashboard-widget.asset.php index a464c37575..4032678234 100644 --- a/build/content-helper/dashboard-widget.asset.php +++ b/build/content-helper/dashboard-widget.asset.php @@ -1 +1 @@ - array('react', 'wp-api-fetch', 'wp-components', 'wp-data', 'wp-element', 'wp-i18n', 'wp-url'), 'version' => 'd852566173ef137708b0'); + array('react', 'wp-api-fetch', 'wp-components', 'wp-data', 'wp-element', 'wp-i18n', 'wp-url'), 'version' => 'b654f2e02d29a1665104'); diff --git a/build/content-helper/dashboard-widget.js b/build/content-helper/dashboard-widget.js index 7b17d4192c..83b5454f96 100644 --- a/build/content-helper/dashboard-widget.js +++ b/build/content-helper/dashboard-widget.js @@ -1 +1 @@ -!function(){"use strict";var e={20:function(e,t,r){var n=r(609),a=Symbol.for("react.element"),s=Symbol.for("react.fragment"),o=Object.prototype.hasOwnProperty,i=n.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED.ReactCurrentOwner,l={key:!0,ref:!0,__self:!0,__source:!0};function c(e,t,r){var n,s={},c=null,u=null;for(n in void 0!==r&&(c=""+r),void 0!==t.key&&(c=""+t.key),void 0!==t.ref&&(u=t.ref),t)o.call(t,n)&&!l.hasOwnProperty(n)&&(s[n]=t[n]);if(e&&e.defaultProps)for(n in t=e.defaultProps)void 0===s[n]&&(s[n]=t[n]);return{$$typeof:a,type:e,key:c,ref:u,props:s,_owner:i.current}}t.Fragment=s,t.jsx=c,t.jsxs=c},848:function(e,t,r){e.exports=r(20)},609:function(e){e.exports=window.React}},t={};function r(n){var a=t[n];if(void 0!==a)return a.exports;var s=t[n]={exports:{}};return e[n](s,s.exports,r),s.exports}r.n=function(e){var t=e&&e.__esModule?function(){return e.default}:function(){return e};return r.d(t,{a:t}),t},r.d=function(e,t){for(var n in t)r.o(t,n)&&!r.o(e,n)&&Object.defineProperty(e,n,{enumerable:!0,get:t[n]})},r.o=function(e,t){return Object.prototype.hasOwnProperty.call(e,t)},function(){var e,t,n,a=r(848),s=window.wp.element,o=window.wp.i18n,i=function(e){void 0===e&&(e=null);var t="";(null==e?void 0:e.children)&&(t=e.children);var r="content-helper-error-message";return(null==e?void 0:e.className)&&(r+=" "+e.className),(0,a.jsx)("div",{className:r,"data-testid":null==e?void 0:e.testId,dangerouslySetInnerHTML:{__html:t}})},l=function(e){var t;return void 0===e&&(e=null),(0,a.jsx)(i,{className:null==e?void 0:e.className,testId:"empty-credentials-message",children:null!==(t=window.wpParselyEmptyCredentialsMessage)&&void 0!==t?t:(0,o.__)("Please ensure that the Site ID and API Secret given in the plugin's settings are correct.","wp-parsely")})},c=function(){return c=Object.assign||function(e){for(var t,r=1,n=arguments.length;r0&&a[a.length-1])||6!==i[0]&&2!==i[0])){o=0;continue}if(3===i[0]&&(!a||i[1]>a[0]&&i[1]=1e4&&(clearInterval(s),r("Telemetry library not loaded"))}),100);else r("Telemetry not enabled")}))},e.prototype.trackEvent=function(t,r){var n;this.isLoaded?(0!==t.indexOf(e.TRACKS_PREFIX)&&(t=e.TRACKS_PREFIX+t),this.isEventNameValid(t)?(r=this.prepareProperties(r),null===(n=this._tkq)||void 0===n||n.push(["recordEvent",t,r])):console.error("Error tracking event: Invalid event name")):console.error("Error tracking event: Telemetry not loaded")},e.prototype.isTelemetryEnabled=function(){return this.isEnabled},e.prototype.isProprietyValid=function(t){return e.PROPERTY_REGEX.test(t)},e.prototype.isEventNameValid=function(t){return e.EVENT_NAME_REGEX.test(t)},e.prototype.prepareProperties=function(e){return(e=this.sanitizeProperties(e)).parsely_version=wpParselyTracksTelemetry.version,wpParselyTracksTelemetry.user&&(e._ut=wpParselyTracksTelemetry.user.type,e._ui=wpParselyTracksTelemetry.user.id),wpParselyTracksTelemetry.vipgo_env&&(e.vipgo_env=wpParselyTracksTelemetry.vipgo_env),this.sanitizeProperties(e)},e.prototype.sanitizeProperties=function(e){var t=this,r={};return Object.keys(e).forEach((function(n){t.isProprietyValid(n)&&(r[n]=e[n])})),r},e.TRACKS_PREFIX="wpparsely_",e.EVENT_NAME_REGEX=/^(([a-z0-9]+)_){2}([a-z0-9_]+)$/,e.PROPERTY_REGEX=/^[a-z_][a-z0-9_]*$/,e}(),h=(d.trackEvent,function(e){var t=e.defaultValue,r=e.items,n=e.onChange;return(0,a.jsx)("select",{onChange:n,value:t,children:r.map((function(e){return(0,a.jsx)("option",{value:e[0],children:e[1]},e[0])}))})}),f=window.wp.data,y=function(){return y=Object.assign||function(e){for(var t,r=1,n=arguments.length;rhere.',"wp-parsely"):s.code===j.ParselySuggestionsApiOpenAiError||s.code===j.ParselySuggestionsApiOpenAiUnavailable?s.message=(0,o.__)("The Parse.ly API returned an internal server error. Please retry with a different input, or try again later.","wp-parsely"):s.code===j.HttpRequestFailed&&s.message.includes("cURL error 28")?s.message=(0,o.__)("The Parse.ly API did not respond in a timely manner. Please try again later.","wp-parsely"):s.code===j.ParselySuggestionsApiSchemaError?s.message=(0,o.__)("The Parse.ly API returned a validation error. Please try again with different parameters.","wp-parsely"):s.code===j.ParselySuggestionsApiNoData?s.message=(0,o.__)("The Parse.ly API couldn't find any relevant data to fulfill the request. Please retry with a different input.","wp-parsely"):s.code===j.ParselySuggestionsApiOpenAiSchema?s.message=(0,o.__)("The Parse.ly API returned an incorrect response. Please try again later.","wp-parsely"):s.code===j.ParselySuggestionsApiAuthUnavailable&&(s.message=(0,o.__)("The Parse.ly API is currently unavailable. Please try again later.","wp-parsely")),s}return C(t,e),t.prototype.Message=function(e){return void 0===e&&(e=null),[j.PluginCredentialsNotSetMessageDetected,j.PluginSettingsSiteIdNotSet,j.PluginSettingsApiSecretNotSet].includes(this.code)?l(e):(this.code===j.FetchError&&(this.hint=this.Hint((0,o.__)("This error can sometimes be caused by ad-blockers or browser tracking protections. Please add this site to any applicable allow lists and try again.","wp-parsely"))),this.code!==j.ParselyApiForbidden&&this.code!==j.ParselySuggestionsApiNoAuthentication||(this.hint=this.Hint((0,o.__)("Please ensure that the Site ID and API Secret given in the plugin's settings are correct.","wp-parsely"))),this.code===j.HttpRequestFailed&&(this.hint=this.Hint((0,o.__)("The Parse.ly API cannot be reached. Please verify that you are online.","wp-parsely"))),(0,a.jsx)(i,{className:null==e?void 0:e.className,testId:"error",children:"

".concat(this.message,"

").concat(this.hint?this.hint:"")}))},t.prototype.Hint=function(e){return'

'.concat((0,o.__)("Hint:","wp-parsely")," ").concat(e,"

")},t.prototype.createErrorSnackbar=function(){//.test(this.message)||(0,f.dispatch)("core/notices").createNotice("error",this.message,{type:"snackbar"})},t}(Error),O=function(){function e(){this.abortControllers=new Map}return e.prototype.cancelRequest=function(e){if(e)(t=this.abortControllers.get(e))&&(t.abort(),this.abortControllers.delete(e));else{var t,r=Array.from(this.abortControllers.keys()).pop();r&&(t=this.abortControllers.get(r))&&(t.abort(),this.abortControllers.delete(r))}},e.prototype.cancelAll=function(){this.abortControllers.forEach((function(e){return e.abort()})),this.abortControllers.clear()},e.prototype.getOrCreateController=function(e){if(e&&this.abortControllers.has(e))return{abortController:this.abortControllers.get(e),abortId:e};var t=null!=e?e:"auto-"+Date.now(),r=new AbortController;return this.abortControllers.set(t,r),{abortController:r,abortId:t}},e.prototype.fetch=function(e,t){return r=this,n=void 0,s=function(){var r,n,a,s,i,l;return function(e,t){var r,n,a,s,o={label:0,sent:function(){if(1&a[0])throw a[1];return a[1]},trys:[],ops:[]};return s={next:i(0),throw:i(1),return:i(2)},"function"==typeof Symbol&&(s[Symbol.iterator]=function(){return this}),s;function i(i){return function(l){return function(i){if(r)throw new TypeError("Generator is already executing.");for(;s&&(s=0,i[0]&&(o=0)),o;)try{if(r=1,n&&(a=2&i[0]?n.return:i[0]?n.throw||((a=n.return)&&a.call(n),0):n.next)&&!(a=a.call(n,i[1])).done)return a;switch(n=0,a&&(i=[2&i[0],a.value]),i[0]){case 0:case 1:a=i;break;case 4:return o.label++,{value:i[1],done:!1};case 5:o.label++,n=i[1],i=[0];continue;case 7:i=o.ops.pop(),o.trys.pop();continue;default:if(!((a=(a=o.trys).length>0&&a[a.length-1])||6!==i[0]&&2!==i[0])){o=0;continue}if(3===i[0]&&(!a||i[1]>a[0]&&i[1]0&&a[a.length-1])||6!==i[0]&&2!==i[0])){o=0;continue}if(3===i[0]&&(!a||i[1]>a[0]&&i[1]=c){var u=t;(a=n/c)%1>1/i&&(u=a>10?1:2),u=parseFloat(a.toFixed(2))===parseFloat(a.toFixed(0))?0:u,s=a.toFixed(u),o=l}i=c})),s+r+o}function z(e){var t=e.metric,r=e.post,n=e.avgEngagedIcon,s=e.viewsIcon;return"views"===t?(0,a.jsxs)("span",{className:"parsely-post-metric-data",children:[(0,a.jsx)("span",{className:"screen-reader-text",children:(0,o.__)("Number of Views","wp-parsely")}),s,$(r.views.toString())]}):"avg_engaged"===t?(0,a.jsxs)("span",{className:"parsely-post-metric-data",children:[(0,a.jsx)("span",{className:"screen-reader-text",children:(0,o.__)("Average Time","wp-parsely")}),n,r.avgEngaged]}):(0,a.jsx)("span",{className:"parsely-post-metric-data",children:"-"})}function X(e){var t,r=e.metric,n=e.post;return(0,a.jsx)("li",{className:"parsely-top-post",children:(0,a.jsxs)("div",{className:"parsely-top-post-content",children:[(0,a.jsx)(B,{post:n}),(0,a.jsxs)("div",{className:"parsely-top-post-data",children:[(0,a.jsx)(z,{metric:r,post:n}),(0,a.jsx)(Z,{post:n}),(0,a.jsxs)("a",{className:"parsely-top-post-icon-link",href:n.url,target:"_blank",rel:"noreferrer",children:[(0,a.jsx)("span",{className:"screen-reader-text",children:(0,o.__)("View Post (opens in new tab)","wp-parsely")}),(0,a.jsx)(q,{})]}),0!==n.postId&&(0,a.jsxs)("a",{className:"parsely-top-post-icon-link",href:(t=n.postId,"/wp-admin/post.php?post=".concat(t,"&action=edit")),target:"_blank",rel:"noreferrer",children:[(0,a.jsx)("span",{className:"screen-reader-text",children:(0,o.__)("Edit Post (opens in new tab)","wp-parsely")}),(0,a.jsx)(G,{})]}),(0,a.jsxs)("div",{className:"parsely-top-post-metadata",children:[(0,a.jsxs)("span",{className:"parsely-top-post-date",children:[(0,a.jsx)("span",{className:"screen-reader-text",children:(0,o.__)("Date","wp-parsely")}),M(new Date(n.date))]}),(0,a.jsxs)("span",{className:"parsely-top-post-author",children:[(0,a.jsx)("span",{className:"screen-reader-text",children:(0,o.__)("Author","wp-parsely")}),n.author]})]})]})]})},n.id)}function B(e){var t=e.post;return t.thumbnailUrl?(0,a.jsxs)("div",{className:"parsely-top-post-thumbnail",children:[(0,a.jsx)("span",{className:"screen-reader-text",children:(0,o.__)("Thumbnail","wp-parsely")}),(0,a.jsx)("img",{src:t.thumbnailUrl,alt:(0,o.__)("Post thumbnail","wp-parsely")})]}):(0,a.jsx)("div",{className:"parsely-top-post-thumbnail",children:(0,a.jsx)("span",{className:"screen-reader-text",children:(0,o.__)("Post thumbnail not available","wp-parsely")})})}function Z(e){var t=e.post;return(0,a.jsxs)("a",{className:"parsely-top-post-title",href:t.dashUrl,target:"_blank",rel:"noreferrer",children:[(0,a.jsx)("span",{className:"screen-reader-text",children:(0,o.__)("View in Parse.ly (opens in new tab)","wp-parsely")}),t.title]})}var W=function(){return W=Object.assign||function(e){for(var t,r=1,n=arguments.length;r0&&a[a.length-1])||6!==i[0]&&2!==i[0])){o=0;continue}if(3===i[0]&&(!a||i[1]>a[0]&&i[1]0&&e.retryFetch?[4,new Promise((function(e){return setTimeout(e,500)}))]:[3,3];case 1:return r.sent(),[4,t(n-1)];case 2:return r.sent(),[3,4];case 3:f(!1),v(e),r.label=4;case 4:return[2]}}))}))})),[2]}))}))};return f(!0),t(1),function(){f(!1),m([]),v(void 0)}}),[i,S]);var j=function(e,t){d.trackEvent("dash_widget_filter_changed",W({filter:e},t))},N=(0,a.jsxs)("div",{className:"parsely-top-posts-filters",children:[(0,a.jsx)(h,{defaultValue:i.Period,items:Object.values(e).map((function(e){return[e,A(e)]})),onChange:function(t){x(t.target.value,e)&&(l({Period:t.target.value}),j("period",{period:t.target.value}),T(1))}}),(0,a.jsx)(h,{defaultValue:i.Metric,items:Object.values(t).map((function(e){return[e,E(e)]})),onChange:function(e){x(e.target.value,t)&&(l({Metric:e.target.value}),j("metric",{metric:e.target.value}),T(1))}})]}),C=(0,a.jsxs)("div",{className:"parsely-top-posts-navigation",children:[(0,a.jsx)("button",{className:"parsely-top-posts-navigation-prev",disabled:S<=1,"aria-label":(0,o.__)("Previous page","wp-parsely"),onClick:function(){T(S-1),d.trackEvent("dash_widget_navigation",{navigation:"previous",to_page:S-1})},children:(0,o.__)("<< Previous","wp-parsely")}),(0,o.sprintf)(/* translators: 1: Current page */ /* translators: 1: Current page */(0,o.__)("Page %1$d","wp-parsely"),S),(0,a.jsx)("button",{className:"parsely-top-posts-navigation-next",disabled:!u&&_.length<5,"aria-label":(0,o.__)("Next page","wp-parsely"),onClick:function(){T(S+1),d.trackEvent("dash_widget_navigation",{navigation:"next",to_page:S+1})},children:(0,o.__)("Next >>","wp-parsely")})]});if(g)return(0,a.jsxs)(a.Fragment,{children:[N,g.Message(),S>1&&C]});var k=(0,a.jsx)("div",{className:"parsely-spinner-wrapper",children:(0,a.jsx)(p.Spinner,{})});return(0,a.jsxs)(a.Fragment,{children:[N,u?k:(0,a.jsx)("ol",{className:"parsely-top-posts",style:{counterReset:"item "+5*(S-1)},children:_.map((function(e){return(0,a.jsx)(X,{metric:i.Metric,post:e},e.id)}))}),(_.length>=5||S>1)&&C]})}var J=function(r){var n;try{n=JSON.parse(r)}catch(r){return{Metric:t.Views,Period:e.Days7}}return x(null==n?void 0:n.Metric,t)||(n.Metric=t.Views),x(null==n?void 0:n.Period,e)||(n.Period=e.Days7),n};window.addEventListener("load",(function(){var e=document.querySelector("#wp-parsely-dashboard-widget > .inside");if(null!==e){var t=(0,a.jsx)(S,{endpoint:"dashboard-widget-settings",defaultSettings:J(window.wpParselyContentHelperSettings),children:(0,a.jsx)(u,{children:(0,a.jsx)(Q,{})})});s.createRoot?(0,s.createRoot)(e).render(t):(0,s.render)(t,e)}}),!1)}()}(); \ No newline at end of file +!function(){"use strict";var e={20:function(e,t,r){var n=r(609),a=Symbol.for("react.element"),s=Symbol.for("react.fragment"),o=Object.prototype.hasOwnProperty,i=n.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED.ReactCurrentOwner,l={key:!0,ref:!0,__self:!0,__source:!0};function c(e,t,r){var n,s={},c=null,u=null;for(n in void 0!==r&&(c=""+r),void 0!==t.key&&(c=""+t.key),void 0!==t.ref&&(u=t.ref),t)o.call(t,n)&&!l.hasOwnProperty(n)&&(s[n]=t[n]);if(e&&e.defaultProps)for(n in t=e.defaultProps)void 0===s[n]&&(s[n]=t[n]);return{$$typeof:a,type:e,key:c,ref:u,props:s,_owner:i.current}}t.Fragment=s,t.jsx=c,t.jsxs=c},848:function(e,t,r){e.exports=r(20)},609:function(e){e.exports=window.React}},t={};function r(n){var a=t[n];if(void 0!==a)return a.exports;var s=t[n]={exports:{}};return e[n](s,s.exports,r),s.exports}r.n=function(e){var t=e&&e.__esModule?function(){return e.default}:function(){return e};return r.d(t,{a:t}),t},r.d=function(e,t){for(var n in t)r.o(t,n)&&!r.o(e,n)&&Object.defineProperty(e,n,{enumerable:!0,get:t[n]})},r.o=function(e,t){return Object.prototype.hasOwnProperty.call(e,t)},function(){var e,t,n,a=r(848),s=window.wp.element,o=window.wp.i18n,i=function(e){void 0===e&&(e=null);var t="";(null==e?void 0:e.children)&&(t=e.children);var r="content-helper-error-message";return(null==e?void 0:e.className)&&(r+=" "+e.className),(0,a.jsx)("div",{className:r,"data-testid":null==e?void 0:e.testId,dangerouslySetInnerHTML:{__html:t}})},l=function(e){var t;return void 0===e&&(e=null),(0,a.jsx)(i,{className:null==e?void 0:e.className,testId:"empty-credentials-message",children:null!==(t=window.wpParselyEmptyCredentialsMessage)&&void 0!==t?t:(0,o.__)("Please ensure that the Site ID and API Secret given in the plugin's settings are correct.","wp-parsely")})},c=function(){return c=Object.assign||function(e){for(var t,r=1,n=arguments.length;r0&&a[a.length-1])||6!==i[0]&&2!==i[0])){o=0;continue}if(3===i[0]&&(!a||i[1]>a[0]&&i[1]=1e4&&(clearInterval(s),r("Telemetry library not loaded"))}),100);else r("Telemetry not enabled")}))},e.prototype.trackEvent=function(t,r){var n;this.isLoaded?(0!==t.indexOf(e.TRACKS_PREFIX)&&(t=e.TRACKS_PREFIX+t),this.isEventNameValid(t)?(r=this.prepareProperties(r),null===(n=this._tkq)||void 0===n||n.push(["recordEvent",t,r])):console.error("Error tracking event: Invalid event name")):console.error("Error tracking event: Telemetry not loaded")},e.prototype.isTelemetryEnabled=function(){return this.isEnabled},e.prototype.isProprietyValid=function(t){return e.PROPERTY_REGEX.test(t)},e.prototype.isEventNameValid=function(t){return e.EVENT_NAME_REGEX.test(t)},e.prototype.prepareProperties=function(e){return(e=this.sanitizeProperties(e)).parsely_version=wpParselyTracksTelemetry.version,wpParselyTracksTelemetry.user&&(e._ut=wpParselyTracksTelemetry.user.type,e._ui=wpParselyTracksTelemetry.user.id),wpParselyTracksTelemetry.vipgo_env&&(e.vipgo_env=wpParselyTracksTelemetry.vipgo_env),this.sanitizeProperties(e)},e.prototype.sanitizeProperties=function(e){var t=this,r={};return Object.keys(e).forEach((function(n){t.isProprietyValid(n)&&(r[n]=e[n])})),r},e.TRACKS_PREFIX="wpparsely_",e.EVENT_NAME_REGEX=/^(([a-z0-9]+)_){2}([a-z0-9_]+)$/,e.PROPERTY_REGEX=/^[a-z_][a-z0-9_]*$/,e}(),h=(d.trackEvent,function(e){var t=e.defaultValue,r=e.items,n=e.onChange;return(0,a.jsx)("select",{onChange:n,value:t,children:r.map((function(e){return(0,a.jsx)("option",{value:e[0],children:e[1]},e[0])}))})}),f=window.wp.data,y=function(){return y=Object.assign||function(e){for(var t,r=1,n=arguments.length;rhere.',"wp-parsely"):s.code===j.ParselySuggestionsApiOpenAiError||s.code===j.ParselySuggestionsApiOpenAiUnavailable?s.message=(0,o.__)("The Parse.ly API returned an internal server error. Please retry with a different input, or try again later.","wp-parsely"):s.code===j.HttpRequestFailed&&s.message.includes("cURL error 28")?s.message=(0,o.__)("The Parse.ly API did not respond in a timely manner. Please try again later.","wp-parsely"):s.code===j.ParselySuggestionsApiSchemaError?s.message=(0,o.__)("The Parse.ly API returned a validation error. Please try again with different parameters.","wp-parsely"):s.code===j.ParselySuggestionsApiNoData?s.message=(0,o.__)("The Parse.ly API couldn't find any relevant data to fulfill the request. Please retry with a different input.","wp-parsely"):s.code===j.ParselySuggestionsApiOpenAiSchema?s.message=(0,o.__)("The Parse.ly API returned an incorrect response. Please try again later.","wp-parsely"):s.code===j.ParselySuggestionsApiAuthUnavailable&&(s.message=(0,o.__)("The Parse.ly API is currently unavailable. Please try again later.","wp-parsely")),s}return C(t,e),t.prototype.Message=function(e){return void 0===e&&(e=null),[j.PluginCredentialsNotSetMessageDetected,j.PluginSettingsSiteIdNotSet,j.PluginSettingsApiSecretNotSet].includes(this.code)?l(e):(this.code===j.FetchError&&(this.hint=this.Hint((0,o.__)("This error can sometimes be caused by ad-blockers or browser tracking protections. Please add this site to any applicable allow lists and try again.","wp-parsely"))),this.code!==j.ParselyApiForbidden&&this.code!==j.ParselySuggestionsApiNoAuthentication||(this.hint=this.Hint((0,o.__)("Please ensure that the Site ID and API Secret given in the plugin's settings are correct.","wp-parsely"))),this.code===j.HttpRequestFailed&&(this.hint=this.Hint((0,o.__)("The Parse.ly API cannot be reached. Please verify that you are online.","wp-parsely"))),(0,a.jsx)(i,{className:null==e?void 0:e.className,testId:"error",children:"

".concat(this.message,"

").concat(this.hint?this.hint:"")}))},t.prototype.Hint=function(e){return'

'.concat((0,o.__)("Hint:","wp-parsely")," ").concat(e,"

")},t.prototype.createErrorSnackbar=function(){//.test(this.message)||(0,f.dispatch)("core/notices").createNotice("error",this.message,{type:"snackbar"})},t}(Error),O=function(){function e(){this.abortControllers=new Map}return e.prototype.cancelRequest=function(e){if(e)(t=this.abortControllers.get(e))&&(t.abort(),this.abortControllers.delete(e));else{var t,r=Array.from(this.abortControllers.keys()).pop();r&&(t=this.abortControllers.get(r))&&(t.abort(),this.abortControllers.delete(r))}},e.prototype.cancelAll=function(){this.abortControllers.forEach((function(e){return e.abort()})),this.abortControllers.clear()},e.prototype.getOrCreateController=function(e){if(e&&this.abortControllers.has(e))return{abortController:this.abortControllers.get(e),abortId:e};var t=null!=e?e:"auto-"+Date.now(),r=new AbortController;return this.abortControllers.set(t,r),{abortController:r,abortId:t}},e.prototype.fetch=function(e,t){return r=this,n=void 0,s=function(){var r,n,a,s,i,l;return function(e,t){var r,n,a,s,o={label:0,sent:function(){if(1&a[0])throw a[1];return a[1]},trys:[],ops:[]};return s={next:i(0),throw:i(1),return:i(2)},"function"==typeof Symbol&&(s[Symbol.iterator]=function(){return this}),s;function i(i){return function(l){return function(i){if(r)throw new TypeError("Generator is already executing.");for(;s&&(s=0,i[0]&&(o=0)),o;)try{if(r=1,n&&(a=2&i[0]?n.return:i[0]?n.throw||((a=n.return)&&a.call(n),0):n.next)&&!(a=a.call(n,i[1])).done)return a;switch(n=0,a&&(i=[2&i[0],a.value]),i[0]){case 0:case 1:a=i;break;case 4:return o.label++,{value:i[1],done:!1};case 5:o.label++,n=i[1],i=[0];continue;case 7:i=o.ops.pop(),o.trys.pop();continue;default:if(!((a=(a=o.trys).length>0&&a[a.length-1])||6!==i[0]&&2!==i[0])){o=0;continue}if(3===i[0]&&(!a||i[1]>a[0]&&i[1]0&&a[a.length-1])||6!==i[0]&&2!==i[0])){o=0;continue}if(3===i[0]&&(!a||i[1]>a[0]&&i[1]=c){var u=t;(a=n/c)%1>1/i&&(u=a>10?1:2),u=parseFloat(a.toFixed(2))===parseFloat(a.toFixed(0))?0:u,s=a.toFixed(u),o=l}i=c})),s+r+o}function z(e){var t=e.metric,r=e.post,n=e.avgEngagedIcon,s=e.viewsIcon;return"views"===t?(0,a.jsxs)("span",{className:"parsely-post-metric-data",children:[(0,a.jsx)("span",{className:"screen-reader-text",children:(0,o.__)("Number of Views","wp-parsely")}),s,$(r.views.toString())]}):"avg_engaged"===t?(0,a.jsxs)("span",{className:"parsely-post-metric-data",children:[(0,a.jsx)("span",{className:"screen-reader-text",children:(0,o.__)("Average Time","wp-parsely")}),n,r.avgEngaged]}):(0,a.jsx)("span",{className:"parsely-post-metric-data",children:"-"})}function X(e){var t,r=e.metric,n=e.post;return(0,a.jsx)("li",{className:"parsely-top-post",children:(0,a.jsxs)("div",{className:"parsely-top-post-content",children:[(0,a.jsx)(B,{post:n}),(0,a.jsxs)("div",{className:"parsely-top-post-data",children:[(0,a.jsx)(z,{metric:r,post:n}),(0,a.jsx)(Z,{post:n}),(0,a.jsxs)("a",{className:"parsely-top-post-icon-link",href:n.url,target:"_blank",rel:"noreferrer",children:[(0,a.jsx)("span",{className:"screen-reader-text",children:(0,o.__)("View Post (opens in new tab)","wp-parsely")}),(0,a.jsx)(q,{})]}),0!==n.postId&&(0,a.jsxs)("a",{className:"parsely-top-post-icon-link",href:(t=n.postId,"/wp-admin/post.php?post=".concat(t,"&action=edit")),target:"_blank",rel:"noreferrer",children:[(0,a.jsx)("span",{className:"screen-reader-text",children:(0,o.__)("Edit Post (opens in new tab)","wp-parsely")}),(0,a.jsx)(G,{})]}),(0,a.jsxs)("div",{className:"parsely-top-post-metadata",children:[(0,a.jsxs)("span",{className:"parsely-top-post-date",children:[(0,a.jsx)("span",{className:"screen-reader-text",children:(0,o.__)("Date","wp-parsely")}),M(new Date(n.date))]}),(0,a.jsxs)("span",{className:"parsely-top-post-author",children:[(0,a.jsx)("span",{className:"screen-reader-text",children:(0,o.__)("Author","wp-parsely")}),n.author]})]})]})]})},n.id)}function B(e){var t=e.post;return t.thumbnailUrl?(0,a.jsxs)("div",{className:"parsely-top-post-thumbnail",children:[(0,a.jsx)("span",{className:"screen-reader-text",children:(0,o.__)("Thumbnail","wp-parsely")}),(0,a.jsx)("img",{src:t.thumbnailUrl,alt:(0,o.__)("Post thumbnail","wp-parsely")})]}):(0,a.jsx)("div",{className:"parsely-top-post-thumbnail",children:(0,a.jsx)("span",{className:"screen-reader-text",children:(0,o.__)("Post thumbnail not available","wp-parsely")})})}function Z(e){var t=e.post;return(0,a.jsxs)("a",{className:"parsely-top-post-title",href:t.dashUrl,target:"_blank",rel:"noreferrer",children:[(0,a.jsx)("span",{className:"screen-reader-text",children:(0,o.__)("View in Parse.ly (opens in new tab)","wp-parsely")}),t.title]})}var W=function(){return W=Object.assign||function(e){for(var t,r=1,n=arguments.length;r0&&a[a.length-1])||6!==i[0]&&2!==i[0])){o=0;continue}if(3===i[0]&&(!a||i[1]>a[0]&&i[1]0&&e.retryFetch?[4,new Promise((function(e){return setTimeout(e,500)}))]:[3,3];case 1:return r.sent(),[4,t(n-1)];case 2:return r.sent(),[3,4];case 3:f(!1),v(e),r.label=4;case 4:return[2]}}))}))})),[2]}))}))};return f(!0),t(1),function(){f(!1),m([]),v(void 0)}}),[i,S]);var j=function(e,t){d.trackEvent("dash_widget_filter_changed",W({filter:e},t))},N=(0,a.jsxs)("div",{className:"parsely-top-posts-filters",children:[(0,a.jsx)(h,{defaultValue:i.Period,items:Object.values(e).map((function(e){return[e,A(e)]})),onChange:function(t){x(t.target.value,e)&&(l({Period:t.target.value}),j("period",{period:t.target.value}),T(1))}}),(0,a.jsx)(h,{defaultValue:i.Metric,items:Object.values(t).map((function(e){return[e,E(e)]})),onChange:function(e){x(e.target.value,t)&&(l({Metric:e.target.value}),j("metric",{metric:e.target.value}),T(1))}})]}),C=(0,a.jsxs)("div",{className:"parsely-top-posts-navigation",children:[(0,a.jsx)("button",{className:"parsely-top-posts-navigation-prev",disabled:S<=1,"aria-label":(0,o.__)("Previous page","wp-parsely"),onClick:function(){T(S-1),d.trackEvent("dash_widget_navigation",{navigation:"previous",to_page:S-1})},children:(0,o.__)("<< Previous","wp-parsely")}),(0,o.sprintf)(/* translators: 1: Current page */ /* translators: 1: Current page */(0,o.__)("Page %1$d","wp-parsely"),S),(0,a.jsx)("button",{className:"parsely-top-posts-navigation-next",disabled:!u&&_.length<5,"aria-label":(0,o.__)("Next page","wp-parsely"),onClick:function(){T(S+1),d.trackEvent("dash_widget_navigation",{navigation:"next",to_page:S+1})},children:(0,o.__)("Next >>","wp-parsely")})]});if(g)return(0,a.jsxs)(a.Fragment,{children:[N,g.Message(),S>1&&C]});var k=(0,a.jsx)("div",{className:"parsely-spinner-wrapper",children:(0,a.jsx)(p.Spinner,{})});return(0,a.jsxs)(a.Fragment,{children:[N,u?k:(0,a.jsx)("ol",{className:"parsely-top-posts",style:{counterReset:"item "+5*(S-1)},children:_.map((function(e){return(0,a.jsx)(X,{metric:i.Metric,post:e},e.id)}))}),(_.length>=5||S>1)&&C]})}var J=function(r){var n;try{n=JSON.parse(r)}catch(r){return{Metric:t.Views,Period:e.Days7}}return x(null==n?void 0:n.Metric,t)||(n.Metric=t.Views),x(null==n?void 0:n.Period,e)||(n.Period=e.Days7),n};window.addEventListener("load",(function(){var e=document.querySelector("#wp-parsely-dashboard-widget > .inside");if(null!==e){var t=(0,a.jsx)(S,{endpoint:"dashboard-widget",defaultSettings:J(window.wpParselyContentHelperSettings),children:(0,a.jsx)(u,{children:(0,a.jsx)(Q,{})})});s.createRoot?(0,s.createRoot)(e).render(t):(0,s.render)(t,e)}}),!1)}()}(); \ No newline at end of file diff --git a/build/content-helper/editor-sidebar.asset.php b/build/content-helper/editor-sidebar.asset.php index a791b1c322..e3d4865528 100644 --- a/build/content-helper/editor-sidebar.asset.php +++ b/build/content-helper/editor-sidebar.asset.php @@ -1 +1 @@ - array('react', 'wp-api-fetch', 'wp-block-editor', 'wp-blocks', 'wp-components', 'wp-compose', 'wp-core-data', 'wp-data', 'wp-dom-ready', 'wp-editor', 'wp-element', 'wp-hooks', 'wp-i18n', 'wp-plugins', 'wp-primitives', 'wp-url'), 'version' => 'eaad1ca5764f3fbdb9e6'); + array('react', 'wp-api-fetch', 'wp-block-editor', 'wp-blocks', 'wp-components', 'wp-compose', 'wp-core-data', 'wp-data', 'wp-dom-ready', 'wp-editor', 'wp-element', 'wp-hooks', 'wp-i18n', 'wp-plugins', 'wp-primitives', 'wp-url'), 'version' => '7d87b64bdabeaf1dc579'); diff --git a/build/content-helper/editor-sidebar.js b/build/content-helper/editor-sidebar.js index 697b66067d..9723913d54 100644 --- a/build/content-helper/editor-sidebar.js +++ b/build/content-helper/editor-sidebar.js @@ -1,9 +1,9 @@ -!function(){"use strict";var e={20:function(e,t,n){var r=n(609),i=Symbol.for("react.element"),s=Symbol.for("react.fragment"),o=Object.prototype.hasOwnProperty,a=r.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED.ReactCurrentOwner,l={key:!0,ref:!0,__self:!0,__source:!0};function c(e,t,n){var r,s={},c=null,u=null;for(r in void 0!==n&&(c=""+n),void 0!==t.key&&(c=""+t.key),void 0!==t.ref&&(u=t.ref),t)o.call(t,r)&&!l.hasOwnProperty(r)&&(s[r]=t[r]);if(e&&e.defaultProps)for(r in t=e.defaultProps)void 0===s[r]&&(s[r]=t[r]);return{$$typeof:i,type:e,key:c,ref:u,props:s,_owner:a.current}}t.Fragment=s,t.jsx=c,t.jsxs=c},848:function(e,t,n){e.exports=n(20)},609:function(e){e.exports=window.React}},t={};function n(r){var i=t[r];if(void 0!==i)return i.exports;var s=t[r]={exports:{}};return e[r](s,s.exports,n),s.exports}n.n=function(e){var t=e&&e.__esModule?function(){return e.default}:function(){return e};return n.d(t,{a:t}),t},n.d=function(e,t){for(var r in t)n.o(t,r)&&!n.o(e,r)&&Object.defineProperty(e,r,{enumerable:!0,get:t[r]})},n.o=function(e,t){return Object.prototype.hasOwnProperty.call(e,t)},function(){n.d({},{_:function(){return ur}});var e,t,r,i,s,o,a,l,c,u,d,p=n(848),f=window.wp.components,h=window.wp.data,v=window.wp.domReady,g=n.n(v);void 0!==window.wp&&(null!==(t=null===(e=window.wp.editor)||void 0===e?void 0:e.PluginDocumentSettingPanel)&&void 0!==t||(null!==(i=null===(r=window.wp.editPost)||void 0===r?void 0:r.PluginDocumentSettingPanel)&&void 0!==i||(null===(s=window.wp.editSite)||void 0===s||s.PluginDocumentSettingPanel)),d=null!==(a=null===(o=window.wp.editor)||void 0===o?void 0:o.PluginSidebar)&&void 0!==a?a:null!==(c=null===(l=window.wp.editPost)||void 0===l?void 0:l.PluginSidebar)&&void 0!==c?c:null===(u=window.wp.editSite)||void 0===u?void 0:u.PluginSidebar);var y,m,w,b=window.wp.element,_=window.wp.i18n,x=window.wp.primitives,k=(0,p.jsx)(x.SVG,{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 24 24",children:(0,p.jsx)(x.Path,{fillRule:"evenodd",d:"M11.25 5h1.5v15h-1.5V5zM6 10h1.5v10H6V10zm12 4h-1.5v6H18v-6z",clipRule:"evenodd"})}),S=window.wp.plugins,P=function(){function e(){this._tkq=[],this.isLoaded=!1,this.isEnabled=!1,"undefined"!=typeof wpParselyTracksTelemetry&&(this.isEnabled=!0,this.loadTrackingLibrary())}return e.getInstance=function(){return window.wpParselyTelemetryInstance||Object.defineProperty(window,"wpParselyTelemetryInstance",{value:new e,writable:!1,configurable:!1,enumerable:!1}),window.wpParselyTelemetryInstance},e.prototype.loadTrackingLibrary=function(){var e=this,t=document.createElement("script");t.async=!0,t.src="//stats.wp.com/w.js",t.onload=function(){e.isLoaded=!0,e._tkq=window._tkq||[]},document.head.appendChild(t)},e.trackEvent=function(t){return n=this,r=arguments,s=function(t,n){var r;return void 0===n&&(n={}),function(e,t){var n,r,i,s,o={label:0,sent:function(){if(1&i[0])throw i[1];return i[1]},trys:[],ops:[]};return s={next:a(0),throw:a(1),return:a(2)},"function"==typeof Symbol&&(s[Symbol.iterator]=function(){return this}),s;function a(a){return function(l){return function(a){if(n)throw new TypeError("Generator is already executing.");for(;s&&(s=0,a[0]&&(o=0)),o;)try{if(n=1,r&&(i=2&a[0]?r.return:a[0]?r.throw||((i=r.return)&&i.call(r),0):r.next)&&!(i=i.call(r,a[1])).done)return i;switch(r=0,i&&(a=[2&a[0],i.value]),a[0]){case 0:case 1:i=a;break;case 4:return o.label++,{value:a[1],done:!1};case 5:o.label++,r=a[1],a=[0];continue;case 7:a=o.ops.pop(),o.trys.pop();continue;default:if(!((i=(i=o.trys).length>0&&i[i.length-1])||6!==a[0]&&2!==a[0])){o=0;continue}if(3===a[0]&&(!i||a[1]>i[0]&&a[1]=1e4&&(clearInterval(s),n("Telemetry library not loaded"))}),100);else n("Telemetry not enabled")}))},e.prototype.trackEvent=function(t,n){var r;this.isLoaded?(0!==t.indexOf(e.TRACKS_PREFIX)&&(t=e.TRACKS_PREFIX+t),this.isEventNameValid(t)?(n=this.prepareProperties(n),null===(r=this._tkq)||void 0===r||r.push(["recordEvent",t,n])):console.error("Error tracking event: Invalid event name")):console.error("Error tracking event: Telemetry not loaded")},e.prototype.isTelemetryEnabled=function(){return this.isEnabled},e.prototype.isProprietyValid=function(t){return e.PROPERTY_REGEX.test(t)},e.prototype.isEventNameValid=function(t){return e.EVENT_NAME_REGEX.test(t)},e.prototype.prepareProperties=function(e){return(e=this.sanitizeProperties(e)).parsely_version=wpParselyTracksTelemetry.version,wpParselyTracksTelemetry.user&&(e._ut=wpParselyTracksTelemetry.user.type,e._ui=wpParselyTracksTelemetry.user.id),wpParselyTracksTelemetry.vipgo_env&&(e.vipgo_env=wpParselyTracksTelemetry.vipgo_env),this.sanitizeProperties(e)},e.prototype.sanitizeProperties=function(e){var t=this,n={};return Object.keys(e).forEach((function(r){t.isProprietyValid(r)&&(n[r]=e[r])})),n},e.TRACKS_PREFIX="wpparsely_",e.EVENT_NAME_REGEX=/^(([a-z0-9]+)_){2}([a-z0-9_]+)$/,e.PROPERTY_REGEX=/^[a-z_][a-z0-9_]*$/,e}(),j=(P.trackEvent,function(){return(0,p.jsx)(f.SVG,{"aria-hidden":"true",version:"1.1",viewBox:"0 0 15 15",width:"15",height:"15",xmlns:"http://www.w3.org/2000/svg",children:(0,p.jsx)(f.Path,{d:"M0 14.0025V11.0025L7.5 3.5025L10.5 6.5025L3 14.0025H0ZM12 5.0025L13.56 3.4425C14.15 2.8525 14.15 1.9025 13.56 1.3225L12.68 0.4425C12.09 -0.1475 11.14 -0.1475 10.56 0.4425L9 2.0025L12 5.0025Z"})})}),T=function(e){var t=e.size,n=void 0===t?24:t,r=e.className,i=void 0===r?"wp-parsely-icon":r;return(0,p.jsxs)(f.SVG,{className:i,height:n,viewBox:"0 0 60 65",width:n,xmlns:"http://www.w3.org/2000/svg",children:[(0,p.jsx)(f.Path,{fill:"#5ba745",d:"M23.72,51.53c0-.18,0-.34-.06-.52a13.11,13.11,0,0,0-2.1-5.53A14.74,14.74,0,0,0,19.12,43c-.27-.21-.5-.11-.51.22l-.24,3.42c0,.33-.38.35-.49,0l-1.5-4.8a1.4,1.4,0,0,0-.77-.78,23.91,23.91,0,0,0-3.1-.84c-1.38-.24-3.39-.39-3.39-.39-.34,0-.45.21-.25.49l2.06,3.76c.2.27,0,.54-.29.33l-4.51-3.6a3.68,3.68,0,0,0-2.86-.48c-1,.16-2.44.46-2.44.46a.68.68,0,0,0-.39.25.73.73,0,0,0-.14.45S.41,43,.54,44a3.63,3.63,0,0,0,1.25,2.62L6.48,50c.28.2.09.49-.23.37l-4.18-.94c-.32-.12-.5,0-.4.37,0,0,.69,1.89,1.31,3.16a24,24,0,0,0,1.66,2.74,1.34,1.34,0,0,0,1,.52l5,.13c.33,0,.41.38.1.48L7.51,58c-.31.1-.34.35-.07.55a14.29,14.29,0,0,0,3.05,1.66,13.09,13.09,0,0,0,5.9.5,25.13,25.13,0,0,0,4.34-1,9.55,9.55,0,0,1-.08-1.2,9.32,9.32,0,0,1,3.07-6.91"}),(0,p.jsx)(f.Path,{fill:"#5ba745",d:"M59.7,41.53a.73.73,0,0,0-.14-.45.68.68,0,0,0-.39-.25s-1.43-.3-2.44-.46a3.64,3.64,0,0,0-2.86.48l-4.51,3.6c-.26.21-.49-.06-.29-.33l2.06-3.76c.2-.28.09-.49-.25-.49,0,0-2,.15-3.39.39a23.91,23.91,0,0,0-3.1.84,1.4,1.4,0,0,0-.77.78l-1.5,4.8c-.11.32-.48.3-.49,0l-.24-3.42c0-.33-.24-.43-.51-.22a14.74,14.74,0,0,0-2.44,2.47A13.11,13.11,0,0,0,36.34,51c0,.18,0,.34-.06.52a9.26,9.26,0,0,1,3,8.1,24.1,24.1,0,0,0,4.34,1,13.09,13.09,0,0,0,5.9-.5,14.29,14.29,0,0,0,3.05-1.66c.27-.2.24-.45-.07-.55l-3.22-1.17c-.31-.1-.23-.47.1-.48l5-.13a1.38,1.38,0,0,0,1-.52A24.6,24.6,0,0,0,57,52.92c.61-1.27,1.31-3.16,1.31-3.16.1-.33-.08-.49-.4-.37l-4.18.94c-.32.12-.51-.17-.23-.37l4.69-3.34A3.63,3.63,0,0,0,59.46,44c.13-1,.24-2.47.24-2.47"}),(0,p.jsx)(f.Path,{fill:"#5ba745",d:"M46.5,25.61c0-.53-.35-.72-.8-.43l-4.86,2.66c-.45.28-.56-.27-.23-.69l4.66-6.23a2,2,0,0,0,.28-1.68,36.51,36.51,0,0,0-2.19-4.89,34,34,0,0,0-2.81-3.94c-.33-.41-.74-.35-.91.16l-2.28,5.68c-.16.5-.6.48-.59-.05l.28-8.93a2.54,2.54,0,0,0-.66-1.64S35,4.27,33.88,3.27,30.78.69,30.78.69a1.29,1.29,0,0,0-1.54,0s-1.88,1.49-3.12,2.59-2.48,2.35-2.48,2.35A2.5,2.5,0,0,0,23,7.27l.27,8.93c0,.53-.41.55-.58.05l-2.29-5.69c-.17-.5-.57-.56-.91-.14a35.77,35.77,0,0,0-3,4.2,35.55,35.55,0,0,0-2,4.62,2,2,0,0,0,.27,1.67l4.67,6.24c.33.42.23,1-.22.69l-4.87-2.66c-.45-.29-.82-.1-.82.43a18.6,18.6,0,0,0,.83,5.07,20.16,20.16,0,0,0,5.37,7.77c3.19,3,5.93,7.8,7.45,11.08A9.6,9.6,0,0,1,30,49.09a9.31,9.31,0,0,1,2.86.45c1.52-3.28,4.26-8.11,7.44-11.09a20.46,20.46,0,0,0,5.09-7,19,19,0,0,0,1.11-5.82"}),(0,p.jsx)(f.Path,{fill:"#5ba745",d:"M36.12,58.44A6.12,6.12,0,1,1,30,52.32a6.11,6.11,0,0,1,6.12,6.12"})]})},L=function(){return L=Object.assign||function(e){for(var t,n=1,r=arguments.length;nhere.',"wp-parsely"):s.code===$.ParselySuggestionsApiOpenAiError||s.code===$.ParselySuggestionsApiOpenAiUnavailable?s.message=(0,_.__)("The Parse.ly API returned an internal server error. Please retry with a different input, or try again later.","wp-parsely"):s.code===$.HttpRequestFailed&&s.message.includes("cURL error 28")?s.message=(0,_.__)("The Parse.ly API did not respond in a timely manner. Please try again later.","wp-parsely"):s.code===$.ParselySuggestionsApiSchemaError?s.message=(0,_.__)("The Parse.ly API returned a validation error. Please try again with different parameters.","wp-parsely"):s.code===$.ParselySuggestionsApiNoData?s.message=(0,_.__)("The Parse.ly API couldn't find any relevant data to fulfill the request. Please retry with a different input.","wp-parsely"):s.code===$.ParselySuggestionsApiOpenAiSchema?s.message=(0,_.__)("The Parse.ly API returned an incorrect response. Please try again later.","wp-parsely"):s.code===$.ParselySuggestionsApiAuthUnavailable&&(s.message=(0,_.__)("The Parse.ly API is currently unavailable. Please try again later.","wp-parsely")),s}return re(t,e),t.prototype.Message=function(e){return void 0===e&&(e=null),[$.PluginCredentialsNotSetMessageDetected,$.PluginSettingsSiteIdNotSet,$.PluginSettingsApiSecretNotSet].includes(this.code)?K(e):(this.code===$.FetchError&&(this.hint=this.Hint((0,_.__)("This error can sometimes be caused by ad-blockers or browser tracking protections. Please add this site to any applicable allow lists and try again.","wp-parsely"))),this.code!==$.ParselyApiForbidden&&this.code!==$.ParselySuggestionsApiNoAuthentication||(this.hint=this.Hint((0,_.__)("Please ensure that the Site ID and API Secret given in the plugin's settings are correct.","wp-parsely"))),this.code===$.HttpRequestFailed&&(this.hint=this.Hint((0,_.__)("The Parse.ly API cannot be reached. Please verify that you are online.","wp-parsely"))),(0,p.jsx)(W,{className:null==e?void 0:e.className,testId:"error",children:"

".concat(this.message,"

").concat(this.hint?this.hint:"")}))},t.prototype.Hint=function(e){return'

'.concat((0,_.__)("Hint:","wp-parsely")," ").concat(e,"

")},t.prototype.createErrorSnackbar=function(){//.test(this.message)||(0,h.dispatch)("core/notices").createNotice("error",this.message,{type:"snackbar"})},t}(Error),se=function(e){var t=e.isDetectingEnabled,n=e.onLinkChange,r=e.onLinkRemove,i=e.onLinkAdd,s=e.debounceValue,o=void 0===s?500:s,a=(0,h.useSelect)((function(e){return{blocks:(0,e("core/block-editor").getBlocks)()}}),[]).blocks,l=(0,b.useRef)(a),c=(0,b.useRef)(t);return(0,b.useEffect)((function(){var e=(0,z.debounce)((function(){for(var t=[],s=0;s0)return r(e.innerBlocks,t[s].innerBlocks);if(JSON.stringify(e)!==JSON.stringify(t[s])){var o=t[s],a=i.parseFromString(e.attributes.content||"","text/html"),l=i.parseFromString((null==o?void 0:o.attributes.content)||"","text/html"),c=Array.from(a.querySelectorAll("a[data-smartlink]")),u=Array.from(l.querySelectorAll("a[data-smartlink]")),d=c.filter((function(e){return!u.some((function(t){return t.dataset.smartlink===e.dataset.smartlink}))})),p=u.filter((function(e){return!c.some((function(t){return t.dataset.smartlink===e.dataset.smartlink}))})),f=c.filter((function(e){var t=u.find((function(t){return t.dataset.smartlink===e.dataset.smartlink}));return t&&t.outerHTML!==e.outerHTML}));(d.length>0||p.length>0||f.length>0)&&n.push({block:e,prevBlock:o,addedLinks:d,removedLinks:p,changedLinks:f})}}}))};return r(e,t),n}(a,l.current);o.length>0&&(o.forEach((function(e){e.changedLinks.length>0&&n&&n(e),e.addedLinks.length>0&&i&&i(e),e.removedLinks.length>0&&r&&r(e)})),l.current=a)}),o);return e(t),function(){e.cancel()}}),[a,o,t,i,n,r]),null},oe=function(e){var t=e.value,n=e.onChange,r=e.max,i=e.min,s=e.suffix,o=e.size,a=e.label,l=e.initialPosition,c=e.disabled,u=e.className;return(0,p.jsxs)("div",{className:"parsely-inputrange-control ".concat(u||""),children:[(0,p.jsx)(f.__experimentalHeading,{className:"parsely-inputrange-control__label",level:3,children:a}),(0,p.jsxs)("div",{className:"parsely-inputrange-control__controls",children:[(0,p.jsx)(f.__experimentalNumberControl,{disabled:c,value:t,suffix:(0,p.jsx)(f.__experimentalInputControlSuffixWrapper,{children:s}),size:null!=o?o:"__unstable-large",min:i,max:r,onChange:function(e){var t=parseInt(e,10);isNaN(t)||n(t)}}),(0,p.jsx)(f.RangeControl,{disabled:c,value:t,showTooltip:!1,initialPosition:l,onChange:function(e){n(e)},withInputField:!1,min:i,max:r})]})]})},ae=function(e,t,n,r){return new(n||(n=Promise))((function(i,s){function o(e){try{l(r.next(e))}catch(e){s(e)}}function a(e){try{l(r.throw(e))}catch(e){s(e)}}function l(e){var t;e.done?i(e.value):(t=e.value,t instanceof n?t:new n((function(e){e(t)}))).then(o,a)}l((r=r.apply(e,t||[])).next())}))},le=function(e,t){var n,r,i,s,o={label:0,sent:function(){if(1&i[0])throw i[1];return i[1]},trys:[],ops:[]};return s={next:a(0),throw:a(1),return:a(2)},"function"==typeof Symbol&&(s[Symbol.iterator]=function(){return this}),s;function a(a){return function(l){return function(a){if(n)throw new TypeError("Generator is already executing.");for(;s&&(s=0,a[0]&&(o=0)),o;)try{if(n=1,r&&(i=2&a[0]?r.return:a[0]?r.throw||((i=r.return)&&i.call(r),0):r.next)&&!(i=i.call(r,a[1])).done)return i;switch(r=0,i&&(a=[2&a[0],i.value]),a[0]){case 0:case 1:i=a;break;case 4:return o.label++,{value:a[1],done:!1};case 5:o.label++,r=a[1],a=[0];continue;case 7:a=o.ops.pop(),o.trys.pop();continue;default:if(!((i=(i=o.trys).length>0&&i[i.length-1])||6!==a[0]&&2!==a[0])){o=0;continue}if(3===a[0]&&(!i||a[1]>i[0]&&a[1]0&&i[i.length-1])||6!==a[0]&&2!==a[0])){o=0;continue}if(3===a[0]&&(!i||a[1]>i[0]&&a[1]0&&i[i.length-1])||6!==a[0]&&2!==a[0])){o=0;continue}if(3===a[0]&&(!i||a[1]>i[0]&&a[1]=1e4&&(clearInterval(s),n("Telemetry library not loaded"))}),100);else n("Telemetry not enabled")}))},e.prototype.trackEvent=function(t,n){var r;this.isLoaded?(0!==t.indexOf(e.TRACKS_PREFIX)&&(t=e.TRACKS_PREFIX+t),this.isEventNameValid(t)?(n=this.prepareProperties(n),null===(r=this._tkq)||void 0===r||r.push(["recordEvent",t,n])):console.error("Error tracking event: Invalid event name")):console.error("Error tracking event: Telemetry not loaded")},e.prototype.isTelemetryEnabled=function(){return this.isEnabled},e.prototype.isProprietyValid=function(t){return e.PROPERTY_REGEX.test(t)},e.prototype.isEventNameValid=function(t){return e.EVENT_NAME_REGEX.test(t)},e.prototype.prepareProperties=function(e){return(e=this.sanitizeProperties(e)).parsely_version=wpParselyTracksTelemetry.version,wpParselyTracksTelemetry.user&&(e._ut=wpParselyTracksTelemetry.user.type,e._ui=wpParselyTracksTelemetry.user.id),wpParselyTracksTelemetry.vipgo_env&&(e.vipgo_env=wpParselyTracksTelemetry.vipgo_env),this.sanitizeProperties(e)},e.prototype.sanitizeProperties=function(e){var t=this,n={};return Object.keys(e).forEach((function(r){t.isProprietyValid(r)&&(n[r]=e[r])})),n},e.TRACKS_PREFIX="wpparsely_",e.EVENT_NAME_REGEX=/^(([a-z0-9]+)_){2}([a-z0-9_]+)$/,e.PROPERTY_REGEX=/^[a-z_][a-z0-9_]*$/,e}(),j=(P.trackEvent,function(){return(0,p.jsx)(f.SVG,{"aria-hidden":"true",version:"1.1",viewBox:"0 0 15 15",width:"15",height:"15",xmlns:"http://www.w3.org/2000/svg",children:(0,p.jsx)(f.Path,{d:"M0 14.0025V11.0025L7.5 3.5025L10.5 6.5025L3 14.0025H0ZM12 5.0025L13.56 3.4425C14.15 2.8525 14.15 1.9025 13.56 1.3225L12.68 0.4425C12.09 -0.1475 11.14 -0.1475 10.56 0.4425L9 2.0025L12 5.0025Z"})})}),T=function(e){var t=e.size,n=void 0===t?24:t,r=e.className,i=void 0===r?"wp-parsely-icon":r;return(0,p.jsxs)(f.SVG,{className:i,height:n,viewBox:"0 0 60 65",width:n,xmlns:"http://www.w3.org/2000/svg",children:[(0,p.jsx)(f.Path,{fill:"#5ba745",d:"M23.72,51.53c0-.18,0-.34-.06-.52a13.11,13.11,0,0,0-2.1-5.53A14.74,14.74,0,0,0,19.12,43c-.27-.21-.5-.11-.51.22l-.24,3.42c0,.33-.38.35-.49,0l-1.5-4.8a1.4,1.4,0,0,0-.77-.78,23.91,23.91,0,0,0-3.1-.84c-1.38-.24-3.39-.39-3.39-.39-.34,0-.45.21-.25.49l2.06,3.76c.2.27,0,.54-.29.33l-4.51-3.6a3.68,3.68,0,0,0-2.86-.48c-1,.16-2.44.46-2.44.46a.68.68,0,0,0-.39.25.73.73,0,0,0-.14.45S.41,43,.54,44a3.63,3.63,0,0,0,1.25,2.62L6.48,50c.28.2.09.49-.23.37l-4.18-.94c-.32-.12-.5,0-.4.37,0,0,.69,1.89,1.31,3.16a24,24,0,0,0,1.66,2.74,1.34,1.34,0,0,0,1,.52l5,.13c.33,0,.41.38.1.48L7.51,58c-.31.1-.34.35-.07.55a14.29,14.29,0,0,0,3.05,1.66,13.09,13.09,0,0,0,5.9.5,25.13,25.13,0,0,0,4.34-1,9.55,9.55,0,0,1-.08-1.2,9.32,9.32,0,0,1,3.07-6.91"}),(0,p.jsx)(f.Path,{fill:"#5ba745",d:"M59.7,41.53a.73.73,0,0,0-.14-.45.68.68,0,0,0-.39-.25s-1.43-.3-2.44-.46a3.64,3.64,0,0,0-2.86.48l-4.51,3.6c-.26.21-.49-.06-.29-.33l2.06-3.76c.2-.28.09-.49-.25-.49,0,0-2,.15-3.39.39a23.91,23.91,0,0,0-3.1.84,1.4,1.4,0,0,0-.77.78l-1.5,4.8c-.11.32-.48.3-.49,0l-.24-3.42c0-.33-.24-.43-.51-.22a14.74,14.74,0,0,0-2.44,2.47A13.11,13.11,0,0,0,36.34,51c0,.18,0,.34-.06.52a9.26,9.26,0,0,1,3,8.1,24.1,24.1,0,0,0,4.34,1,13.09,13.09,0,0,0,5.9-.5,14.29,14.29,0,0,0,3.05-1.66c.27-.2.24-.45-.07-.55l-3.22-1.17c-.31-.1-.23-.47.1-.48l5-.13a1.38,1.38,0,0,0,1-.52A24.6,24.6,0,0,0,57,52.92c.61-1.27,1.31-3.16,1.31-3.16.1-.33-.08-.49-.4-.37l-4.18.94c-.32.12-.51-.17-.23-.37l4.69-3.34A3.63,3.63,0,0,0,59.46,44c.13-1,.24-2.47.24-2.47"}),(0,p.jsx)(f.Path,{fill:"#5ba745",d:"M46.5,25.61c0-.53-.35-.72-.8-.43l-4.86,2.66c-.45.28-.56-.27-.23-.69l4.66-6.23a2,2,0,0,0,.28-1.68,36.51,36.51,0,0,0-2.19-4.89,34,34,0,0,0-2.81-3.94c-.33-.41-.74-.35-.91.16l-2.28,5.68c-.16.5-.6.48-.59-.05l.28-8.93a2.54,2.54,0,0,0-.66-1.64S35,4.27,33.88,3.27,30.78.69,30.78.69a1.29,1.29,0,0,0-1.54,0s-1.88,1.49-3.12,2.59-2.48,2.35-2.48,2.35A2.5,2.5,0,0,0,23,7.27l.27,8.93c0,.53-.41.55-.58.05l-2.29-5.69c-.17-.5-.57-.56-.91-.14a35.77,35.77,0,0,0-3,4.2,35.55,35.55,0,0,0-2,4.62,2,2,0,0,0,.27,1.67l4.67,6.24c.33.42.23,1-.22.69l-4.87-2.66c-.45-.29-.82-.1-.82.43a18.6,18.6,0,0,0,.83,5.07,20.16,20.16,0,0,0,5.37,7.77c3.19,3,5.93,7.8,7.45,11.08A9.6,9.6,0,0,1,30,49.09a9.31,9.31,0,0,1,2.86.45c1.52-3.28,4.26-8.11,7.44-11.09a20.46,20.46,0,0,0,5.09-7,19,19,0,0,0,1.11-5.82"}),(0,p.jsx)(f.Path,{fill:"#5ba745",d:"M36.12,58.44A6.12,6.12,0,1,1,30,52.32a6.11,6.11,0,0,1,6.12,6.12"})]})},L=function(){return L=Object.assign||function(e){for(var t,n=1,r=arguments.length;nhere.',"wp-parsely"):s.code===$.ParselySuggestionsApiOpenAiError||s.code===$.ParselySuggestionsApiOpenAiUnavailable?s.message=(0,_.__)("The Parse.ly API returned an internal server error. Please retry with a different input, or try again later.","wp-parsely"):s.code===$.HttpRequestFailed&&s.message.includes("cURL error 28")?s.message=(0,_.__)("The Parse.ly API did not respond in a timely manner. Please try again later.","wp-parsely"):s.code===$.ParselySuggestionsApiSchemaError?s.message=(0,_.__)("The Parse.ly API returned a validation error. Please try again with different parameters.","wp-parsely"):s.code===$.ParselySuggestionsApiNoData?s.message=(0,_.__)("The Parse.ly API couldn't find any relevant data to fulfill the request. Please retry with a different input.","wp-parsely"):s.code===$.ParselySuggestionsApiOpenAiSchema?s.message=(0,_.__)("The Parse.ly API returned an incorrect response. Please try again later.","wp-parsely"):s.code===$.ParselySuggestionsApiAuthUnavailable&&(s.message=(0,_.__)("The Parse.ly API is currently unavailable. Please try again later.","wp-parsely")),s}return re(t,e),t.prototype.Message=function(e){return void 0===e&&(e=null),[$.PluginCredentialsNotSetMessageDetected,$.PluginSettingsSiteIdNotSet,$.PluginSettingsApiSecretNotSet].includes(this.code)?K(e):(this.code===$.FetchError&&(this.hint=this.Hint((0,_.__)("This error can sometimes be caused by ad-blockers or browser tracking protections. Please add this site to any applicable allow lists and try again.","wp-parsely"))),this.code!==$.ParselyApiForbidden&&this.code!==$.ParselySuggestionsApiNoAuthentication||(this.hint=this.Hint((0,_.__)("Please ensure that the Site ID and API Secret given in the plugin's settings are correct.","wp-parsely"))),this.code===$.HttpRequestFailed&&(this.hint=this.Hint((0,_.__)("The Parse.ly API cannot be reached. Please verify that you are online.","wp-parsely"))),(0,p.jsx)(W,{className:null==e?void 0:e.className,testId:"error",children:"

".concat(this.message,"

").concat(this.hint?this.hint:"")}))},t.prototype.Hint=function(e){return'

'.concat((0,_.__)("Hint:","wp-parsely")," ").concat(e,"

")},t.prototype.createErrorSnackbar=function(){//.test(this.message)||(0,h.dispatch)("core/notices").createNotice("error",this.message,{type:"snackbar"})},t}(Error),se=function(e){var t=e.isDetectingEnabled,n=e.onLinkChange,r=e.onLinkRemove,i=e.onLinkAdd,s=e.debounceValue,o=void 0===s?500:s,a=(0,h.useSelect)((function(e){return{blocks:(0,e("core/block-editor").getBlocks)()}}),[]).blocks,l=(0,b.useRef)(a),c=(0,b.useRef)(t);return(0,b.useEffect)((function(){var e=(0,z.debounce)((function(){for(var t=[],s=0;s0)return r(e.innerBlocks,t[s].innerBlocks);if(JSON.stringify(e)!==JSON.stringify(t[s])){var o=t[s],a=i.parseFromString(e.attributes.content||"","text/html"),l=i.parseFromString((null==o?void 0:o.attributes.content)||"","text/html"),c=Array.from(a.querySelectorAll("a[data-smartlink]")),u=Array.from(l.querySelectorAll("a[data-smartlink]")),d=c.filter((function(e){return!u.some((function(t){return t.dataset.smartlink===e.dataset.smartlink}))})),p=u.filter((function(e){return!c.some((function(t){return t.dataset.smartlink===e.dataset.smartlink}))})),f=c.filter((function(e){var t=u.find((function(t){return t.dataset.smartlink===e.dataset.smartlink}));return t&&t.outerHTML!==e.outerHTML}));(d.length>0||p.length>0||f.length>0)&&n.push({block:e,prevBlock:o,addedLinks:d,removedLinks:p,changedLinks:f})}}}))};return r(e,t),n}(a,l.current);o.length>0&&(o.forEach((function(e){e.changedLinks.length>0&&n&&n(e),e.addedLinks.length>0&&i&&i(e),e.removedLinks.length>0&&r&&r(e)})),l.current=a)}),o);return e(t),function(){e.cancel()}}),[a,o,t,i,n,r]),null},oe=function(e){var t=e.value,n=e.onChange,r=e.max,i=e.min,s=e.suffix,o=e.size,a=e.label,l=e.initialPosition,c=e.disabled,u=e.className;return(0,p.jsxs)("div",{className:"parsely-inputrange-control ".concat(u||""),children:[(0,p.jsx)(f.__experimentalHeading,{className:"parsely-inputrange-control__label",level:3,children:a}),(0,p.jsxs)("div",{className:"parsely-inputrange-control__controls",children:[(0,p.jsx)(f.__experimentalNumberControl,{disabled:c,value:t,suffix:(0,p.jsx)(f.__experimentalInputControlSuffixWrapper,{children:s}),size:null!=o?o:"__unstable-large",min:i,max:r,onChange:function(e){var t=parseInt(e,10);isNaN(t)||n(t)}}),(0,p.jsx)(f.RangeControl,{disabled:c,value:t,showTooltip:!1,initialPosition:l,onChange:function(e){n(e)},withInputField:!1,min:i,max:r})]})]})},ae=function(e,t,n,r){return new(n||(n=Promise))((function(i,s){function o(e){try{l(r.next(e))}catch(e){s(e)}}function a(e){try{l(r.throw(e))}catch(e){s(e)}}function l(e){var t;e.done?i(e.value):(t=e.value,t instanceof n?t:new n((function(e){e(t)}))).then(o,a)}l((r=r.apply(e,t||[])).next())}))},le=function(e,t){var n,r,i,s,o={label:0,sent:function(){if(1&i[0])throw i[1];return i[1]},trys:[],ops:[]};return s={next:a(0),throw:a(1),return:a(2)},"function"==typeof Symbol&&(s[Symbol.iterator]=function(){return this}),s;function a(a){return function(l){return function(a){if(n)throw new TypeError("Generator is already executing.");for(;s&&(s=0,a[0]&&(o=0)),o;)try{if(n=1,r&&(i=2&a[0]?r.return:a[0]?r.throw||((i=r.return)&&i.call(r),0):r.next)&&!(i=i.call(r,a[1])).done)return i;switch(r=0,i&&(a=[2&a[0],i.value]),a[0]){case 0:case 1:i=a;break;case 4:return o.label++,{value:a[1],done:!1};case 5:o.label++,r=a[1],a=[0];continue;case 7:a=o.ops.pop(),o.trys.pop();continue;default:if(!((i=(i=o.trys).length>0&&i[i.length-1])||6!==a[0]&&2!==a[0])){o=0;continue}if(3===a[0]&&(!i||a[1]>i[0]&&a[1]0&&i[i.length-1])||6!==a[0]&&2!==a[0])){o=0;continue}if(3===a[0]&&(!i||a[1]>i[0]&&a[1]0&&i[i.length-1])||6!==a[0]&&2!==a[0])){o=0;continue}if(3===a[0]&&(!i||a[1]>i[0]&&a[1]0&&i[i.length-1])||6!==a[0]&&2!==a[0])){o=0;continue}if(3===a[0]&&(!i||a[1]>i[0]&&a[1]0&&i[i.length-1])||6!==a[0]&&2!==a[0])){o=0;continue}if(3===a[0]&&(!i||a[1]>i[0]&&a[1]

","\n\x3c!-- /wp:paragraph --\x3e");t&&h((0,Q.parse)(n))}),[s]),(0,p.jsxs)("div",{className:"smart-linking-review-suggestion",children:[(0,p.jsx)(f.KeyboardShortcuts,{shortcuts:{left:o,right:a,up:o,down:a}}),(0,p.jsx)("div",{className:"review-suggestion-post-title",children:null===(t=s.post_data)||void 0===t?void 0:t.title}),(0,p.jsxs)("div",{className:"review-suggestion-preview",children:[!(null===(n=s.post_data)||void 0===n?void 0:n.is_first_paragraph)&&(0,p.jsx)($e,{topOrBottom:"top"}),(0,p.jsx)(Ze,{block:d[0],link:s,useOriginalBlock:!0}),!(null===(r=s.post_data)||void 0===r?void 0:r.is_last_paragraph)&&(0,p.jsx)($e,{topOrBottom:"bottom"})]}),(0,p.jsx)(f.__experimentalDivider,{}),(0,p.jsx)(We,{link:s}),(0,p.jsxs)("div",{className:"review-controls",children:[(0,p.jsx)(f.Tooltip,{shortcut:"←",text:(0,_.__)("Previous","wp-parsely"),children:(0,p.jsx)(f.Button,{disabled:!l,className:"wp-parsely-review-suggestion-previous",onClick:o,icon:He,children:(0,_.__)("Previous","wp-parsely")})}),(0,p.jsx)("div",{className:"reviews-controls-middle",children:(0,p.jsx)(f.Button,{target:"_blank",href:(null===(i=s.post_data)||void 0===i?void 0:i.edit_link)+"&smart-link="+s.uid,variant:"secondary",onClick:function(){P.trackEvent("smart_linking_open_in_editor_pressed",{type:"inbound",uid:s.uid})},children:(0,_.__)("Open in the Editor","wp-parsely")})}),(0,p.jsx)(f.Tooltip,{shortcut:"→",text:(0,_.__)("Next","wp-parsely"),children:(0,p.jsxs)(f.Button,{disabled:!c,onClick:a,className:"wp-parsely-review-suggestion-next",children:[(0,_.__)("Next","wp-parsely"),(0,p.jsx)(X,{icon:ze})]})})]})]})},Ye=function(e){var t=e.size,n=void 0===t?24:t,r=e.className,i=void 0===r?"wp-parsely-icon":r;return(0,p.jsxs)(f.SVG,{xmlns:"http://www.w3.org/2000/svg",className:i,width:n,height:n,viewBox:"0 0 24 24",fill:"none",children:[(0,p.jsx)(f.Path,{d:"M8.18983 5.90381L8.83642 7.54325L10.4758 8.18983L8.83642 8.8364L8.18983 10.4759L7.54324 8.8364L5.90381 8.18983L7.54324 7.54325L8.18983 5.90381Z"}),(0,p.jsx)(f.Path,{d:"M15.048 5.90381L15.9101 8.08972L18.0961 8.95186L15.9101 9.81397L15.048 11.9999L14.1859 9.81397L12 8.95186L14.1859 8.08972L15.048 5.90381Z"}),(0,p.jsx)(f.Path,{d:"M11.238 10.4761L12.3157 13.2085L15.048 14.2861L12.3157 15.3638L11.238 18.0962L10.1603 15.3638L7.42798 14.2861L10.1603 13.2085L11.238 10.4761Z"})]})},Je=function(e,t,n){if(n||2===arguments.length)for(var r,i=0,s=t.length;ii.bottom)&&(n.scrollTop=r.offsetTop-n.offsetTop)}}}}),[t,l]);var u=function(){var e=document.querySelector(".smart-linking-review-sidebar-tabs [data-active-item]"),t=null==e?void 0:e.nextElementSibling;t||(t=document.querySelector('.smart-linking-review-sidebar-tabs [role="tab"]')),t&&t.click()},d=(0,p.jsxs)("span",{className:"smart-linking-menu-label",children:[(0,_.__)("NEW","wp-parsely"),(0,p.jsx)(Ye,{})]}),h=[];n&&n.length>0&&h.push({name:"outbound",title:(0,_.__)("Outbound","wp-parsely")}),r&&r.length>0&&h.push({name:"inbound",title:(0,_.__)("Inbound","wp-parsely")});var v="outbound";return h=h.filter((function(e){return"outbound"===e.name&&r&&0===r.length&&(e.title=(0,_.__)("Outbound Smart Links","wp-parsely"),v="outbound"),"inbound"===e.name&&n&&0===n.length&&(e.title=(0,_.__)("Inbound Smart Links","wp-parsely"),v="inbound"),e})),(0,p.jsxs)("div",{className:"smart-linking-review-sidebar",ref:s,children:[(0,p.jsx)(f.KeyboardShortcuts,{shortcuts:{tab:function(){return u()},"shift+tab":function(){return u()}}}),(0,p.jsx)(f.TabPanel,{className:"smart-linking-review-sidebar-tabs",initialTabName:v,tabs:h,onSelect:function(e){var t,s;"outbound"===e&&n&&n.length>0&&i(n[0]),"inbound"===e&&r&&r.length>0&&i(r[0]),P.trackEvent("smart_linking_modal_tab_selected",{tab:e,total_inbound:null!==(t=null==r?void 0:r.length)&&void 0!==t?t:0,total_outbound:null!==(s=null==n?void 0:n.length)&&void 0!==s?s:0})},children:function(e){return(0,p.jsxs)(p.Fragment,{children:["outbound"===e.name&&(0,p.jsx)(p.Fragment,{children:n&&0!==n.length?n.map((function(e,n){return(0,p.jsxs)(f.MenuItem,{ref:function(e){o.current[n]=e},className:(null==t?void 0:t.uid)===e.uid?"is-selected":"",role:"menuitemradio",isSelected:(null==t?void 0:t.uid)===e.uid,onClick:function(){return i(e)},children:[(0,p.jsx)("span",{className:"smart-linking-menu-item",children:e.text}),!e.applied&&d]},e.uid)})):(0,p.jsxs)(p.Fragment,{children:[" ",(0,_.__)("No outbound links found.","wp-parsely")]})}),"inbound"===e.name&&(0,p.jsxs)(p.Fragment,{children:[(0,p.jsx)("div",{className:"review-sidebar-tip",children:(0,_.__)("This section shows external posts that link back to the current post.","wp-parsely")}),r&&0!==r.length?r.map((function(e,r){var s;return(0,p.jsx)(f.MenuItem,{ref:function(e){o.current[(n?n.length:0)+r]=e},className:(null==t?void 0:t.uid)===e.uid?"is-selected":"",role:"menuitemradio",isSelected:(null==t?void 0:t.uid)===e.uid,onClick:function(){return i(e)},children:(0,p.jsx)("span",{className:"smart-linking-menu-item",children:null===(s=e.post_data)||void 0===s?void 0:s.title})},e.uid)})):(0,p.jsxs)(p.Fragment,{children:[" ",(0,_.__)("No inbound links found.","wp-parsely")]})]})]})}})]})},Xe=(0,p.jsx)(x.SVG,{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 24 24",children:(0,p.jsx)(x.Path,{d:"M12 13.06l3.712 3.713 1.061-1.06L13.061 12l3.712-3.712-1.06-1.06L12 10.938 8.288 7.227l-1.061 1.06L10.939 12l-3.712 3.712 1.06 1.061L12 13.061z"})}),et=(0,p.jsx)(x.SVG,{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 24 24",children:(0,p.jsx)(x.Path,{d:"M16.7 7.1l-6.3 8.5-3.3-2.5-.9 1.2 4.5 3.4L17.9 8z"})}),tt=function(e){var t,n,r,i,s=null===(t=e.link.match)||void 0===t?void 0:t.blockId,o=(0,h.useSelect)((function(e){var t=e("core/block-editor"),n=t.getBlock,r=t.getBlockParents;return s?{block:n(s),parents:r(s).map((function(e){return n(e)})).filter((function(e){return void 0!==e}))}:{block:void 0,parents:[]}}),[s]),a=o.block,l=o.parents;return a?(0,p.jsxs)("div",{className:"review-suggestions-breadcrumbs",children:[l.map((function(e,t){var n;return(0,p.jsxs)("span",{children:[(0,p.jsx)("span",{className:"breadcrumbs-parent-block",children:null===(n=(0,Q.getBlockType)(e.name))||void 0===n?void 0:n.title}),(0,p.jsx)("span",{className:"breadcrumbs-parent-separator",children:" / "})]},t)})),(0,p.jsxs)("span",{className:"breadcrumbs-current-block",children:[(0,p.jsx)("span",{className:"breadcrumbs-current-block-type",children:null===(n=(0,Q.getBlockType)(a.name))||void 0===n?void 0:n.title}),(null===(i=null===(r=a.attributes)||void 0===r?void 0:r.metadata)||void 0===i?void 0:i.name)&&(0,p.jsx)("span",{className:"breadcrumbs-current-block-name",children:a.attributes.metadata.name})]})]}):(0,p.jsx)(p.Fragment,{})},nt=function(e){var t,n=e.link,r=(0,b.useState)(n.href),i=r[0],s=r[1],o=(0,b.useState)(null===(t=n.destination)||void 0===t?void 0:t.post_type),a=o[0],l=o[1],c=(0,b.useRef)(null),u=(0,h.useDispatch)(Te).updateSmartLink;return(0,b.useEffect)((function(){n.destination?l(n.destination.post_type):(l((0,_.__)("External","wp-parsely")),De.getInstance().getPostTypeByURL(n.href).then((function(e){e&&l(e.post_type),n.destination=e,u(n)})))}),[n,u]),(0,b.useEffect)((function(){var e=function(){if(c.current){var e=c.current.offsetWidth,t=Math.floor(e/8);s(function(e,t){var n=e.replace(/(^\w+:|^)\/\//,"").replace(/^www\./,"");if(!t||n.length<=t)return n;var r=n.split("/")[0],i=n.substring(r.length);t-=r.length;var s=Math.floor((t-3)/2),o=i.substring(0,s),a=i.substring(i.length-s);return"".concat(r).concat(o,"...").concat(a)}(n.href,t))}};return e(),window.addEventListener("resize",e),function(){window.removeEventListener("resize",e)}}),[n]),(0,p.jsx)(f.MenuItem,{ref:c,info:i,iconPosition:"left",icon:Ge,shortcut:a,className:"block-editor-link-control__search-item wp-parsely-link-suggestion-link-details",children:n.title})},rt=function(e){var t=e.link,n=e.onNext,r=e.onPrevious,i=e.onAccept,s=e.onReject,o=e.onRemove,a=e.onSelectInEditor,l=e.hasPrevious,c=e.hasNext;if(t&&void 0!==t.post_data)return(0,p.jsx)(Ke,{link:t,onNext:n,onPrevious:r,onAccept:i,onReject:s,onRemove:o,onSelectInEditor:a,hasPrevious:l,hasNext:c});if(!(null==t?void 0:t.match))return(0,p.jsx)(p.Fragment,{children:(0,_.__)("This Smart Link does not have any matches in the current content.","wp-parsely")});var u=t.match.blockId,d=(0,h.select)("core/block-editor").getBlock(u),v=t.applied;return d?(0,p.jsxs)("div",{className:"smart-linking-review-suggestion",children:[(0,p.jsx)(f.KeyboardShortcuts,{shortcuts:{left:r,right:n,up:r,down:n,a:function(){t&&!t.applied&&i()},r:function(){t&&(t.applied?o():s())}}}),(0,p.jsx)(tt,{link:t}),(0,p.jsx)("div",{className:"review-suggestion-preview",children:(0,p.jsx)(Ze,{block:d,link:t})}),(0,p.jsx)(f.__experimentalDivider,{}),(0,p.jsx)(nt,{link:t}),(0,p.jsxs)("div",{className:"review-controls",children:[(0,p.jsx)(f.Tooltip,{shortcut:"←",text:(0,_.__)("Previous","wp-parsely"),children:(0,p.jsx)(f.Button,{disabled:!l,className:"wp-parsely-review-suggestion-previous",onClick:r,icon:He,children:(0,_.__)("Previous","wp-parsely")})}),(0,p.jsxs)("div",{className:"reviews-controls-middle",children:[!v&&(0,p.jsxs)(p.Fragment,{children:[(0,p.jsx)(f.Tooltip,{shortcut:"R",text:(0,_.__)("Reject","wp-parsely"),children:(0,p.jsx)(f.Button,{className:"wp-parsely-review-suggestion-reject",icon:Xe,onClick:s,variant:"secondary",children:(0,_.__)("Reject","wp-parsely")})}),(0,p.jsx)(f.Tooltip,{shortcut:"A",text:(0,_.__)("Accept","wp-parsely"),children:(0,p.jsx)(f.Button,{className:"wp-parsely-review-suggestion-accept",icon:et,onClick:i,variant:"secondary",children:(0,_.__)("Accept","wp-parsely")})})]}),v&&(0,p.jsxs)(p.Fragment,{children:[(0,p.jsx)(f.Tooltip,{shortcut:"R",text:(0,_.__)("Remove","wp-parsely"),children:(0,p.jsx)(f.Button,{className:"wp-parsely-review-suggestion-reject",icon:Xe,onClick:o,variant:"secondary",children:(0,_.__)("Remove","wp-parsely")})}),(0,p.jsx)(f.Button,{className:"wp-parsely-review-suggestion-accept",onClick:a,variant:"secondary",children:(0,_.__)("Select in Editor","wp-parsely")})]})]}),(0,p.jsx)(f.Tooltip,{shortcut:"→",text:(0,_.__)("Next","wp-parsely"),children:(0,p.jsxs)(f.Button,{disabled:!c,onClick:n,className:"wp-parsely-review-suggestion-next",children:[(0,_.__)("Next","wp-parsely"),(0,p.jsx)(X,{icon:ze})]})})]})]}):(0,p.jsx)(p.Fragment,{children:(0,_.__)("No block is selected.","wp-parsely")})},it=function(e,t,n,r){return new(n||(n=Promise))((function(i,s){function o(e){try{l(r.next(e))}catch(e){s(e)}}function a(e){try{l(r.throw(e))}catch(e){s(e)}}function l(e){var t;e.done?i(e.value):(t=e.value,t instanceof n?t:new n((function(e){e(t)}))).then(o,a)}l((r=r.apply(e,t||[])).next())}))},st=function(e,t){var n,r,i,s,o={label:0,sent:function(){if(1&i[0])throw i[1];return i[1]},trys:[],ops:[]};return s={next:a(0),throw:a(1),return:a(2)},"function"==typeof Symbol&&(s[Symbol.iterator]=function(){return this}),s;function a(a){return function(l){return function(a){if(n)throw new TypeError("Generator is already executing.");for(;s&&(s=0,a[0]&&(o=0)),o;)try{if(n=1,r&&(i=2&a[0]?r.return:a[0]?r.throw||((i=r.return)&&i.call(r),0):r.next)&&!(i=i.call(r,a[1])).done)return i;switch(r=0,i&&(a=[2&a[0],i.value]),a[0]){case 0:case 1:i=a;break;case 4:return o.label++,{value:a[1],done:!1};case 5:o.label++,r=a[1],a=[0];continue;case 7:a=o.ops.pop(),o.trys.pop();continue;default:if(!((i=(i=o.trys).length>0&&i[i.length-1])||6!==a[0]&&2!==a[0])){o=0;continue}if(3===a[0]&&(!i||a[1]>i[0]&&a[1]0&&(a=o[0],(l=a.parentNode)&&(c=document.createTextNode(null!==(u=a.textContent)&&void 0!==u?u:""),l.replaceChild(c,a),te.updateBlockAttributes(n,{content:s.innerHTML}))),[4,E(t.uid)]):[2]):[2];case 1:return d.sent(),[2]}}))}))},C=(0,b.useCallback)((function(){c(!1),w().filter((function(e){return!e.applied})).length>0?o(!0):(ne.unlockPostAutosaving("smart-linking-review-modal"),t())}),[w,t]),A=function(e){o(!1),e?(c(!1),T().then((function(){C()}))):c(!0)},O=function(){if(ue(k)){var e=g.indexOf(k);if(!g[t=e+1])return;S(g[t])}else{var t;if(e=v.indexOf(k),!v[t=e+1])return;S(v[t])}},R=function(){if(ue(k)){var e=g.indexOf(k);if(!g[t=e-1])return;S(g[t])}else{var t;if(e=v.indexOf(k),!v[t=e-1])return;S(v[t])}};return(0,b.useEffect)((function(){l?ne.lockPostAutosaving("smart-linking-review-modal"):l&&0===d.length&&C()}),[l,t,d,C]),(0,b.useEffect)((function(){c(n)}),[n]),(0,p.jsxs)(p.Fragment,{children:[l&&(0,p.jsx)(f.Modal,{title:(0,_.__)("Review Smart Links","wp-parsely"),className:"wp-parsely-smart-linking-review-modal",onRequestClose:C,shouldCloseOnClickOutside:!1,shouldCloseOnEsc:!1,children:(0,p.jsxs)("div",{className:"smart-linking-modal-body",children:[(0,p.jsx)(Qe,{outboundLinks:v,inboundLinks:g,activeLink:k,setSelectedLink:S}),k&&(ue(k)?(0,p.jsx)(Ke,{link:k,onNext:O,onPrevious:R,hasNext:g.indexOf(k)0}):(0,p.jsx)(rt,{link:k,hasNext:m().indexOf(k)0,onNext:O,onPrevious:R,onAccept:function(){return it(void 0,void 0,void 0,(function(){var e,t;return st(this,(function(n){switch(n.label){case 0:return k.match?(r(k),[4,(i=k.match.blockId,s=k,it(void 0,void 0,void 0,(function(){var e,t;return st(this,(function(n){switch(n.label){case 0:return(e=document.createElement("a")).href=s.href,e.title=s.title,e.setAttribute("data-smartlink",s.uid),(t=(0,h.select)("core/block-editor").getBlock(i))?(fe(t,s,e),s.applied=!0,[4,L(s)]):[2];case 1:return n.sent(),[2]}}))})))]):[2];case 1:return n.sent(),P.trackEvent("smart_linking_link_accepted",{link:k.href,title:k.title,text:k.text,uid:k.uid}),0===y().length?(C(),[2]):(e=v.indexOf(k),v[t=e+1]?S(v[t]):S(v[0]),[2])}var i,s}))}))},onReject:function(){return it(void 0,void 0,void 0,(function(){var e,t;return st(this,(function(n){switch(n.label){case 0:return e=v.indexOf(k),v[t=e+1]?S(v[t]):v[0]?S(v[0]):C(),[4,E(k.uid)];case 1:return n.sent(),P.trackEvent("smart_linking_link_rejected",{link:k.href,title:k.title,text:k.text,uid:k.uid}),[2]}}))}))},onRemove:function(){return it(void 0,void 0,void 0,(function(){var e,t,n,r;return st(this,(function(i){switch(i.label){case 0:return k.match?(e=(0,h.select)("core/block-editor").getBlock(k.match.blockId))?(t=m(),n=t.indexOf(k),r=n-1,[4,N(e,k)]):[3,2]:[2];case 1:if(i.sent(),P.trackEvent("smart_linking_link_removed",{link:k.href,title:k.title,text:k.text,uid:k.uid}),0===(t=m()).length&&g.length>0)return S(g[0]),[2];if(0===t.length&&0===g.length)return C(),[2];if(t[r])return S(t[r]),[2];S(t[0]),i.label=2;case 2:return[2]}}))}))},onSelectInEditor:function(){if(k.match){var e=(0,h.select)("core/block-editor").getBlock(k.match.blockId);if(e){te.selectBlock(e.clientId);var t=document.querySelector('[data-block="'.concat(e.clientId,'"]'));t&&ke(t,k.uid),P.trackEvent("smart_linking_select_in_editor_pressed",{type:"outbound",uid:k.uid}),C()}}}}))]})}),s&&(0,p.jsxs)(f.Modal,{title:(0,_.__)("Review Smart Links","wp-parsely"),onRequestClose:function(){return A(!1)},className:"wp-parsely-smart-linking-close-dialog",children:[(0,_.__)("Are you sure you want to close? All un-accepted smart links will not be added.","wp-parsely"),(0,p.jsxs)("div",{className:"smart-linking-close-dialog-actions",children:[(0,p.jsx)(f.Button,{variant:"secondary",onClick:function(){return A(!1)},children:(0,_.__)("Go Back","wp-parsely")}),(0,p.jsx)(f.Button,{variant:"primary",onClick:function(){return A(!0)},children:(0,_.__)("Close","wp-parsely")})]})]})]})})),at=function(){return at=Object.assign||function(e){for(var t,n=1,r=arguments.length;n0&&i[i.length-1])||6!==a[0]&&2!==a[0])){o=0;continue}if(3===a[0]&&(!i||a[1]>i[0]&&a[1]0&&k("success",/* translators: %d: number of smart links applied */ /* translators: %d: number of smart links applied */ (0,_.sprintf)((0,_.__)("%s smart links successfully applied.","wp-parsely"),g),{type:"snackbar"}):y(0)}),[w]),(0,b.useEffect)((function(){if(!(Object.keys(R).length>0)){var e={maxLinksPerPost:a.SmartLinking.MaxLinks};te(e)}}),[te,a]);var he=(0,h.useSelect)((function(e){var t=e("core/block-editor"),r=t.getSelectedBlock,i=t.getBlock,s=t.getBlocks,o=e("core/editor"),a=o.getEditedPostContent,l=o.getCurrentPostAttribute;return{allBlocks:s(),selectedBlock:n?i(n):r(),postContent:a(),postPermalink:l("link")}}),[n]),ve=he.allBlocks,me=he.selectedBlock,xe=he.postContent,ke=he.postPermalink,Se=function(e){return lt(void 0,void 0,void 0,(function(){var t,n,r,i,s;return ct(this,(function(o){switch(o.label){case 0:t=[],o.label=1;case 1:return o.trys.push([1,4,,9]),[4,re((n=E||!me)?_e.All:_e.Selected)];case 2:return o.sent(),a=ke.replace(/^https?:\/\//i,""),r=["http://"+a,"https://"+a],i=function(e){return e.map((function(e){return e.href}))}(F),r.push.apply(r,i),[4,De.getInstance().generateSmartLinks(me&&!n?(0,Q.getBlockContent)(me):xe,O,r)];case 3:return t=o.sent(),[3,9];case 4:if((s=o.sent()).code&&s.code===$.ParselyAborted)throw s.numRetries=3-e,s;return e>0&&s.retryFetch?(console.error(s),[4,ce(!0)]):[3,8];case 5:return o.sent(),[4,ue()];case 6:return o.sent(),[4,Se(e-1)];case 7:return[2,o.sent()];case 8:throw s;case 9:return[2,t]}var a}))}))},Pe=function(){for(var e=[],t=0;t[type="button"]').forEach((function(e){e.setAttribute("disabled","disabled")}))},Ne=function(){document.querySelectorAll('.edit-post-header__settings>[type="button"]').forEach((function(e){e.removeAttribute("disabled")})),ne.unlockPostSaving("wp-parsely-block-overlay")};return(0,p.jsxs)("div",{className:"wp-parsely-smart-linking",children:[(0,p.jsx)(se,{isDetectingEnabled:!L,onLinkRemove:function(e){!function(e){ae(this,void 0,void 0,(function(){var t,n,r;return le(this,(function(i){switch(i.label){case 0:return[4,we((0,Q.getBlockContent)(e),e.clientId)];case 1:return t=i.sent(),n=t.missingSmartLinks,r=t.didAnyFixes,n.forEach((function(e){(0,h.dispatch)(Te).removeSmartLink(e.uid)})),[2,r]}}))}))}(e.block)}}),(0,p.jsxs)(f.PanelRow,{className:t,children:[(0,p.jsxs)("div",{className:"smart-linking-text",children:[(0,_.__)("Automatically insert links to your most relevant, top performing content.","wp-parsely"),(0,p.jsxs)(f.Button,{href:"https://docs.parse.ly/plugin-content-helper/#h-smart-linking-beta",target:"_blank",variant:"link",children:[(0,_.__)("Learn more about Parse.ly AI","wp-parsely"),(0,p.jsx)(X,{icon:ee,size:18,className:"parsely-external-link-icon"})]})]}),C&&(0,p.jsx)(f.Notice,{status:"info",onRemove:function(){return Z(null)},className:"wp-parsely-content-helper-error",children:C.Message()}),w&&g>0&&(0,p.jsx)(f.Notice,{status:"success",onRemove:function(){return x(!1)},className:"wp-parsely-smart-linking-suggested-links",children:(0,_.sprintf)(/* translators: 1 - number of smart links generated */ /* translators: 1 - number of smart links generated */ (0,_.__)("Successfully added %s smart links.","wp-parsely"),g>0?g:A.length)}),(0,p.jsx)(Ce,{disabled:T,selectedBlock:me,onSettingChange:function(e,t){var n;d({SmartLinking:at(at({},a.SmartLinking),(n={},n[e]=t,n))}),"MaxLinks"===e&&oe(t)}}),(0,p.jsx)("div",{className:"smart-linking-generate",children:(0,p.jsx)(f.Button,{onClick:function(){return lt(void 0,void 0,void 0,(function(){var e,t,n,r,s,o,a,l;return ct(this,(function(c){switch(c.label){case 0:return[4,q(!0)];case 1:return c.sent(),[4,de()];case 2:return c.sent(),[4,Z(null)];case 3:return c.sent(),x(!1),P.trackEvent("smart_linking_generate_pressed",{is_full_content:E,selected_block:null!==(o=null==me?void 0:me.name)&&void 0!==o?o:"none",context:i}),[4,Pe(E?"all":null==me?void 0:me.clientId)];case 4:c.sent(),e=setTimeout((function(){var e;q(!1),P.trackEvent("smart_linking_generate_timeout",{is_full_content:E,selected_block:null!==(e=null==me?void 0:me.name)&&void 0!==e?e:"none",context:i}),je(E?"all":null==me?void 0:me.clientId)}),18e4),t=I,c.label=5;case 5:return c.trys.push([5,8,10,15]),[4,Se(3)];case 6:return n=c.sent(),[4,(u=n,lt(void 0,void 0,void 0,(function(){var e;return ct(this,(function(t){switch(t.label){case 0:return u=u.filter((function(e){return!F.some((function(t){return t.uid===e.uid&&t.applied}))})),e=ke.replace(/^https?:\/\//,"").replace(/\/+$/,""),u=(u=u.filter((function(t){return!t.href.includes(e)||(console.warn("PCH Smart Linking: Skipping self-reference link: ".concat(t.href)),!1)}))).filter((function(e){return!F.some((function(t){return t.href===e.href?(console.warn("PCH Smart Linking: Skipping duplicate link: ".concat(e.href)),!0):t.text===e.text&&t.offset!==e.offset&&(console.warn("PCH Smart Linking: Skipping duplicate link text: ".concat(e.text)),!0)}))})),u=(u=ge(E?ve:[me],u,{}).filter((function(e){return e.match}))).filter((function(e){if(!e.match)return!1;var t=e.match.blockLinkPosition,n=t+e.text.length;return!F.some((function(r){if(!r.match)return!1;if(e.match.blockId!==r.match.blockId)return!1;var i=r.match.blockLinkPosition,s=i+r.text.length;return t>=i&&n<=s}))})),[4,W(u)];case 1:return t.sent(),[2,u]}}))})))];case 7:if(0===c.sent().length)throw new ie((0,_.__)("No smart links were generated.","wp-parsely"),$.ParselySuggestionsApiNoData,"");return pe(!0),[3,15];case 8:return r=c.sent(),s=new ie(null!==(a=r.message)&&void 0!==a?a:"An unknown error has occurred.",null!==(l=r.code)&&void 0!==l?l:$.UnknownError),r.code&&r.code===$.ParselyAborted&&(s.message=(0,_.sprintf)(/* translators: %d: number of retry attempts, %s: attempt plural */ /* translators: %d: number of retry attempts, %s: attempt plural */ (0,_.__)("The Smart Linking process was cancelled after %1$d %2$s.","wp-parsely"),r.numRetries,(0,_._n)("attempt","attempts",r.numRetries,"wp-parsely"))),console.error(r),[4,Z(s)];case 9:return c.sent(),s.createErrorSnackbar(),[3,15];case 10:return[4,q(!1)];case 11:return c.sent(),[4,re(t)];case 12:return c.sent(),[4,ce(!1)];case 13:return c.sent(),[4,je(E?"all":null==me?void 0:me.clientId)];case 14:return c.sent(),clearTimeout(e),[7];case 15:return[2]}var u}))}))},variant:"primary",isBusy:T,disabled:T,children:M?(0,_.sprintf)(/* translators: %1$d: number of retry attempts, %2$d: maximum number of retries */ /* translators: %1$d: number of retry attempts, %2$d: maximum number of retries */ -(0,_.__)("Retrying… Attempt %1$d of %2$d","wp-parsely"),D,3):T?(0,_.__)("Generating Smart Links…","wp-parsely"):(0,_.__)("Add Smart Links","wp-parsely")})}),(G.length>0||V.length>0)&&(0,p.jsx)("div",{className:"smart-linking-manage",children:(0,p.jsx)(f.Button,{onClick:function(){return lt(void 0,void 0,void 0,(function(){var e,t;return ct(this,(function(n){switch(n.label){case 0:return[4,be()];case 1:return e=n.sent(),t=ye(),[4,W(t)];case 2:return n.sent(),pe(!0),P.trackEvent("smart_linking_review_pressed",{num_smart_links:F.length,has_fixed_links:e,context:i}),[2]}}))}))},variant:"secondary",disabled:T,children:(0,_.__)("Review Smart Links","wp-parsely")})})]}),L&&(0,p.jsx)(ot,{isOpen:L,onAppliedLink:function(){y((function(e){return e+1}))},onClose:function(){x(!0),pe(!1)}})]})},ft=function(){return ft=Object.assign||function(e){for(var t,n=1,r=arguments.length;n0&&i[i.length-1])||6!==a[0]&&2!==a[0])){o=0;continue}if(3===a[0]&&(!i||a[1]>i[0]&&a[1]0)&&(t(),e())}))}))]}))},new((n=void 0)||(n=Promise))((function(i,s){function o(e){try{l(r.next(e))}catch(e){s(e)}}function a(e){try{l(r.throw(e))}catch(e){s(e)}}function l(e){var t;e.done?i(e.value):(t=e.value,t instanceof n?t:new n((function(e){e(t)}))).then(o,a)}l((r=r.apply(e,t||[])).next())}));var e,t,n,r}().then((function(){var t=document.querySelector(".wp-block-post-content");ke(t,e)}))})))},Pt=(0,p.jsx)(x.SVG,{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 24 24",children:(0,p.jsx)(x.Path,{d:"M7 11.5h10V13H7z"})}),jt=(0,p.jsx)(x.SVG,{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 24 24",children:(0,p.jsx)(x.Path,{d:"M13 19h-2v-2h2v2zm0-6h-2v-2h2v2zm0-6h-2V5h2v2z"})}),Tt=function(e){var t=e.title,n=e.icon,r=e.subtitle,i=e.level,s=void 0===i?2:i,o=e.children,a=e.controls,l=e.onClick,c=e.isOpen,u=e.isLoading,d=e.dropdownChildren;return(0,p.jsxs)("div",{className:"performance-stat-panel",children:[(0,p.jsxs)(f.__experimentalHStack,{className:"panel-header level-"+s,children:[(0,p.jsx)(f.__experimentalHeading,{level:s,children:t}),r&&!c&&(0,p.jsx)("span",{className:"panel-subtitle",children:r}),a&&!d&&(0,p.jsx)(f.DropdownMenu,{icon:n,label:(0,_.__)("Settings","wp-parsely"),className:"panel-settings-button",controls:a}),d&&(0,p.jsx)(f.DropdownMenu,{icon:n,label:(0,_.__)("Settings","wp-parsely"),className:"panel-settings-button",children:d}),n&&!d&&!a&&(0,p.jsx)(f.Button,{icon:n,className:"panel-settings-button",isPressed:c,onClick:l})]}),(0,p.jsx)("div",{className:"panel-body",children:u?(0,p.jsx)("div",{className:"parsely-spinner-wrapper","data-testid":"parsely-spinner-wrapper",children:(0,p.jsx)(f.Spinner,{})}):o})]})};function Lt(e,t,n){void 0===t&&(t=1),void 0===n&&(n="");var r=parseInt(e.replace(/\D/g,""),10);if(r<1e3)return e;r<1e4&&(t=1);var i=r,s=r.toString(),o="",a=0;return Object.entries({1e3:"k","1,000,000":"M","1,000,000,000":"B","1,000,000,000,000":"T","1,000,000,000,000,000":"Q"}).forEach((function(e){var n=e[0],l=e[1],c=parseInt(n.replace(/\D/g,""),10);if(r>=c){var u=t;(i=r/c)%1>1/a&&(u=i>10?1:2),u=parseFloat(i.toFixed(2))===parseFloat(i.toFixed(0))?0:u,s=i.toFixed(u),o=l}a=c})),s+n+o}var Et=function(e){var t=e.data,n=e.isLoading,r=(0,b.useState)(m.Views),i=r[0],s=r[1],o=(0,b.useState)(!1),a=o[0],l=o[1];n||delete t.referrers.types.totals;var c=function(e){switch(e){case"social":return(0,_.__)("Social","wp-parsely");case"search":return(0,_.__)("Search","wp-parsely");case"other":return(0,_.__)("Other","wp-parsely");case"internal":return(0,_.__)("Internal","wp-parsely");case"direct":return(0,_.__)("Direct","wp-parsely")}return e},u=(0,_.sprintf)((0,_.__)("By %s","wp-parsely"),V(i)); +(0,_.__)("Retrying… Attempt %1$d of %2$d","wp-parsely"),D,3):T?(0,_.__)("Generating Smart Links…","wp-parsely"):(0,_.__)("Add Smart Links","wp-parsely")})}),(G.length>0||V.length>0)&&(0,p.jsx)("div",{className:"smart-linking-manage",children:(0,p.jsx)(f.Button,{onClick:function(){return lt(void 0,void 0,void 0,(function(){var e,t;return ct(this,(function(n){switch(n.label){case 0:return[4,be()];case 1:return e=n.sent(),t=ye(),[4,W(t)];case 2:return n.sent(),pe(!0),P.trackEvent("smart_linking_review_pressed",{num_smart_links:F.length,has_fixed_links:e,context:i}),[2]}}))}))},variant:"secondary",disabled:T,children:(0,_.__)("Review Smart Links","wp-parsely")})})]}),L&&(0,p.jsx)(ot,{isOpen:L,onAppliedLink:function(){y((function(e){return e+1}))},onClose:function(){x(!0),pe(!1)}})]})},ft=function(){return ft=Object.assign||function(e){for(var t,n=1,r=arguments.length;n0&&i[i.length-1])||6!==a[0]&&2!==a[0])){o=0;continue}if(3===a[0]&&(!i||a[1]>i[0]&&a[1]0)&&(t(),e())}))}))]}))},new((n=void 0)||(n=Promise))((function(i,s){function o(e){try{l(r.next(e))}catch(e){s(e)}}function a(e){try{l(r.throw(e))}catch(e){s(e)}}function l(e){var t;e.done?i(e.value):(t=e.value,t instanceof n?t:new n((function(e){e(t)}))).then(o,a)}l((r=r.apply(e,t||[])).next())}));var e,t,n,r}().then((function(){var t=document.querySelector(".wp-block-post-content");ke(t,e)}))})))},Pt=(0,p.jsx)(x.SVG,{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 24 24",children:(0,p.jsx)(x.Path,{d:"M7 11.5h10V13H7z"})}),jt=(0,p.jsx)(x.SVG,{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 24 24",children:(0,p.jsx)(x.Path,{d:"M13 19h-2v-2h2v2zm0-6h-2v-2h2v2zm0-6h-2V5h2v2z"})}),Tt=function(e){var t=e.title,n=e.icon,r=e.subtitle,i=e.level,s=void 0===i?2:i,o=e.children,a=e.controls,l=e.onClick,c=e.isOpen,u=e.isLoading,d=e.dropdownChildren;return(0,p.jsxs)("div",{className:"performance-stat-panel",children:[(0,p.jsxs)(f.__experimentalHStack,{className:"panel-header level-"+s,children:[(0,p.jsx)(f.__experimentalHeading,{level:s,children:t}),r&&!c&&(0,p.jsx)("span",{className:"panel-subtitle",children:r}),a&&!d&&(0,p.jsx)(f.DropdownMenu,{icon:n,label:(0,_.__)("Settings","wp-parsely"),className:"panel-settings-button",controls:a}),d&&(0,p.jsx)(f.DropdownMenu,{icon:n,label:(0,_.__)("Settings","wp-parsely"),className:"panel-settings-button",children:d}),n&&!d&&!a&&(0,p.jsx)(f.Button,{icon:n,className:"panel-settings-button",isPressed:c,onClick:l})]}),(0,p.jsx)("div",{className:"panel-body",children:u?(0,p.jsx)("div",{className:"parsely-spinner-wrapper","data-testid":"parsely-spinner-wrapper",children:(0,p.jsx)(f.Spinner,{})}):o})]})};function Lt(e,t,n){void 0===t&&(t=1),void 0===n&&(n="");var r=parseInt(e.replace(/\D/g,""),10);if(r<1e3)return e;r<1e4&&(t=1);var i=r,s=r.toString(),o="",a=0;return Object.entries({1e3:"k","1,000,000":"M","1,000,000,000":"B","1,000,000,000,000":"T","1,000,000,000,000,000":"Q"}).forEach((function(e){var n=e[0],l=e[1],c=parseInt(n.replace(/\D/g,""),10);if(r>=c){var u=t;(i=r/c)%1>1/a&&(u=i>10?1:2),u=parseFloat(i.toFixed(2))===parseFloat(i.toFixed(0))?0:u,s=i.toFixed(u),o=l}a=c})),s+n+o}var Et=function(e){var t=e.data,n=e.isLoading,r=(0,b.useState)(m.Views),i=r[0],s=r[1],o=(0,b.useState)(!1),a=o[0],l=o[1];n||delete t.referrers.types.totals;var c=function(e){switch(e){case"social":return(0,_.__)("Social","wp-parsely");case"search":return(0,_.__)("Search","wp-parsely");case"other":return(0,_.__)("Other","wp-parsely");case"internal":return(0,_.__)("Internal","wp-parsely");case"direct":return(0,_.__)("Direct","wp-parsely")}return e},u=(0,_.sprintf)((0,_.__)("By %s","wp-parsely"),V(i)); /* translators: %s: metric description */return(0,p.jsxs)(Tt,{title:(0,_.__)("Categories","wp-parsely"),level:3,subtitle:u,isOpen:a,onClick:function(){return l(!a)},children:[a&&(0,p.jsx)("div",{className:"panel-settings",children:(0,p.jsx)(f.SelectControl,{value:i,prefix:(0,_.__)("By: ","wp-parsely"),onChange:function(e){D(e,m)&&s(e)},children:Object.values(m).map((function(e){return(0,p.jsxs)("option",{value:e,disabled:"avg_engaged"===e,children:[V(e),"avg_engaged"===e&&(0,_.__)(" (coming soon)","wp-parsely")]},e)}))})}),n?(0,p.jsx)("div",{className:"parsely-spinner-wrapper","data-testid":"parsely-spinner-wrapper",children:(0,p.jsx)(f.Spinner,{})}):(0,p.jsxs)("div",{children:[(0,p.jsx)("div",{className:"multi-percentage-bar",children:Object.entries(t.referrers.types).map((function(e){var t=e[0],n=e[1],r=(0,_.sprintf)(/* translators: 1: Referrer type, 2: Percentage value, %%: Escaped percent sign */ /* translators: 1: Referrer type, 2: Percentage value, %%: Escaped percent sign */ (0,_.__)("%1$s: %2$s%%","wp-parsely"),c(t),n.viewsPercentage);return(0,p.jsx)(f.Tooltip /* translators: %s: percentage value */,{ @@ -25,4 +25,4 @@ message:(0,_.sprintf)((0,_.__)('by author "%1$s"',"wp-parsely"),n.value)};throw (0,_.__)("Top related posts by %1$s in the %2$s.","wp-parsely"),I.value,F(r,!0)):null!=E?E:""})}),j&&j.Message(),x&&(0,p.jsx)("div",{className:"related-posts-loading-message","data-testid":"parsely-related-posts-loading-message",children:(0,_.__)("Loading…","wp-parsely")}),!x&&!j&&0===A.length&&(0,p.jsx)("div",{className:"related-posts-empty","data-testid":"parsely-related-posts-empty",children:(0,_.__)("No related posts found.","wp-parsely")}),!x&&A.length>0&&(0,p.jsx)("div",{className:"related-posts-list",children:A.map((function(e){return(0,p.jsx)(hn,{metric:i,post:e,postContent:H},e.id)}))})]})]})]})},jn=(0,p.jsx)(x.SVG,{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 24 24",children:(0,p.jsx)(x.Path,{d:"m19 7-3-3-8.5 8.5-1 4 4-1L19 7Zm-7 11.5H5V20h7v-1.5Z"})}),Tn=function(){return(0,p.jsx)(f.SVG,{xmlns:"http://www.w3.org/2000/svg",width:"18",height:"18",viewBox:"0 0 18 18",fill:"none",children:(0,p.jsx)(f.Path,{fillRule:"evenodd",clipRule:"evenodd",d:"M13.5034 7.91642L9 12.0104L4.49662 7.91642L5.25337 7.08398L8.99999 10.49L12.7466 7.08398L13.5034 7.91642Z",fill:"#1E1E1E"})})},Ln={journalist:{label:(0,_.__)("Journalist","wp-parsely")},editorialWriter:{label:(0,_.__)("Editorial Writer","wp-parsely")},investigativeReporter:{label:(0,_.__)("Investigative Reporter","wp-parsely")},techAnalyst:{label:(0,_.__)("Tech Analyst","wp-parsely")},businessAnalyst:{label:(0,_.__)("Business Analyst","wp-parsely")},culturalCommentator:{label:(0,_.__)("Cultural Commentator","wp-parsely")},scienceCorrespondent:{label:(0,_.__)("Science Correspondent","wp-parsely")},politicalAnalyst:{label:(0,_.__)("Political Analyst","wp-parsely")},healthWellnessAdvocate:{label:(0,_.__)("Health and Wellness Advocate","wp-parsely")},environmentalJournalist:{label:(0,_.__)("Environmental Journalist","wp-parsely")},custom:{label:(0,_.__)("Custom Persona","wp-parsely"),icon:jn}},En=Object.keys(Ln),Nn=function(e){return"custom"===e||""===e?Ln.custom.label:Cn(e)?e:Ln[e].label},Cn=function(e){return!En.includes(e)||"custom"===e},An=function(e){var t=e.value,n=e.onChange,r=(0,b.useState)(""),i=r[0],s=r[1],o=(0,z.useDebounce)(n,500);return(0,p.jsx)("div",{className:"parsely-persona-selector-custom",children:(0,p.jsx)(f.TextControl,{value:i||t,placeholder:(0,_.__)("Enter a custom persona…","wp-parsely"),onChange:function(e){if(""===e)return n(""),void s("");e.length>32&&(e=e.slice(0,32)),o(e),s(e)}})})},On=function(e){var t=e.persona,n=e.value,r=void 0===n?(0,_.__)("Select a persona…","wp-parsely"):n,i=e.label,s=void 0===i?(0,_.__)("Persona","wp-parsely"):i,o=e.onChange,a=e.onDropdownChange,l=e.disabled,c=void 0!==l&&l,u=e.allowCustom,d=void 0!==u&&u;return(0,p.jsxs)(f.Disabled,{isDisabled:c,children:[s&&(0,p.jsx)("div",{className:"wp-parsely-dropdown-label",children:s}),(0,p.jsx)(f.DropdownMenu,{label:(0,_.__)("Persona","wp-parsely"),className:"parsely-persona-selector-dropdown"+(c?" is-disabled":""),popoverProps:{className:"wp-parsely-popover"},toggleProps:{children:(0,p.jsxs)(p.Fragment,{children:[(0,p.jsx)("div",{className:"parsely-persona-selector-label",children:Cn(t)?Ln.custom.label:r}),(0,p.jsx)(Tn,{})]})},children:function(e){var n=e.onClose;return(0,p.jsx)(f.MenuGroup,{label:(0,_.__)("Persona","wp-parsely"),children:(0,p.jsx)(p.Fragment,{children:En.map((function(e){if(!d&&"custom"===e)return null;var r=Ln[e],i=e===t||Cn(t)&&"custom"===e;return(0,p.jsxs)(f.MenuItem,{isSelected:i,className:i?"is-selected":"",role:"menuitemradio",onClick:function(){null==a||a(e),o(e),n(),"custom"===e&&setTimeout((function(){var e=document.querySelector(".parsely-persona-selector-custom input");e&&e.focus()}),0)},children:[r.icon&&(0,p.jsx)(X,{icon:r.icon}),r.label]},e)}))})})}}),d&&Cn(t)&&(0,p.jsx)(An,{onChange:function(e){o(""!==e?e:"custom")},value:"custom"===t?"":t})]})},Rn={neutral:{label:(0,_.__)("Neutral","wp-parsely")},formal:{label:(0,_.__)("Formal","wp-parsely")},humorous:{label:(0,_.__)("Humorous","wp-parsely")},confident:{label:(0,_.__)("Confident","wp-parsely")},provocative:{label:(0,_.__)("Provocative","wp-parsely")},serious:{label:(0,_.__)("Serious","wp-parsely")},inspirational:{label:(0,_.__)("Inspirational","wp-parsely")},skeptical:{label:(0,_.__)("Skeptical","wp-parsely")},conversational:{label:(0,_.__)("Conversational","wp-parsely")},analytical:{label:(0,_.__)("Analytical","wp-parsely")},custom:{label:(0,_.__)("Custom Tone","wp-parsely"),icon:jn}},In=Object.keys(Rn),Bn=function(e){return"custom"===e||""===e?Rn.custom.label:Mn(e)?e:Rn[e].label},Mn=function(e){return!In.includes(e)||"custom"===e},Dn=function(e){var t=e.value,n=e.onChange,r=(0,b.useState)(""),i=r[0],s=r[1],o=(0,z.useDebounce)(n,500);return(0,p.jsx)("div",{className:"parsely-tone-selector-custom",children:(0,p.jsx)(f.TextControl,{value:i||t,placeholder:(0,_.__)("Enter a custom tone","wp-parsely"),onChange:function(e){if(""===e)return n(""),void s("");e.length>32&&(e=e.slice(0,32)),o(e),s(e)}})})},Fn=function(e){var t=e.tone,n=e.value,r=void 0===n?(0,_.__)("Select a tone","wp-parsely"):n,i=e.label,s=void 0===i?(0,_.__)("Tone","wp-parsely"):i,o=e.onChange,a=e.onDropdownChange,l=e.disabled,c=void 0!==l&&l,u=e.allowCustom,d=void 0!==u&&u;return(0,p.jsxs)(f.Disabled,{isDisabled:c,children:[(0,p.jsx)("div",{className:"wp-parsely-dropdown-label",children:s}),(0,p.jsx)(f.DropdownMenu,{label:(0,_.__)("Tone","wp-parsely"),className:"parsely-tone-selector-dropdown"+(c?" is-disabled":""),popoverProps:{className:"wp-parsely-popover"},toggleProps:{children:(0,p.jsxs)(p.Fragment,{children:[(0,p.jsx)("div",{className:"parsely-tone-selector-label",children:Mn(t)?Rn.custom.label:r}),(0,p.jsx)(Tn,{})]})},children:function(e){var n=e.onClose;return(0,p.jsx)(f.MenuGroup,{label:(0,_.__)("Select a tone","wp-parsely"),children:(0,p.jsx)(p.Fragment,{children:In.map((function(e){if(!d&&"custom"===e)return null;var r=Rn[e],i=e===t||Mn(t)&&"custom"===e;return(0,p.jsxs)(f.MenuItem,{isSelected:i,className:i?"is-selected":"",role:"menuitemradio",onClick:function(){null==a||a(e),o(e),n(),"custom"===e&&setTimeout((function(){var e=document.querySelector(".parsely-tone-selector-custom input");e&&e.focus()}),0)},children:[r.icon&&(0,p.jsx)(X,{icon:r.icon}),r.label]},e)}))})})}}),d&&Mn(t)&&(0,p.jsx)(Dn,{onChange:function(e){o(""!==e?e:"custom")},value:"custom"===t?"":t})]})},Vn=(0,p.jsx)(x.SVG,{width:"24",height:"24",viewBox:"0 0 24 24",xmlns:"http://www.w3.org/2000/svg",children:(0,p.jsx)(x.Path,{d:"M10.97 10.159a3.382 3.382 0 0 0-2.857.955l1.724 1.723-2.836 2.913L7 17h1.25l2.913-2.837 1.723 1.723a3.38 3.38 0 0 0 .606-.825c.33-.63.446-1.343.35-2.032L17 10.695 13.305 7l-2.334 3.159Z"})}),Gn=(0,p.jsx)(x.SVG,{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 24 24",children:(0,p.jsx)(x.Path,{d:"M18.3 11.7c-.6-.6-1.4-.9-2.3-.9H6.7l2.9-3.3-1.1-1-4.5 5L8.5 16l1-1-2.7-2.7H16c.5 0 .9.2 1.3.5 1 1 1 3.4 1 4.5v.3h1.5v-.2c0-1.5 0-4.3-1.5-5.7z"})}),Hn=(0,p.jsx)(x.SVG,{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 24 24",children:(0,p.jsx)(x.Path,{fillRule:"evenodd",clipRule:"evenodd",d:"M12 5.5A2.25 2.25 0 0 0 9.878 7h4.244A2.251 2.251 0 0 0 12 5.5ZM12 4a3.751 3.751 0 0 0-3.675 3H5v1.5h1.27l.818 8.997a2.75 2.75 0 0 0 2.739 2.501h4.347a2.75 2.75 0 0 0 2.738-2.5L17.73 8.5H19V7h-3.325A3.751 3.751 0 0 0 12 4Zm4.224 4.5H7.776l.806 8.861a1.25 1.25 0 0 0 1.245 1.137h4.347a1.25 1.25 0 0 0 1.245-1.137l.805-8.861Z"})}),zn=(0,p.jsx)(x.SVG,{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 24 24",children:(0,p.jsx)(x.Path,{d:"m21.5 9.1-6.6-6.6-4.2 5.6c-1.2-.1-2.4.1-3.6.7-.1 0-.1.1-.2.1-.5.3-.9.6-1.2.9l3.7 3.7-5.7 5.7v1.1h1.1l5.7-5.7 3.7 3.7c.4-.4.7-.8.9-1.2.1-.1.1-.2.2-.3.6-1.1.8-2.4.6-3.6l5.6-4.1zm-7.3 3.5.1.9c.1.9 0 1.8-.4 2.6l-6-6c.8-.4 1.7-.5 2.6-.4l.9.1L15 4.9 19.1 9l-4.9 3.6z"})}),Un=function(){return Un=Object.assign||function(e){for(var t,n=1,r=arguments.length;n0&&i[i.length-1])||6!==a[0]&&2!==a[0])){o=0;continue}if(3===a[0]&&(!i||a[1]>i[0]&&a[1]0&&i[i.length-1])||6!==a[0]&&2!==a[0])){o=0;continue}if(3===a[0]&&(!i||a[1]>i[0]&&a[1]0&&i[i.length-1])||6!==a[0]&&2!==a[0])){o=0;continue}if(3===a[0]&&(!i||a[1]>i[0]&&a[1]0?(0,p.jsx)("span",{className:"parsely-write-titles-text",children:(0,b.createInterpolateElement)( // translators: %1$s is the tone, %2$s is the persona. // translators: %1$s is the tone, %2$s is the persona. -(0,_.__)("We've generated a few titles based on the content of your post, written as a .","wp-parsely"),{tone:(0,p.jsx)("strong",{children:Bn(a)}),persona:(0,p.jsx)("strong",{children:Nn(u)})})}):(0,_.__)("Use Parse.ly AI to generate a title for your post.","wp-parsely"),(0,p.jsxs)(f.Button,{href:"https://docs.parse.ly/plugin-content-helper/#h-title-suggestions-beta",target:"_blank",variant:"link",children:[(0,_.__)("Learn more about Parse.ly AI","wp-parsely"),(0,p.jsx)(X,{icon:ee,size:18,className:"parsely-external-link-icon"})]})]}),i&&(0,p.jsx)(f.Notice,{className:"wp-parsely-content-helper-error",onRemove:function(){return s(void 0)},status:"info",children:i.Message()}),void 0!==k&&(0,p.jsx)(Jn,{title:k,type:fn.PostTitle,isOriginal:!0}),00&&(0,p.jsx)(Qn,{pinnedTitles:m,isOpen:!0}),y.length>0&&(0,p.jsx)(er,{suggestions:y,isOpen:!0,isLoading:g})]}),(0,p.jsx)(Xn,{isLoading:g,onPersonaChange:function(e){C("Persona",e),d(e)},onSettingChange:C,onToneChange:function(e){C("Tone",e),l(e)},persona:t.TitleSuggestions.Persona,tone:t.TitleSuggestions.Tone}),(0,p.jsx)("div",{className:"title-suggestions-generate",children:(0,p.jsxs)(f.Button,{variant:"primary",isBusy:g,disabled:g||"custom"===a||"custom"===u,onClick:function(){return ir(void 0,void 0,void 0,(function(){return sr(this,(function(e){switch(e.label){case 0:return s(void 0),!1!==g?[3,2]:(P.trackEvent("title_suggestions_generate_pressed",{request_more:y.length>0,total_titles:y.length,total_pinned:y.filter((function(e){return e.isPinned})).length,tone:a,persona:u}),[4,(t=fn.PostTitle,n=A,r=a,i=u,ir(void 0,void 0,void 0,(function(){var e,o,a;return sr(this,(function(l){switch(l.label){case 0:return[4,T(!0)];case 1:l.sent(),e=nr.getInstance(),l.label=2;case 2:return l.trys.push([2,5,,6]),[4,e.generateTitles(n,3,r,i)];case 3:return o=l.sent(),[4,j(t,o)];case 4:return l.sent(),[3,6];case 5:return a=l.sent(),s(a),j(t,[]),[3,6];case 6:return[4,T(!1)];case 7:return l.sent(),[2]}}))})))]);case 1:e.sent(),e.label=2;case 2:return[2]}var t,n,r,i}))}))},children:[g&&(0,_.__)("Generating Titles…","wp-parsely"),!g&&w.length>0&&(0,_.__)("Generate More","wp-parsely"),!g&&0===w.length&&(0,_.__)("Generate Titles","wp-parsely")]})})]})})},ar=function(){return ar=Object.assign||function(e){for(var t,n=1,r=arguments.length;n titles based on the content of your post, written as a .","wp-parsely"),{tone:(0,p.jsx)("strong",{children:Bn(a)}),persona:(0,p.jsx)("strong",{children:Nn(u)})})}):(0,_.__)("Use Parse.ly AI to generate a title for your post.","wp-parsely"),(0,p.jsxs)(f.Button,{href:"https://docs.parse.ly/plugin-content-helper/#h-title-suggestions-beta",target:"_blank",variant:"link",children:[(0,_.__)("Learn more about Parse.ly AI","wp-parsely"),(0,p.jsx)(X,{icon:ee,size:18,className:"parsely-external-link-icon"})]})]}),i&&(0,p.jsx)(f.Notice,{className:"wp-parsely-content-helper-error",onRemove:function(){return s(void 0)},status:"info",children:i.Message()}),void 0!==k&&(0,p.jsx)(Jn,{title:k,type:fn.PostTitle,isOriginal:!0}),00&&(0,p.jsx)(Qn,{pinnedTitles:m,isOpen:!0}),y.length>0&&(0,p.jsx)(er,{suggestions:y,isOpen:!0,isLoading:g})]}),(0,p.jsx)(Xn,{isLoading:g,onPersonaChange:function(e){C("Persona",e),d(e)},onSettingChange:C,onToneChange:function(e){C("Tone",e),l(e)},persona:t.TitleSuggestions.Persona,tone:t.TitleSuggestions.Tone}),(0,p.jsx)("div",{className:"title-suggestions-generate",children:(0,p.jsxs)(f.Button,{variant:"primary",isBusy:g,disabled:g||"custom"===a||"custom"===u,onClick:function(){return ir(void 0,void 0,void 0,(function(){return sr(this,(function(e){switch(e.label){case 0:return s(void 0),!1!==g?[3,2]:(P.trackEvent("title_suggestions_generate_pressed",{request_more:y.length>0,total_titles:y.length,total_pinned:y.filter((function(e){return e.isPinned})).length,tone:a,persona:u}),[4,(t=fn.PostTitle,n=A,r=a,i=u,ir(void 0,void 0,void 0,(function(){var e,o,a;return sr(this,(function(l){switch(l.label){case 0:return[4,T(!0)];case 1:l.sent(),e=nr.getInstance(),l.label=2;case 2:return l.trys.push([2,5,,6]),[4,e.generateTitles(n,3,r,i)];case 3:return o=l.sent(),[4,j(t,o)];case 4:return l.sent(),[3,6];case 5:return a=l.sent(),s(a),j(t,[]),[3,6];case 6:return[4,T(!1)];case 7:return l.sent(),[2]}}))})))]);case 1:e.sent(),e.label=2;case 2:return[2]}var t,n,r,i}))}))},children:[g&&(0,_.__)("Generating Titles…","wp-parsely"),!g&&w.length>0&&(0,_.__)("Generate More","wp-parsely"),!g&&0===w.length&&(0,_.__)("Generate Titles","wp-parsely")]})})]})})},ar=function(){return ar=Object.assign||function(e){for(var t,n=1,r=arguments.length;n diff --git a/src/content-helper/editor-sidebar/editor-sidebar.tsx b/src/content-helper/editor-sidebar/editor-sidebar.tsx index f3e36d922f..2c57a49887 100644 --- a/src/content-helper/editor-sidebar/editor-sidebar.tsx +++ b/src/content-helper/editor-sidebar/editor-sidebar.tsx @@ -228,7 +228,7 @@ const ContentHelperEditorSidebar = (): React.JSX.Element => { title={ __( 'Parse.ly', 'wp-parsely' ) } > @@ -279,7 +279,7 @@ registerPlugin( BLOCK_PLUGIN_ID, { icon: LeafIcon, render: () => ( diff --git a/src/content-helper/editor-sidebar/smart-linking/smart-linking.tsx b/src/content-helper/editor-sidebar/smart-linking/smart-linking.tsx index bdec814f69..cb40a3558e 100644 --- a/src/content-helper/editor-sidebar/smart-linking/smart-linking.tsx +++ b/src/content-helper/editor-sidebar/smart-linking/smart-linking.tsx @@ -53,7 +53,7 @@ const withSettingsProvider = createHigherOrderComponent( ( BlockEdit ) => { return ( From 5b62a4e5b03df145a38a5c5deff16442c1b18ac3 Mon Sep 17 00:00:00 2001 From: Henrique Mouta Date: Fri, 30 Aug 2024 13:12:55 +0100 Subject: [PATCH 23/49] Add tests --- .../EditorSidebarSettingsEndpointTest.php | 1 - .../Integration/RestAPI/BaseEndpointTest.php | 8 +- .../Settings/BaseSettingsEndpointTest.php | 219 ++++++++++++ .../EndpointDashboardWidgetSettingsTest.php | 203 +++++++++++ .../EndpointEditorSidebarSettingsTest.php | 314 ++++++++++++++++++ 5 files changed, 742 insertions(+), 3 deletions(-) create mode 100644 tests/Integration/RestAPI/Settings/BaseSettingsEndpointTest.php create mode 100644 tests/Integration/RestAPI/Settings/EndpointDashboardWidgetSettingsTest.php create mode 100644 tests/Integration/RestAPI/Settings/EndpointEditorSidebarSettingsTest.php diff --git a/tests/Integration/Endpoints/UserMeta/EditorSidebarSettingsEndpointTest.php b/tests/Integration/Endpoints/UserMeta/EditorSidebarSettingsEndpointTest.php index 00071489b4..1b16f855b7 100644 --- a/tests/Integration/Endpoints/UserMeta/EditorSidebarSettingsEndpointTest.php +++ b/tests/Integration/Endpoints/UserMeta/EditorSidebarSettingsEndpointTest.php @@ -176,7 +176,6 @@ public function test_endpoint_returns_value_on_get_request(): void { * @uses \Parsely\Endpoints\User_Meta\Base_Endpoint_User_Meta::get_nested_specs * @uses \Parsely\Endpoints\User_Meta\Base_Endpoint_User_Meta::get_valid_values * @uses \Parsely\Endpoints\User_Meta\Base_Endpoint_User_Meta::is_available_to_current_user - * @uses \Parsely\Endpoints\User_Meta\Base_Endpoint_User_Meta::is_valid_key * @uses \Parsely\Endpoints\User_Meta\Base_Endpoint_User_Meta::run * @uses \Parsely\Endpoints\User_Meta\Editor_Sidebar_Settings_Endpoint::get_meta_key * @uses \Parsely\Endpoints\User_Meta\Editor_Sidebar_Settings_Endpoint::get_route diff --git a/tests/Integration/RestAPI/BaseEndpointTest.php b/tests/Integration/RestAPI/BaseEndpointTest.php index 00f2f7bb83..ae2c3e80f9 100644 --- a/tests/Integration/RestAPI/BaseEndpointTest.php +++ b/tests/Integration/RestAPI/BaseEndpointTest.php @@ -78,8 +78,12 @@ class BaseEndpointTest extends TestCase { * Constructor that initializes the Parsely and API controller instances. * * @since 3.17.0 + * + * @param string|null $name The name of the test. + * @param array $data The data for the test. + * @param string $data_name The name of the data. */ - public function __construct() { + public function __construct( $name = null, array $data = array(), $data_name = '' ) { // Create Parsely class, if not already created (by an inherited class). if ( null === $this->parsely ) { $this->parsely = new Parsely(); @@ -90,7 +94,7 @@ public function __construct() { $this->api_controller = new REST_API_Controller( $this->parsely ); } - parent::__construct(); + parent::__construct( $name, $data, $data_name ); } /** diff --git a/tests/Integration/RestAPI/Settings/BaseSettingsEndpointTest.php b/tests/Integration/RestAPI/Settings/BaseSettingsEndpointTest.php new file mode 100644 index 0000000000..8d7b18f114 --- /dev/null +++ b/tests/Integration/RestAPI/Settings/BaseSettingsEndpointTest.php @@ -0,0 +1,219 @@ + $extra_data Any Extra key/value pairs to add. + * @return array The generated JSON array. + */ + abstract protected function generate_json( + ?string $metric = null, + ?string $period = null, + array $extra_data = array() + ): array; + + /** + * Returns the default value for the endpoint. + * + * @since 3.17.0 + * + * @return array The default value for the endpoint. + */ + abstract protected function get_default_value(): array; + + /** + * Verifies that the route is registered. + * + * @since 3.17.0 + */ + protected function run_test_route_is_registered(): void { + $routes = rest_get_server()->get_routes(); + + // Check that the main route is registered. + $expected_route = $this->get_endpoint()->get_full_endpoint( '/' ); + self::assertArrayHasKey( $expected_route, $routes ); + + // Check that the route is associated with the POST method. + $route_data = $routes[ $expected_route ]; + self::assertArrayHasKey( 'GET', $route_data[0]['methods'] ); + self::assertArrayHasKey( 'PUT', $route_data[0]['methods'] ); + + // Check the `/get` route. + $expected_route = $this->get_endpoint()->get_full_endpoint( '/get' ); + self::assertArrayHasKey( $expected_route, $routes ); + + // Check that the route is associated with the GET method. + $route_data = $routes[ $expected_route ]; + self::assertArrayHasKey( 'GET', $route_data[0]['methods'] ); + + // Check the `/set` route. + $expected_route = $this->get_endpoint()->get_full_endpoint( '/set' ); + self::assertArrayHasKey( $expected_route, $routes ); + + // Check that the route is associated with the PUT method. + $route_data = $routes[ $expected_route ]; + self::assertArrayHasKey( 'PUT', $route_data[0]['methods'] ); + } + + /** + * Verifies that the endpoint returns the correct default value. + * + * @since 3.13.0 + * @since 3.17.0 Moved from the old BaseUserMetaEndpointTest class. + */ + public function run_test_endpoint_returns_value_on_get_request(): void { + $this->set_current_user_to_admin(); + + $value = rest_do_request( + new WP_REST_Request( + 'GET', + $this->get_endpoint()->get_full_endpoint( '/get' ) + ) + )->get_data(); + $value = $this->wp_json_encode( $value ); + + $expected = $this->wp_json_encode( + $this->get_default_value() + ); + + self::assertSame( $expected, $value ); + } + + /** + * Provides data for testing PUT requests. + * + * @since 3.13.0 + * @since 3.17.0 Moved from the old BaseUserMetaEndpointTest class. + * + * @return iterable + */ + public function provide_put_requests_data(): iterable { + $default_value = $this->generate_json( 'views', '7d' ); + $valid_value = $this->generate_json( 'avg_engaged', '1h' ); + + // Valid non-default value. It should be returned unmodified. + yield 'valid period and metric values' => array( + 'test_data' => $valid_value, + 'expected' => $valid_value, + ); + + // Missing or problematic keys. Defaults should be used for the missing or problematic keys. + yield 'valid period value, no metric value' => array( + 'test_data' => $this->generate_json( null, '1h' ), + 'expected' => $this->generate_json( 'views', '1h' ), + ); + yield 'valid metric value, no period value' => array( + 'test_data' => $this->generate_json( 'avg_engaged' ), + 'expected' => $this->generate_json( 'avg_engaged', '7d' ), + ); + yield 'no values' => array( + 'test_data' => $this->generate_json(), + 'expected' => $default_value, + ); + + // Invalid values. They should be adjusted to their defaults. + yield 'invalid period value' => array( + 'test_data' => $this->generate_json( 'avg_engaged', 'invalid' ), + 'expected' => $this->generate_json( 'avg_engaged', '7d' ), + ); + yield 'invalid metric value' => array( + 'test_data' => $this->generate_json( 'invalid', '1h' ), + 'expected' => $this->generate_json( 'views', '1h' ), + ); + yield 'invalid period and metric values' => array( + 'test_data' => $this->generate_json( 'invalid', 'invalid' ), + 'expected' => $default_value, + ); + + // Invalid extra data passed. Any such data should be discarded. + yield 'invalid additional value' => array( + 'test_data' => $this->generate_json( + 'avg_engaged', + '1h', + array( 'invalid' ) + ), + 'expected' => $valid_value, + ); + yield 'invalid additional key/value pair' => array( + 'test_data' => $this->generate_json( + 'avg_engaged', + '1h', + array( 'invalid_key' => 'invalid_value' ) + ), + 'expected' => $valid_value, + ); + } + + /** + * Sends a PUT request to the endpoint. + * + * @since 3.13.0 + * @since 3.17.0 Moved from the old BaseUserMetaEndpointTest class. + * + * @param array $data The data to be sent in the request. + * @return array The response returned by the endpoint. + */ + protected function send_put_request( array $data ): array { + $this->set_current_user_to_admin(); + $result = $this->send_wp_rest_request( + 'PUT', + $this->get_endpoint()->get_full_endpoint( '/set' ), + $this->wp_json_encode( $data ) + ); + + if ( ! is_array( $result ) ) { + return array(); + } + + return $result; + } + + /** + * Test that the endpoint is not available if the API secret is not set. + * This test should be disabled since the endpoint does not requires the API secret. + * + * @since 3.17.0 + * @coversNothing + */ + public function test_is_available_to_current_user_returns_error_api_secret_not_set(): void { + // This test is disabled since the endpoint does not requires the API secret. + self::assertTrue( true ); + } + + /** + * Test that the endpoint is not available if the site ID is not set. + * This test should be disabled since the endpoint does not requires the site ID. + * + * @since 3.17.0 + * @coversNothing + */ + public function test_is_available_to_current_user_returns_error_site_id_not_set(): void { + // This test is disabled since the endpoint does not requires the site ID. + self::assertTrue( true ); + } +} diff --git a/tests/Integration/RestAPI/Settings/EndpointDashboardWidgetSettingsTest.php b/tests/Integration/RestAPI/Settings/EndpointDashboardWidgetSettingsTest.php new file mode 100644 index 0000000000..b1ebc2035c --- /dev/null +++ b/tests/Integration/RestAPI/Settings/EndpointDashboardWidgetSettingsTest.php @@ -0,0 +1,203 @@ +api_controller = new Content_Helper_Controller( $this->parsely ); + $this->endpoint = new Endpoint_Dashboard_Widget_Settings( $this->api_controller ); + + parent::set_up(); + } + + /** + * Returns the endpoint to be used in tests. + * + * @since 3.17.0 + * + * @return \Parsely\REST_API\Base_Endpoint + */ + public function get_endpoint(): \Parsely\REST_API\Base_Endpoint { + return $this->endpoint; + } + + /** + * Returns the default value for the endpoint. + * + * @since 3.17.0 + * + * @return array The default value for the endpoint. + */ + public function get_default_value(): array { + return array( + 'Metric' => 'views', + 'Period' => '7d', + ); + } + + /** + * Generates a JSON array for the passed period, metric, and extra data. + * + * @since 3.13.0 + * + * @param string|null $metric The Metric value. + * @param string|null $period The Period value. + * @param array $extra_data Any Extra key/value pairs to add. + * @return array The generated JSON array. + */ + protected function generate_json( + ?string $metric = null, + ?string $period = null, + array $extra_data = array() + ): array { + $array = $this->get_default_value(); + unset( $array['Metric'], $array['Period'] ); + + if ( null !== $metric ) { + $array['Metric'] = $metric; + } + + if ( null !== $period ) { + $array['Period'] = $period; + } + + ksort( $array ); + + return array_merge( $array, $extra_data ); + } + + /** + * Verifies that the route is registered. + * + * @since 3.17.0 + * + * @covers \Parsely\REST_API\Settings\Endpoint_Dashboard_Widget_Settings::register_routes + * @uses \Parsely\REST_API\Base_API_Controller::__construct + * @uses \Parsely\REST_API\Base_API_Controller::get_full_namespace + * @uses \Parsely\REST_API\Base_API_Controller::get_parsely + * @uses \Parsely\REST_API\Base_API_Controller::prefix_route + * @uses \Parsely\REST_API\Base_Endpoint::__construct + * @uses \Parsely\REST_API\Base_Endpoint::get_full_endpoint + * @uses \Parsely\REST_API\Base_Endpoint::init + * @uses \Parsely\REST_API\Base_Endpoint::register_rest_route + * @uses \Parsely\REST_API\Content_Helper\Content_Helper_Controller::get_route_prefix + * @uses \Parsely\REST_API\REST_API_Controller::get_namespace + * @uses \Parsely\REST_API\REST_API_Controller::get_version + * @uses \Parsely\REST_API\Settings\Base_Settings_Endpoint::__construct + * @uses \Parsely\REST_API\Settings\Base_Settings_Endpoint::init + * @uses \Parsely\REST_API\Settings\Base_Settings_Endpoint::is_available_to_current_user + * @uses \Parsely\REST_API\Settings\Endpoint_Dashboard_Widget_Settings::get_endpoint_name + * @uses \Parsely\REST_API\Settings\Endpoint_Dashboard_Widget_Settings::get_subvalues_specs + * @uses \Parsely\Utils\Utils::convert_endpoint_to_filter_key + */ + public function test_route_is_registered(): void { + parent::run_test_route_is_registered(); + } + + /** + * Verifies that the endpoint returns the correct default settings. + * + * @since 3.13.0 + * @since 3.17.0 Moved from old test class. + * + * @covers \Parsely\REST_API\Settings\Endpoint_Dashboard_Widget_Settings::process_request + * @covers \Parsely\REST_API\Settings\Endpoint_Dashboard_Widget_Settings::get_settings + * @uses \Parsely\REST_API\Base_API_Controller::__construct + * @uses \Parsely\REST_API\Base_API_Controller::get_full_namespace + * @uses \Parsely\REST_API\Base_API_Controller::get_parsely + * @uses \Parsely\REST_API\Base_API_Controller::prefix_route + * @uses \Parsely\REST_API\Base_Endpoint::__construct + * @uses \Parsely\REST_API\Base_Endpoint::get_full_endpoint + * @uses \Parsely\REST_API\Base_Endpoint::init + * @uses \Parsely\REST_API\Base_Endpoint::register_rest_route + * @uses \Parsely\REST_API\Content_Helper\Content_Helper_Controller::get_route_prefix + * @uses \Parsely\REST_API\REST_API_Controller::get_namespace + * @uses \Parsely\REST_API\REST_API_Controller::get_version + * @uses \Parsely\REST_API\Settings\Base_Settings_Endpoint::__construct + * @uses \Parsely\REST_API\Settings\Base_Settings_Endpoint::init + * @uses \Parsely\REST_API\Settings\Base_Settings_Endpoint::is_available_to_current_user + * @uses \Parsely\REST_API\Settings\Base_Settings_Endpoint::register_routes + * @uses \Parsely\REST_API\Settings\Endpoint_Dashboard_Widget_Settings::get_endpoint_name + * @uses \Parsely\REST_API\Settings\Endpoint_Dashboard_Widget_Settings::get_meta_key + * @uses \Parsely\REST_API\Settings\Endpoint_Dashboard_Widget_Settings::get_subvalues_specs + * @uses \Parsely\Utils\Utils::convert_endpoint_to_filter_key + */ + public function test_endpoint_returns_value_on_get_request(): void { + parent::run_test_endpoint_returns_value_on_get_request(); + } + + /** + * Verifies that the endpoint can correctly handle PUT requests. + * + * @since 3.13.0 + * @since 3.17.0 Moved from old test class. + * + * @param array $test_data The data to send in the PUT request. + * @param array $expected The expected value of the setting after the PUT request. + * + * @covers \Parsely\REST_API\Settings\Endpoint_Dashboard_Widget_Settings::get_subvalues_specs + * @covers \Parsely\REST_API\Settings\Endpoint_Dashboard_Widget_Settings::sanitize_subvalue + * @covers \Parsely\REST_API\Settings\Endpoint_Dashboard_Widget_Settings::sanitize_value + * @covers \Parsely\REST_API\Settings\Endpoint_Dashboard_Widget_Settings::set_settings + * @uses \Parsely\REST_API\Base_API_Controller::__construct + * @uses \Parsely\REST_API\Base_API_Controller::get_full_namespace + * @uses \Parsely\REST_API\Base_API_Controller::get_parsely + * @uses \Parsely\REST_API\Base_API_Controller::prefix_route + * @uses \Parsely\REST_API\Base_Endpoint::__construct + * @uses \Parsely\REST_API\Base_Endpoint::get_full_endpoint + * @uses \Parsely\REST_API\Base_Endpoint::init + * @uses \Parsely\REST_API\Base_Endpoint::register_rest_route + * @uses \Parsely\REST_API\Content_Helper\Content_Helper_Controller::get_route_prefix + * @uses \Parsely\REST_API\REST_API_Controller::get_namespace + * @uses \Parsely\REST_API\REST_API_Controller::get_version + * @uses \Parsely\REST_API\Settings\Base_Settings_Endpoint::__construct + * @uses \Parsely\REST_API\Settings\Base_Settings_Endpoint::get_settings + * @uses \Parsely\REST_API\Settings\Base_Settings_Endpoint::get_valid_values + * @uses \Parsely\REST_API\Settings\Base_Settings_Endpoint::init + * @uses \Parsely\REST_API\Settings\Base_Settings_Endpoint::is_available_to_current_user + * @uses \Parsely\REST_API\Settings\Base_Settings_Endpoint::register_routes + * @uses \Parsely\REST_API\Settings\Base_Settings_Endpoint::get_default + * @uses \Parsely\REST_API\Settings\Endpoint_Dashboard_Widget_Settings::get_endpoint_name + * @uses \Parsely\REST_API\Settings\Endpoint_Dashboard_Widget_Settings::get_meta_key + * @uses \Parsely\Utils\Utils::convert_endpoint_to_filter_key + * @dataProvider provide_put_requests_data + */ + public function test_endpoint_correctly_handles_put_requests( + array $test_data, + array $expected + ): void { + $value = $this->send_put_request( $test_data ); + self::assertSame( $expected, $value ); + } +} diff --git a/tests/Integration/RestAPI/Settings/EndpointEditorSidebarSettingsTest.php b/tests/Integration/RestAPI/Settings/EndpointEditorSidebarSettingsTest.php new file mode 100644 index 0000000000..3b9bb654cb --- /dev/null +++ b/tests/Integration/RestAPI/Settings/EndpointEditorSidebarSettingsTest.php @@ -0,0 +1,314 @@ +api_controller = new Content_Helper_Controller( $this->parsely ); + $this->endpoint = new Endpoint_Editor_Sidebar_Settings( $this->api_controller ); + + parent::set_up(); + } + + /** + * Returns the endpoint to be used in tests. + * + * @since 3.17.0 + * + * @return \Parsely\REST_API\Base_Endpoint + */ + public function get_endpoint(): \Parsely\REST_API\Base_Endpoint { + return $this->endpoint; + } + + /** + * Returns the default value for the endpoint. + * + * @since 3.17.0 + * + * @return array The default value for the endpoint. + */ + public function get_default_value(): array { + return array( + 'InitialTabName' => 'tools', + 'PerformanceStats' => array( + 'Period' => '7d', + 'VisibleDataPoints' => array( 'views', 'visitors', 'avgEngaged', 'recirculation' ), + 'VisiblePanels' => array( 'overview', 'categories', 'referrers' ), + ), + 'RelatedPosts' => array( + 'FilterBy' => 'unavailable', + 'FilterValue' => '', + 'Metric' => 'views', + 'Open' => false, + 'Period' => '7d', + ), + 'SmartLinking' => array( + 'MaxLinks' => 10, + 'MaxLinkWords' => 4, + 'Open' => false, + ), + 'TitleSuggestions' => array( + 'Open' => false, + 'Persona' => 'journalist', + 'Tone' => 'neutral', + ), + ); + } + + /** + * Verifies that the route is registered. + * + * @since 3.17.0 + * + * @covers \Parsely\REST_API\Settings\Endpoint_Editor_Sidebar_Settings::register_routes + * @uses \Parsely\REST_API\Base_API_Controller::__construct + * @uses \Parsely\REST_API\Base_API_Controller::get_full_namespace + * @uses \Parsely\REST_API\Base_API_Controller::get_parsely + * @uses \Parsely\REST_API\Base_API_Controller::prefix_route + * @uses \Parsely\REST_API\Base_Endpoint::__construct + * @uses \Parsely\REST_API\Base_Endpoint::get_full_endpoint + * @uses \Parsely\REST_API\Base_Endpoint::init + * @uses \Parsely\REST_API\Base_Endpoint::register_rest_route + * @uses \Parsely\REST_API\Content_Helper\Content_Helper_Controller::get_route_prefix + * @uses \Parsely\REST_API\REST_API_Controller::get_namespace + * @uses \Parsely\REST_API\REST_API_Controller::get_version + * @uses \Parsely\REST_API\Settings\Base_Settings_Endpoint::__construct + * @uses \Parsely\REST_API\Settings\Base_Settings_Endpoint::init + * @uses \Parsely\REST_API\Settings\Base_Settings_Endpoint::is_available_to_current_user + * @uses \Parsely\REST_API\Settings\Endpoint_Editor_Sidebar_Settings::get_endpoint_name + * @uses \Parsely\REST_API\Settings\Endpoint_Editor_Sidebar_Settings::get_subvalues_specs + * @uses \Parsely\Utils\Utils::convert_endpoint_to_filter_key + */ + public function test_route_is_registered(): void { + parent::run_test_route_is_registered(); + } + + /** + * Verifies that the endpoint returns the correct default settings. + * + * @since 3.13.0 + * @since 3.17.0 Moved from old test class. + * + * @covers \Parsely\REST_API\Settings\Endpoint_Editor_Sidebar_Settings::process_request + * @covers \Parsely\REST_API\Settings\Endpoint_Editor_Sidebar_Settings::get_settings + * @uses \Parsely\REST_API\Base_API_Controller::__construct + * @uses \Parsely\REST_API\Base_API_Controller::get_full_namespace + * @uses \Parsely\REST_API\Base_API_Controller::get_parsely + * @uses \Parsely\REST_API\Base_API_Controller::prefix_route + * @uses \Parsely\REST_API\Base_Endpoint::__construct + * @uses \Parsely\REST_API\Base_Endpoint::get_full_endpoint + * @uses \Parsely\REST_API\Base_Endpoint::init + * @uses \Parsely\REST_API\Base_Endpoint::register_rest_route + * @uses \Parsely\REST_API\Content_Helper\Content_Helper_Controller::get_route_prefix + * @uses \Parsely\REST_API\REST_API_Controller::get_namespace + * @uses \Parsely\REST_API\REST_API_Controller::get_version + * @uses \Parsely\REST_API\Settings\Base_Settings_Endpoint::__construct + * @uses \Parsely\REST_API\Settings\Base_Settings_Endpoint::init + * @uses \Parsely\REST_API\Settings\Base_Settings_Endpoint::is_available_to_current_user + * @uses \Parsely\REST_API\Settings\Base_Settings_Endpoint::register_routes + * @uses \Parsely\REST_API\Settings\Endpoint_Editor_Sidebar_Settings::get_endpoint_name + * @uses \Parsely\REST_API\Settings\Endpoint_Editor_Sidebar_Settings::get_meta_key + * @uses \Parsely\REST_API\Settings\Endpoint_Editor_Sidebar_Settings::get_subvalues_specs + * @uses \Parsely\Utils\Utils::convert_endpoint_to_filter_key + */ + public function test_endpoint_returns_value_on_get_request(): void { + parent::run_test_endpoint_returns_value_on_get_request(); + } + + /** + * Verifies that the endpoint can correctly handle PUT requests. + * + * @param array $test_data The data to send in the PUT request. + * @param array $expected The expected value of the setting after the PUT request. + * + * @since 3.13.0 + * @since 3.17.0 Moved from old test class. + * + * @covers \Parsely\REST_API\Settings\Endpoint_Editor_Sidebar_Settings::get_subvalues_specs + * @covers \Parsely\REST_API\Settings\Endpoint_Editor_Sidebar_Settings::sanitize_subvalue + * @covers \Parsely\REST_API\Settings\Endpoint_Editor_Sidebar_Settings::sanitize_value + * @covers \Parsely\REST_API\Settings\Endpoint_Editor_Sidebar_Settings::set_settings + * @uses \Parsely\REST_API\Base_API_Controller::__construct + * @uses \Parsely\REST_API\Base_API_Controller::get_full_namespace + * @uses \Parsely\REST_API\Base_API_Controller::get_parsely + * @uses \Parsely\REST_API\Base_API_Controller::prefix_route + * @uses \Parsely\REST_API\Base_Endpoint::__construct + * @uses \Parsely\REST_API\Base_Endpoint::get_full_endpoint + * @uses \Parsely\REST_API\Base_Endpoint::init + * @uses \Parsely\REST_API\Base_Endpoint::register_rest_route + * @uses \Parsely\REST_API\Content_Helper\Content_Helper_Controller::get_route_prefix + * @uses \Parsely\REST_API\REST_API_Controller::get_namespace + * @uses \Parsely\REST_API\REST_API_Controller::get_version + * @uses \Parsely\REST_API\Settings\Base_Settings_Endpoint::__construct + * @uses \Parsely\REST_API\Settings\Base_Settings_Endpoint::get_nested_specs + * @uses \Parsely\REST_API\Settings\Base_Settings_Endpoint::get_settings + * @uses \Parsely\REST_API\Settings\Base_Settings_Endpoint::get_valid_values + * @uses \Parsely\REST_API\Settings\Base_Settings_Endpoint::init + * @uses \Parsely\REST_API\Settings\Base_Settings_Endpoint::is_available_to_current_user + * @uses \Parsely\REST_API\Settings\Base_Settings_Endpoint::register_routes + * @uses \Parsely\REST_API\Settings\Base_Settings_Endpoint::get_default + * @uses \Parsely\REST_API\Settings\Endpoint_Editor_Sidebar_Settings::get_endpoint_name + * @uses \Parsely\REST_API\Settings\Endpoint_Editor_Sidebar_Settings::get_meta_key + * @uses \Parsely\Utils\Utils::convert_endpoint_to_filter_key + * @dataProvider provide_put_requests_data* + */ + public function test_endpoint_correctly_handles_put_requests( + array $test_data, + array $expected + ): void { + $value = $this->send_put_request( $test_data ); + self::assertSame( $expected, $value ); + } + + /** + * Tests that the endpoint can correctly handle PUT requests with valid + * nested PerformanceStats values. + * + * @since 3.14.0 + * @since 3.17.0 Moved from old test class. + * + * @covers \Parsely\REST_API\Settings\Endpoint_Editor_Sidebar_Settings::sanitize_subvalue + * @uses \Parsely\REST_API\Base_API_Controller::__construct + * @uses \Parsely\REST_API\Base_API_Controller::get_full_namespace + * @uses \Parsely\REST_API\Base_API_Controller::get_parsely + * @uses \Parsely\REST_API\Base_API_Controller::prefix_route + * @uses \Parsely\REST_API\Base_Endpoint::__construct + * @uses \Parsely\REST_API\Base_Endpoint::get_full_endpoint + * @uses \Parsely\REST_API\Base_Endpoint::init + * @uses \Parsely\REST_API\Base_Endpoint::register_rest_route + * @uses \Parsely\REST_API\Content_Helper\Content_Helper_Controller::get_route_prefix + * @uses \Parsely\REST_API\REST_API_Controller::get_namespace + * @uses \Parsely\REST_API\REST_API_Controller::get_version + * @uses \Parsely\REST_API\Settings\Base_Settings_Endpoint::__construct + * @uses \Parsely\REST_API\Settings\Base_Settings_Endpoint::get_nested_specs + * @uses \Parsely\REST_API\Settings\Base_Settings_Endpoint::get_settings + * @uses \Parsely\REST_API\Settings\Base_Settings_Endpoint::get_valid_values + * @uses \Parsely\REST_API\Settings\Base_Settings_Endpoint::init + * @uses \Parsely\REST_API\Settings\Base_Settings_Endpoint::is_available_to_current_user + * @uses \Parsely\REST_API\Settings\Base_Settings_Endpoint::register_routes + * @uses \Parsely\REST_API\Settings\Base_Settings_Endpoint::sanitize_value + * @uses \Parsely\REST_API\Settings\Base_Settings_Endpoint::set_settings + * @uses \Parsely\REST_API\Settings\Endpoint_Editor_Sidebar_Settings::get_endpoint_name + * @uses \Parsely\REST_API\Settings\Endpoint_Editor_Sidebar_Settings::get_meta_key + * @uses \Parsely\REST_API\Settings\Endpoint_Editor_Sidebar_Settings::get_subvalues_specs + * @uses \Parsely\Utils\Utils::convert_endpoint_to_filter_key + */ + public function test_valid_nested_performance_stats_settings_period(): void { + $this->set_current_user_to_admin(); + + $value = $this->send_put_request( + $this->generate_json( + 'views', + '7d', + array( + 'PerformanceStats' => array( + 'Period' => '1h', + 'VisibleDataPoints' => array( 'views', 'avgEngaged', 'recirculation' ), + 'VisiblePanels' => array( 'overview', 'referrers' ), + ), + ) + ) + ); + + $expected = array_merge( + $this->get_default_value(), + array( + 'PerformanceStats' => array( + 'Period' => '1h', + 'VisibleDataPoints' => array( 'views', 'avgEngaged', 'recirculation' ), + 'VisiblePanels' => array( 'overview', 'referrers' ), + ), + ) + ); + + self::assertSame( $expected, $value ); + } + + /** + * Generates a JSON array for the passed period, metric, and extra data. + * + * @since 3.13.0 + * @since 3.17.0 Moved from old test class. + * + * @param string|null $metric The RelatedPostsMetric value. + * @param string|null $period The RelatedPostsPeriod value. + * @param array $extra_data Any Extra key/value pairs to add. + * @return array The generated JSON array. + */ + protected function generate_json( + ?string $metric = null, + ?string $period = null, + array $extra_data = array() + ): array { + $array = $this->get_default_value(); + assert( is_array( $array['RelatedPosts'] ) ); + + unset( $array['RelatedPosts']['Metric'], $array['RelatedPosts']['Period'] ); + + if ( null !== $metric ) { + $array['RelatedPosts']['Metric'] = $metric; + } + + if ( null !== $period ) { + $array['RelatedPosts']['Period'] = $period; + } + + $merged_array = array_merge( $array, $extra_data ); + + $this->ksortRecursive( $merged_array, SORT_NATURAL | SORT_FLAG_CASE ); + + return $merged_array; + } + + /** + * Recursively sorts an array by key using a specified sort flag. + * + * @since 3.14.3 + * @since 3.17.0 Moved from old test class. + * + * @param array &$unsorted_array The array to be sorted, passed by reference. + * @param int $sort_flags Optional sorting flags. Defaults to SORT_REGULAR. + */ + private function ksortRecursive( array &$unsorted_array, int $sort_flags = SORT_REGULAR ): void { + ksort( $unsorted_array, $sort_flags ); + foreach ( $unsorted_array as &$value ) { + if ( is_array( $value ) ) { + $this->ksortRecursive( $value, $sort_flags ); + } + } + } +} From de7630a2f23fc975246b7b5d38721c29f2f37b5b Mon Sep 17 00:00:00 2001 From: Henrique Mouta Date: Fri, 30 Aug 2024 13:30:28 +0100 Subject: [PATCH 24/49] Remove old code --- src/Endpoints/class-base-api-proxy.php | 281 ------------- .../class-base-endpoint-user-meta.php | 391 ------------------ ...ass-dashboard-widget-settings-endpoint.php | 54 --- ...class-editor-sidebar-settings-endpoint.php | 102 ----- .../class-content-suggestions-base-api.php | 3 +- .../common/class-content-helper-feature.php | 8 +- .../class-dashboard-widget.php | 3 +- .../editor-sidebar/class-editor-sidebar.php | 4 +- ...ass-endpoint-dashboard-widget-settings.php | 4 +- ...class-endpoint-editor-sidebar-settings.php | 4 +- .../Endpoints/Proxy/BaseProxyEndpointTest.php | 245 ----------- .../Endpoints/RestMetadataTest.php | 1 - .../UserMeta/BaseUserMetaEndpointTest.php | 169 -------- .../DashboardWidgetSettingsEndpointTest.php | 204 --------- .../EditorSidebarSettingsEndpointTest.php | 308 -------------- wp-parsely.php | 47 --- 16 files changed, 10 insertions(+), 1818 deletions(-) delete mode 100644 src/Endpoints/class-base-api-proxy.php delete mode 100644 src/Endpoints/user-meta/class-base-endpoint-user-meta.php delete mode 100644 src/Endpoints/user-meta/class-dashboard-widget-settings-endpoint.php delete mode 100644 src/Endpoints/user-meta/class-editor-sidebar-settings-endpoint.php delete mode 100644 tests/Integration/Endpoints/Proxy/BaseProxyEndpointTest.php delete mode 100644 tests/Integration/Endpoints/UserMeta/BaseUserMetaEndpointTest.php delete mode 100644 tests/Integration/Endpoints/UserMeta/DashboardWidgetSettingsEndpointTest.php delete mode 100644 tests/Integration/Endpoints/UserMeta/EditorSidebarSettingsEndpointTest.php diff --git a/src/Endpoints/class-base-api-proxy.php b/src/Endpoints/class-base-api-proxy.php deleted file mode 100644 index 6275431287..0000000000 --- a/src/Endpoints/class-base-api-proxy.php +++ /dev/null @@ -1,281 +0,0 @@ - $response The response received by the proxy. - * @return array The generated data. - */ - abstract protected function generate_data( $response ): array; - - /** - * Cached "proxy" to the Parse.ly API endpoint. - * - * @param WP_REST_Request $request The request object. - * @return stdClass|WP_Error stdClass containing the data or a WP_Error object on failure. - */ - abstract public function get_items( WP_REST_Request $request ); - - /** - * Returns whether the endpoint is available for access by the current - * user. - * - * @since 3.14.0 Renamed from `permission_callback()`. - * - * @return bool - */ - public function is_available_to_current_user(): bool { - return $this->api->is_available_to_current_user(); - } - - /** - * Constructor. - * - * @param Parsely $parsely Instance of Parsely class. - * @param Remote_API_Interface $api API object which does the actual calls to the Parse.ly API. - */ - public function __construct( Parsely $parsely, Remote_API_Interface $api ) { - $this->parsely = $parsely; - $this->api = $api; - } - - /** - * Registers the endpoint's WP REST route. - * - * @param string $endpoint The endpoint's route (e.g. /stats/posts). - * @param array $methods The HTTP methods to use for the endpoint. - */ - protected function register_endpoint( string $endpoint, array $methods = array( WP_REST_Server::READABLE ) ): void { - if ( ! apply_filters( 'wp_parsely_enable_' . Utils::convert_endpoint_to_filter_key( $endpoint ) . '_api_proxy', true ) ) { - return; - } - - $get_items_args = array( - 'query' => array( - 'default' => array(), - 'sanitize_callback' => function ( array $query ) { - $sanitized_query = array(); - foreach ( $query as $key => $value ) { - $sanitized_query[ sanitize_key( $key ) ] = sanitize_text_field( $value ); - } - - return $sanitized_query; - }, - ), - ); - - $rest_route_args = array( - array( - 'methods' => $methods, - 'callback' => array( $this, 'get_items' ), - 'permission_callback' => array( $this, 'is_available_to_current_user' ), - 'args' => $get_items_args, - 'show_in_index' => $this->is_available_to_current_user(), - ), - ); - - register_rest_route( 'wp-parsely/v1', $endpoint, $rest_route_args ); - } - - /** - * Cached "proxy" to the endpoint. - * - * @param WP_REST_Request $request The request object. - * @param bool $require_api_secret Specifies if the API Secret is required. - * @param string|null $param_item The param element to use to get the items. - * @return stdClass|WP_Error stdClass containing the data or a WP_Error object on failure. - */ - protected function get_data( WP_REST_Request $request, bool $require_api_secret = true, string $param_item = null ) { - // Validate Site ID and secret. - $validation = $this->validate_apikey_and_secret( $require_api_secret ); - if ( is_wp_error( $validation ) ) { - return $validation; - } - - if ( null !== $param_item ) { - $params = $request->get_param( $param_item ); - } else { - $params = $request->get_params(); - } - - if ( is_array( $params ) && isset( $params['itm_source'] ) ) { - $this->itm_source = $params['itm_source']; - } - - // A proxy with caching behavior is used here. - $response = $this->api->get_items( $params ); - - if ( is_wp_error( $response ) ) { - return $response; - } - - return (object) array( - 'data' => $this->generate_data( $response ), // @phpstan-ignore-line. - ); - } - - /** - * Validates that the Site ID and secret are set. - * If the API secret is not required, it will not be validated. - * - * @since 3.13.0 - * - * @param bool $require_api_secret Specifies if the API Secret is required. - * @return WP_Error|bool - */ - protected function validate_apikey_and_secret( bool $require_api_secret = true ) { - if ( false === $this->parsely->site_id_is_set() ) { - return new WP_Error( - 'parsely_site_id_not_set', - __( 'A Parse.ly Site ID must be set in site options to use this endpoint', 'wp-parsely' ), - array( 'status' => 403 ) - ); - } - - if ( $require_api_secret && false === $this->parsely->api_secret_is_set() ) { - return new WP_Error( - 'parsely_api_secret_not_set', - __( 'A Parse.ly API Secret must be set in site options to use this endpoint', 'wp-parsely' ), - array( 'status' => 403 ) - ); - } - - return true; - } - - /** - * Extracts the post data from the passed object. - * - * Should only be used with endpoints that return post data. - * - * @since 3.10.0 - * - * @param stdClass $item The object to extract the data from. - * @return array The extracted data. - */ - protected function extract_post_data( stdClass $item ): array { - $data = array(); - - if ( isset( $item->author ) ) { - $data['author'] = $item->author; - } - - if ( isset( $item->metrics->views ) ) { - $data['views'] = number_format_i18n( $item->metrics->views ); - } - - if ( isset( $item->metrics->visitors ) ) { - $data['visitors'] = number_format_i18n( $item->metrics->visitors ); - } - - // The avg_engaged metric can be in different locations depending on the - // endpoint and passed sort/url parameters. - $avg_engaged = $item->metrics->avg_engaged ?? $item->avg_engaged ?? null; - if ( null !== $avg_engaged ) { - $data['avgEngaged'] = Utils::get_formatted_duration( (float) $avg_engaged ); - } - - if ( isset( $item->pub_date ) ) { - $data['date'] = wp_date( Utils::get_date_format(), strtotime( $item->pub_date ) ); - } - - if ( isset( $item->title ) ) { - $data['title'] = $item->title; - } - - if ( isset( $item->url ) ) { - $site_id = $this->parsely->get_site_id(); - // phpcs:ignore WordPressVIPMinimum.Functions.RestrictedFunctions.url_to_postid_url_to_postid - $post_id = url_to_postid( $item->url ); // 0 if the post cannot be found. - - $post_url = Parsely::get_url_with_itm_source( $item->url, null ); - if ( Utils::parsely_is_https_supported() ) { - $post_url = str_replace( 'http://', 'https://', $post_url ); - } - - $data['rawUrl'] = $post_url; - $data['dashUrl'] = Parsely::get_dash_url( $site_id, $post_url ); - $data['id'] = Parsely::get_url_with_itm_source( $post_url, null ); // Unique. - $data['postId'] = $post_id; // Might not be unique. - $data['url'] = Parsely::get_url_with_itm_source( $post_url, $this->itm_source ); - - // Set thumbnail URL, falling back to the Parse.ly thumbnail if needed. - $thumbnail_url = get_the_post_thumbnail_url( $post_id, 'thumbnail' ); - if ( false !== $thumbnail_url ) { - $data['thumbnailUrl'] = $thumbnail_url; - } elseif ( isset( $item->thumb_url_medium ) ) { - $data['thumbnailUrl'] = $item->thumb_url_medium; - } - } - - return $data; - } - - /** - * Generates the post data from the passed response. - * - * Should only be used with endpoints that return post data. - * - * @since 3.10.0 - * - * @param array $response The response received by the proxy. - * @return array The generated data. - */ - protected function generate_post_data( array $response ): array { - $data = array(); - - foreach ( $response as $item ) { - $data [] = (object) $this->extract_post_data( $item ); - } - - return $data; - } -} diff --git a/src/Endpoints/user-meta/class-base-endpoint-user-meta.php b/src/Endpoints/user-meta/class-base-endpoint-user-meta.php deleted file mode 100644 index 72d1ca9e36..0000000000 --- a/src/Endpoints/user-meta/class-base-endpoint-user-meta.php +++ /dev/null @@ -1,391 +0,0 @@ -, default: mixed} - */ -abstract class Base_Endpoint_User_Meta extends Base_Endpoint { - /** - * The meta entry's default value. Initialized in the constructor. - * - * @since 3.13.0 - * - * @var array - */ - protected $default_value = array(); - - /** - * The valid values that can be used for each subvalue. Initialized in the - * constructor. - * - * @since 3.13.0 - * - * @var array> - */ - protected $valid_subvalues = array(); - - /** - * The current user's ID. - * - * @since 3.14.0 - * - * @var int - */ - protected $current_user_id = 0; - - /** - * Returns the meta entry's key. - * - * @since 3.13.0 - * - * @return string The meta entry's key. - */ - abstract protected function get_meta_key(): string; - - /** - * Returns the endpoint's subvalues specifications. - * - * @since 3.13.0 - * - * @return array - */ - abstract protected function get_subvalues_specs(): array; - - /** - * Constructor. - * - * @since 3.13.0 - * - * @param Parsely $parsely Parsely instance. - */ - public function __construct( Parsely $parsely ) { - parent::__construct( $parsely ); - - $subvalues_specs = $this->get_subvalues_specs(); - - foreach ( $subvalues_specs as $key => $value ) { - $this->default_value[ $key ] = $value['default']; - $this->valid_subvalues[ $key ] = $value['values']; - } - } - - /** - * Registers the endpoint's WP REST route. - * - * @since 3.13.0 - */ - public function run(): void { - // Initialize the current user ID here, as doing it in the constructor - // is too early. - $this->current_user_id = get_current_user_id(); - - $this->register_endpoint( - static::get_route(), - 'process_request', - array( 'GET', 'PUT' ) - ); - } - - /** - * Returns the endpoint's route. - * - * @since 3.13.0 - * - * @return string The endpoint's route. - */ - public static function get_route(): string { - return static::ENDPOINT; - } - - /** - * Processes the requests sent to the endpoint. - * - * @since 3.13.0 - * - * @param WP_REST_Request $request The request sent to the endpoint. - * @return string The meta entry's value as JSON. - */ - public function process_request( WP_REST_Request $request ): string { - $request_method = $request->get_method(); - - // Update the meta entry's value if the request method is PUT. - if ( 'PUT' === $request_method ) { - $meta_value = $request->get_json_params(); - $this->set_value( $meta_value ); - } - - return $this->get_value(); - } - - /** - * Returns whether the endpoint is available for access by the current - * user. - * - * @since 3.14.0 - * @since 3.16.0 Added the `$request` parameter. - * - * @param WP_REST_Request|null $request The request object. - * @return bool - */ - public function is_available_to_current_user( $request = null ): bool { - return current_user_can( 'edit_user', $this->current_user_id ); - } - - /** - * Returns the meta entry's value as JSON. - * - * @since 3.13.0 - * - * @return string The meta entry's value as JSON. - */ - protected function get_value(): string { - $meta_key = $this->get_meta_key(); - $meta_value = get_user_meta( $this->current_user_id, $meta_key, true ); - - if ( ! is_array( $meta_value ) || 0 === count( $meta_value ) ) { - $meta_value = $this->default_value; - } - - $result = wp_json_encode( $meta_value ); - - return false !== $result ? $result : ''; - } - - /** - * Sets the meta entry's value. - * - * @since 3.13.0 - * - * @param array $meta_value The value to set the meta entry to. - * @return bool Whether updating the meta entry's value was successful. - */ - protected function set_value( array $meta_value ): bool { - $sanitized_value = $this->sanitize_value( $meta_value ); - - $update_meta = update_user_meta( - $this->current_user_id, - $this->get_meta_key(), - $sanitized_value - ); - - if ( false !== $update_meta ) { - return true; - } - - return false; - } - - /** - * Sanitizes the passed meta value. - * - * @since 3.13.0 - * @since 3.14.0 Added support for nested arrays. - * - * @param array $meta_value The meta value to sanitize. - * @param string $parent_key The parent key for the current level of the meta. - * @return array The sanitized meta as an array of subvalues. - */ - protected function sanitize_value( array $meta_value, string $parent_key = '' ): array { - $sanitized_value = array(); - - // Determine the current level's specifications based on the parent key. - /** - * Current level's specifications. - * - * @var array $current_specs - */ - $current_specs = ( '' === $parent_key ) ? $this->get_subvalues_specs() : $this->get_nested_specs( $parent_key ); - - foreach ( $current_specs as $key => $spec ) { - $composite_key = '' === $parent_key ? $key : $parent_key . '.' . $key; - - // Check if the key exists in the input meta value array. - if ( array_key_exists( $key, $meta_value ) ) { - $value = $meta_value[ $key ]; - } else { - // Key is missing in the input, use the default value from the specifications. - $value = $this->get_default( explode( '.', $composite_key ) ); - } - - /** - * Spec for the current key. - * - * @var array{default: mixed, values?: array} $spec - */ - if ( is_array( $value ) && isset( $spec['values'] ) ) { - // Recursively handle nested arrays if 'values' spec exists for this key. - $sanitized_value[ $key ] = $this->sanitize_value( $value, $composite_key ); - } else { - // Directly sanitize non-array values or non-nested specs. - $sanitized_value[ $key ] = $this->sanitize_subvalue( $composite_key, $value ); - } - } - - return $sanitized_value; - } - - /** - * Sanitizes the passed subvalue. - * - * @since 3.14.0 Added support for nested arrays. - * @since 3.13.0 - * - * @param string $composite_key The subvalue's key. - * @param mixed $value The value to sanitize. - * @return mixed The sanitized subvalue. - */ - protected function sanitize_subvalue( string $composite_key, $value ) { - $keys = explode( '.', $composite_key ); - $valid_values = $this->get_valid_values( $keys ); - - if ( is_array( $value ) ) { - // Check if $value elements are inside $valid_values - // If not, the value should be the default value. - $valid_value = array(); - foreach ( $value as $key => $val ) { - if ( in_array( $val, $valid_values, true ) ) { - $valid_value[ $key ] = $val; - } - } - return $valid_value; - } - - if ( is_string( $value ) ) { - $value = sanitize_text_field( $value ); - } - - if ( count( $valid_values ) === 0 ) { - return $value; - } - - if ( ! in_array( $value, $valid_values, true ) ) { - return $this->get_default( $keys ); - } - - return $value; - } - - /** - * Checks if a given composite key is valid. - * - * @since 3.14.3 - * - * @param string|mixed $composite_key The composite key representing the nested path. - * @return bool Whether the key is valid. - */ - protected function is_valid_key( $composite_key ): bool { - if ( ! is_string( $composite_key ) ) { - return false; // Key path is not a string. - } - - $keys = explode( '.', $composite_key ); - $current = $this->valid_subvalues; - - foreach ( $keys as $key ) { - if ( ! is_array( $current ) || ! isset( $current[ $key ] ) ) { - return false; // Key path is invalid. - } - - if ( isset( $current[ $key ]['values'] ) ) { - $current = $current[ $key ]['values']; - } else { - $current = $current[ $key ]; - } - } - - return true; - } - - /** - * Gets the valid values for a given setting path. - * - * @since 3.14.3 - * - * @param array $keys The path to the setting. - * @return array The valid values for the setting path. - */ - protected function get_valid_values( array $keys ): array { - $current = $this->valid_subvalues; - - foreach ( $keys as $key ) { - if ( ! is_array( $current ) || ! isset( $current[ $key ] ) ) { - return array(); // No valid values for invalid key path. - } - if ( isset( $current[ $key ]['values'] ) ) { - $current = $current[ $key ]['values']; - } else { - $current = $current[ $key ]; - } - } - - return is_array( $current ) ? $current : array(); - } - - /** - * Gets the default value for a given setting path. - * - * @since 3.14.3 - * - * @param array $keys The path to the setting. - * @return mixed|array|null The default value for the setting path. - */ - protected function get_default( array $keys ) { - $current = $this->default_value; - - foreach ( $keys as $key ) { - if ( ! is_array( $current ) || ! isset( $current[ $key ] ) ) { - return null; // No default value for invalid key path. - } - if ( isset( $current[ $key ]['default'] ) ) { - $current = $current[ $key ]['default']; - } else { - $current = $current[ $key ]; - } - } - - return $current; // Return default value for valid key path. - } - - - /** - * Gets the specifications for nested settings based on a composite key. - * - * @since 3.14.3 - * - * @param string $composite_key The composite key representing the nested path. - * @return array The specifications for the nested path. - */ - protected function get_nested_specs( string $composite_key ): array { - $keys = explode( '.', $composite_key ); - $specs = $this->get_subvalues_specs(); - - foreach ( $keys as $key ) { - if ( is_array( $specs[ $key ] ) && array_key_exists( 'values', $specs[ $key ] ) ) { - $specs = $specs[ $key ]['values']; - } else { - break; - } - } - - return $specs; - } -} diff --git a/src/Endpoints/user-meta/class-dashboard-widget-settings-endpoint.php b/src/Endpoints/user-meta/class-dashboard-widget-settings-endpoint.php deleted file mode 100644 index 32ae65efdb..0000000000 --- a/src/Endpoints/user-meta/class-dashboard-widget-settings-endpoint.php +++ /dev/null @@ -1,54 +0,0 @@ - - */ - protected function get_subvalues_specs(): array { - return array( - 'Metric' => array( - 'values' => array( 'views', 'avg_engaged' ), - 'default' => 'views', - ), - 'Period' => array( - 'values' => array( '10m', '1h', '2h', '4h', '24h', '7d', '30d' ), - 'default' => '7d', - ), - ); - } -} diff --git a/src/Endpoints/user-meta/class-editor-sidebar-settings-endpoint.php b/src/Endpoints/user-meta/class-editor-sidebar-settings-endpoint.php deleted file mode 100644 index 75de86489d..0000000000 --- a/src/Endpoints/user-meta/class-editor-sidebar-settings-endpoint.php +++ /dev/null @@ -1,102 +0,0 @@ - - */ - protected function get_subvalues_specs(): array { - return array( - 'InitialTabName' => array( - 'values' => array( 'tools', 'performance' ), - 'default' => 'tools', - ), - 'PerformanceStats' => array( - 'values' => array( - 'Period' => array( '10m', '1h', '2h', '4h', '24h', '7d', '30d' ), - 'VisibleDataPoints' => array( 'views', 'visitors', 'avgEngaged', 'recirculation' ), - 'VisiblePanels' => array( 'overview', 'categories', 'referrers' ), - ), - 'default' => array( - 'Period' => '7d', - 'VisibleDataPoints' => array( 'views', 'visitors', 'avgEngaged', 'recirculation' ), - 'VisiblePanels' => array( 'overview', 'categories', 'referrers' ), - ), - ), - 'RelatedPosts' => array( - 'values' => array( - 'FilterBy' => array( 'unavailable', 'tag', 'section', 'author' ), - 'FilterValue' => array(), - 'Metric' => array( 'views', 'avg_engaged' ), - 'Open' => array( true, false ), - 'Period' => array( '10m', '1h', '2h', '4h', '24h', '7d', '30d' ), - ), - 'default' => array( - 'FilterBy' => 'unavailable', - 'FilterValue' => '', - 'Metric' => 'views', - 'Open' => false, - 'Period' => '7d', - ), - ), - 'SmartLinking' => array( - 'values' => array( - 'MaxLinks' => array(), - 'MaxLinkWords' => array(), - 'Open' => array( true, false ), - ), - 'default' => array( - 'MaxLinks' => 10, - 'MaxLinkWords' => 4, - 'Open' => false, - ), - ), - 'TitleSuggestions' => array( - 'values' => array( - 'Open' => array( true, false ), - 'Persona' => array(), - 'Tone' => array(), - ), - 'default' => array( - 'Open' => false, - 'Persona' => 'journalist', - 'Tone' => 'neutral', - ), - ), - ); - } -} diff --git a/src/RemoteAPI/content-suggestions/class-content-suggestions-base-api.php b/src/RemoteAPI/content-suggestions/class-content-suggestions-base-api.php index 3d2a1b050b..ec071681b0 100644 --- a/src/RemoteAPI/content-suggestions/class-content-suggestions-base-api.php +++ b/src/RemoteAPI/content-suggestions/class-content-suggestions-base-api.php @@ -10,7 +10,6 @@ namespace Parsely\RemoteAPI\ContentSuggestions; -use Parsely\Endpoints\Base_Endpoint; use Parsely\Parsely; use Parsely\RemoteAPI\Base_Endpoint_Remote; use UnexpectedValueException; @@ -61,7 +60,7 @@ public function is_available_to_current_user( $request = null ): bool { return current_user_can( // phpcs:ignore WordPress.WP.Capabilities.Undetermined $this->apply_capability_filters( - Base_Endpoint::DEFAULT_ACCESS_CAPABILITY + self::DEFAULT_ACCESS_CAPABILITY ) ); } diff --git a/src/content-helper/common/class-content-helper-feature.php b/src/content-helper/common/class-content-helper-feature.php index b0edb65056..83c6f04b70 100644 --- a/src/content-helper/common/class-content-helper-feature.php +++ b/src/content-helper/common/class-content-helper-feature.php @@ -166,15 +166,17 @@ protected function inject_inline_scripts( $settings = rest_do_request( new WP_REST_Request( 'GET', - '/wp-parsely/v1' . $settings_route + '/wp-parsely/v2/settings/' . $settings_route ) )->get_data(); } - if ( ! is_string( $settings ) ) { - $settings = ''; + if ( ! is_array( $settings ) ) { + $settings = array(); } + $settings = wp_json_encode( $settings ); + wp_add_inline_script( static::get_script_id(), "window.wpParselyContentHelperSettings = '$settings';", diff --git a/src/content-helper/dashboard-widget/class-dashboard-widget.php b/src/content-helper/dashboard-widget/class-dashboard-widget.php index a166adff69..bab7daff5f 100644 --- a/src/content-helper/dashboard-widget/class-dashboard-widget.php +++ b/src/content-helper/dashboard-widget/class-dashboard-widget.php @@ -10,7 +10,6 @@ namespace Parsely\Content_Helper; -use Parsely\Endpoints\User_Meta\Dashboard_Widget_Settings_Endpoint; use Parsely\Parsely; use Parsely\RemoteAPI\Analytics_Posts_API; @@ -136,7 +135,7 @@ public function enqueue_assets(): void { true ); - $this->inject_inline_scripts( Dashboard_Widget_Settings_Endpoint::get_route() ); + $this->inject_inline_scripts( 'dashboard-widget' ); wp_enqueue_style( static::get_style_id(), diff --git a/src/content-helper/editor-sidebar/class-editor-sidebar.php b/src/content-helper/editor-sidebar/class-editor-sidebar.php index 9d0accb25b..d98e7bd19a 100644 --- a/src/content-helper/editor-sidebar/class-editor-sidebar.php +++ b/src/content-helper/editor-sidebar/class-editor-sidebar.php @@ -12,9 +12,7 @@ use Parsely\Content_Helper\Editor_Sidebar\Smart_Linking; use Parsely\Dashboard_Link; -use Parsely\Endpoints\User_Meta\Editor_Sidebar_Settings_Endpoint; use Parsely\Parsely; -use Parsely\Content_Helper\Content_Helper_Feature; use Parsely\Utils\Utils; use WP_Post; @@ -163,7 +161,7 @@ public function run(): void { true ); - $this->inject_inline_scripts( Editor_Sidebar_Settings_Endpoint::get_route() ); + $this->inject_inline_scripts( 'editor-sidebar' ); // Inject inline variables for the editor sidebar, without UTM parameters. $parsely_post_url = $this->get_parsely_post_url( null, false ); diff --git a/src/rest-api/settings/class-endpoint-dashboard-widget-settings.php b/src/rest-api/settings/class-endpoint-dashboard-widget-settings.php index bf7d429fee..00473d8f85 100644 --- a/src/rest-api/settings/class-endpoint-dashboard-widget-settings.php +++ b/src/rest-api/settings/class-endpoint-dashboard-widget-settings.php @@ -10,14 +10,12 @@ namespace Parsely\REST_API\Settings; -use Parsely\Endpoints\User_Meta\Base_Endpoint_User_Meta; - /** * Endpoint for saving and retrieving Content Helper Dashboard Widget settings. * * @since 3.17.0 * - * @phpstan-import-type Subvalue_Spec from Base_Endpoint_User_Meta + * @phpstan-import-type Subvalue_Spec from Base_Settings_Endpoint */ class Endpoint_Dashboard_Widget_Settings extends Base_Settings_Endpoint { /** diff --git a/src/rest-api/settings/class-endpoint-editor-sidebar-settings.php b/src/rest-api/settings/class-endpoint-editor-sidebar-settings.php index ccbd33c825..5f600fa3cf 100644 --- a/src/rest-api/settings/class-endpoint-editor-sidebar-settings.php +++ b/src/rest-api/settings/class-endpoint-editor-sidebar-settings.php @@ -10,14 +10,12 @@ namespace Parsely\REST_API\Settings; -use Parsely\Endpoints\User_Meta\Base_Endpoint_User_Meta; - /** * Endpoint for saving and retrieving Content Helper Editor Sidebar settings. * * @since 3.17.0 * - * @phpstan-import-type Subvalue_Spec from Base_Endpoint_User_Meta + * @phpstan-import-type Subvalue_Spec from Base_Settings_Endpoint */ class Endpoint_Editor_Sidebar_Settings extends Base_Settings_Endpoint { /** diff --git a/tests/Integration/Endpoints/Proxy/BaseProxyEndpointTest.php b/tests/Integration/Endpoints/Proxy/BaseProxyEndpointTest.php deleted file mode 100644 index 208e0abaad..0000000000 --- a/tests/Integration/Endpoints/Proxy/BaseProxyEndpointTest.php +++ /dev/null @@ -1,245 +0,0 @@ -wp_rest_server_global_backup = $GLOBALS['wp_rest_server'] ?? null; - $endpoint = $this->get_endpoint(); - $this->rest_api_init_proxy = static function () use ( $endpoint ) { - $endpoint->run(); - }; - add_action( 'rest_api_init', $this->rest_api_init_proxy ); - } - - /** - * Teardown method called after each test. - * - * Resets globals. - */ - public function tear_down(): void { - parent::tear_down(); - remove_action( 'rest_api_init', $this->rest_api_init_proxy ); - - // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedVariableFound - $GLOBALS['wp_rest_server'] = $this->wp_rest_server_global_backup; - } - - /** - * Verifies that the route is registered. - * - * @param array $methods The methods supported by the route. - */ - public function run_test_register_routes_by_default( - array $methods = array( 'GET' => true ) - ): void { - $routes = rest_get_server()->get_routes(); - self::assertArrayHasKey( self::$route, $routes ); - self::assertCount( 1, $routes[ self::$route ] ); - self::assertSame( $methods, $routes[ self::$route ][0]['methods'] ); - } - - /** - * Verifies that the route is not registered when the respective filter is - * set to false. - */ - public function run_test_do_not_register_route_when_proxy_is_disabled(): void { - // Override some setup steps in order to set the filter to false. - remove_action( 'rest_api_init', $this->rest_api_init_proxy ); - $endpoint = $this->get_endpoint(); - $this->rest_api_init_proxy = static function () use ( $endpoint ) { - add_filter( 'wp_parsely_enable_' . self::$filter_key . '_api_proxy', '__return_false' ); - $endpoint->run(); - }; - add_action( 'rest_api_init', $this->rest_api_init_proxy ); - - $routes = rest_get_server()->get_routes(); - self::assertFalse( array_key_exists( self::$route, $routes ) ); - } - - /** - * Verifies that calls return an error and do not perform a remote call when - * the Site ID is not populated in site options. - * - * @param WP_REST_Request|null $request The request object to be used. - */ - public function run_test_get_items_fails_without_site_id_set( - ?WP_REST_Request $request = null - ): void { - $this->run_test_get_items_fails( - array( 'apikey' => '' ), - 'parsely_site_id_not_set', - 'A Parse.ly Site ID must be set in site options to use this endpoint', - $request - ); - } - - /** - * Verifies that calls return an error and do not perform a remote call when - * the API Secret is not populated in site options. - * - * @param WP_REST_Request|null $request The request object to be used. - */ - public function run_test_get_items_fails_without_api_secret_set( - ?WP_REST_Request $request = null - ): void { - $this->run_test_get_items_fails( - array( - 'apikey' => 'example.com', - 'api_secret' => '', - ), - 'parsely_api_secret_not_set', - 'A Parse.ly API Secret must be set in site options to use this endpoint', - $request - ); - } - - /** - * Verifies that attempting to get items under the given conditions will - * fail. - * - * @param array $options The WordPress options to be set. - * @param string $expected_error_code The expected error code. - * @param string $expected_error_message The expected error message. - * @param WP_REST_Request|null $request The request object to be used. - */ - private function run_test_get_items_fails( - array $options, - string $expected_error_code, - string $expected_error_message, - ?WP_REST_Request $request = null - ): void { - TestCase::set_options( $options ); - if ( null === $request ) { - $request = new WP_REST_Request( 'GET', self::$route ); - } - - $response = rest_get_server()->dispatch( $request ); - /** - * Variable. - * - * @var WP_Error - */ - $error = $response->as_error(); - self::assertSame( 403, $response->get_status() ); - self::assertSame( $expected_error_code, $error->get_error_code() ); - self::assertSame( $expected_error_message, $error->get_error_message() ); - } - - /** - * Verifies default user capability filter. - */ - public function run_test_user_is_allowed_to_make_proxy_api_call_if_default_user_capability_is_changed(): void { - $this->set_current_user_to_contributor(); - add_filter( - 'wp_parsely_user_capability_for_all_private_apis', - function () { - return 'edit_posts'; - } - ); - - self::assertTrue( static::get_endpoint()->is_available_to_current_user() ); - } - - /** - * Verifies endpoint specific user capability filter. - * - * @param string|null $filter_key The key to use for the filter. - */ - public function run_test_user_is_allowed_to_make_proxy_api_call_if_endpoint_specific_user_capability_is_changed( - $filter_key = null - ): void { - $this->set_current_user_to_contributor(); - $filter_key = $filter_key ?? static::$filter_key; - - add_filter( - 'wp_parsely_user_capability_for_' . $filter_key . '_api', - function () { - return 'edit_posts'; - } - ); - - self::assertTrue( static::get_endpoint()->is_available_to_current_user() ); - } -} diff --git a/tests/Integration/Endpoints/RestMetadataTest.php b/tests/Integration/Endpoints/RestMetadataTest.php index 226e1071ca..23854b34cd 100644 --- a/tests/Integration/Endpoints/RestMetadataTest.php +++ b/tests/Integration/Endpoints/RestMetadataTest.php @@ -14,7 +14,6 @@ use Parsely\Endpoints\Rest_Metadata; use Parsely\Tests\Integration\TestCase; - /** * Integration Tests for the REST API Metadata Endpoint. */ diff --git a/tests/Integration/Endpoints/UserMeta/BaseUserMetaEndpointTest.php b/tests/Integration/Endpoints/UserMeta/BaseUserMetaEndpointTest.php deleted file mode 100644 index c2ad8ec403..0000000000 --- a/tests/Integration/Endpoints/UserMeta/BaseUserMetaEndpointTest.php +++ /dev/null @@ -1,169 +0,0 @@ - - */ - protected $default_value = array(); - - /** - * Generates a JSON string for the passed period, metric, and extra data. - * - * @since 3.13.0 - * - * @param string|null $metric The Metric value. - * @param string|null $period The Period value. - * @param array $extra_data Any Extra key/value pairs to add. - * @return string The generated JSON string. - */ - abstract protected function generate_json( - ?string $metric = null, - ?string $period = null, - array $extra_data = array() - ): string; - - /** - * Verifies that the endpoint returns the correct default value. - * - * @since 3.13.0 - */ - public function run_test_endpoint_returns_value_on_get_request(): void { - $this->set_current_user_to_admin(); - - $value = rest_do_request( - new WP_REST_Request( - 'GET', - self::$route - ) - )->get_data(); - - $expected = $this->wp_json_encode( - $this->default_value - ); - - self::assertSame( $expected, $value ); - } - - /** - * Provides data for testing PUT requests. - * - * @since 3.13.0 - * @return iterable - */ - public function provide_put_requests_data(): iterable { - $default_value = $this->generate_json( 'views', '7d' ); - $valid_value = $this->generate_json( 'avg_engaged', '1h' ); - - // Valid non-default value. It should be returned unmodified. - yield 'valid period and metric values' => array( - 'test_data' => $valid_value, - 'expected' => $valid_value, - ); - - // Missing or problematic keys. Defaults should be used for the missing or problematic keys. - yield 'valid period value, no metric value' => array( - 'test_data' => $this->generate_json( null, '1h' ), - 'expected' => $this->generate_json( 'views', '1h' ), - ); - yield 'valid metric value, no period value' => array( - 'test_data' => $this->generate_json( 'avg_engaged' ), - 'expected' => $this->generate_json( 'avg_engaged', '7d' ), - ); - yield 'no values' => array( - 'test_data' => $this->generate_json(), - 'expected' => $default_value, - ); - - // Invalid values. They should be adjusted to their defaults. - yield 'invalid period value' => array( - 'test_data' => $this->generate_json( 'avg_engaged', 'invalid' ), - 'expected' => $this->generate_json( 'avg_engaged', '7d' ), - ); - yield 'invalid metric value' => array( - 'test_data' => $this->generate_json( 'invalid', '1h' ), - 'expected' => $this->generate_json( 'views', '1h' ), - ); - yield 'invalid period and metric values' => array( - 'test_data' => $this->generate_json( 'invalid', 'invalid' ), - 'expected' => $default_value, - ); - - // Invalid extra data passed. Any such data should be discarded. - yield 'invalid additional value' => array( - 'test_data' => $this->generate_json( - 'avg_engaged', - '1h', - array( 'invalid' ) - ), - 'expected' => $valid_value, - ); - yield 'invalid additional key/value pair' => array( - 'test_data' => $this->generate_json( - 'avg_engaged', - '1h', - array( 'invalid_key' => 'invalid_value' ) - ), - 'expected' => $valid_value, - ); - } - - /** - * Sends a PUT request to the endpoint. - * - * @since 3.13.0 - * - * @param string $data The data to be sent in the request. - * @return string The response returned by the endpoint. - */ - protected function send_put_request( string $data ): string { - $this->set_current_user_to_admin(); - $result = $this->send_wp_rest_request( 'PUT', self::$route, $data ); - - if ( ! is_string( $result ) ) { - return ''; - } - - return $result; - } - - /** - * Verifies that the route is not registered when the respective filter is - * set to false. - * - * @since 3.16.0 - */ - public function run_test_do_not_register_route_when_proxy_is_disabled(): void { - // Override some setup steps in order to set the filter to false. - remove_action( 'rest_api_init', $this->rest_api_init_proxy ); - $endpoint = $this->get_endpoint(); - $this->rest_api_init_proxy = static function () use ( $endpoint ) { - add_filter( 'wp_parsely_enable_' . self::$filter_key . '_api_proxy', '__return_false' ); - $endpoint->run(); - }; - add_action( 'rest_api_init', $this->rest_api_init_proxy ); - - $routes = rest_get_server()->get_routes(); - self::assertFalse( array_key_exists( self::$route, $routes ) ); - } -} diff --git a/tests/Integration/Endpoints/UserMeta/DashboardWidgetSettingsEndpointTest.php b/tests/Integration/Endpoints/UserMeta/DashboardWidgetSettingsEndpointTest.php deleted file mode 100644 index 7af0872829..0000000000 --- a/tests/Integration/Endpoints/UserMeta/DashboardWidgetSettingsEndpointTest.php +++ /dev/null @@ -1,204 +0,0 @@ - - */ - protected $default_value = array( - 'Metric' => 'views', - 'Period' => '7d', - ); - - /** - * Initializes all required values for the test. - * - * @since 3.13.0 - */ - public static function initialize(): void { - $route = Dashboard_Widget_Settings_Endpoint::get_route(); - - self::$route = '/wp-parsely/v1' . $route; - self::$filter_key = Utils::convert_endpoint_to_filter_key( $route ); - } - - /** - * Returns the endpoint to be used in tests. - * - * @since 3.13.0 - * - * @return Base_Endpoint_User_Meta The endpoint to be used in tests. - */ - public function get_endpoint(): Base_Endpoint_User_Meta { - return new Dashboard_Widget_Settings_Endpoint( new Parsely() ); - } - - /** - * Verifies that the route is registered. - * - * @since 3.13.0 - * - * @covers \Parsely\Endpoints\User_Meta\Base_Endpoint_User_Meta::get_route - * @covers \Parsely\Endpoints\User_Meta\Base_Endpoint_User_Meta::run - * @uses \Parsely\Endpoints\Base_Endpoint::__construct - * @uses \Parsely\Endpoints\Base_Endpoint::register_endpoint - * @uses \Parsely\Endpoints\User_Meta\Base_Endpoint_User_Meta::__construct - * @uses \Parsely\Endpoints\User_Meta\Base_Endpoint_User_Meta::get_subvalues_specs - * @uses \Parsely\Endpoints\User_Meta\Base_Endpoint_User_Meta::is_available_to_current_user - * @uses \Parsely\Endpoints\User_Meta\Dashboard_Widget_Settings_Endpoint::get_subvalues_specs - * @uses \Parsely\Parsely::__construct - * @uses \Parsely\Parsely::allow_parsely_remote_requests - * @uses \Parsely\Parsely::are_credentials_managed - * @uses \Parsely\Parsely::set_managed_options - * @uses \Parsely\Utils\Utils::convert_endpoint_to_filter_key - */ - public function test_register_routes_by_default(): void { - parent::run_test_register_routes_by_default( - array( - 'GET' => true, - 'PUT' => true, - ) - ); - } - - /** - * Verifies that the route is not registered when the endpoint filter is set - * to false. - * - * @since 3.13.0 - * - * @covers \Parsely\Endpoints\User_Meta\Base_Endpoint_User_Meta::run - * @uses \Parsely\Endpoints\Base_Endpoint::__construct - * @uses \Parsely\Endpoints\Base_Endpoint::register_endpoint - * @uses \Parsely\Endpoints\User_Meta\Base_Endpoint_User_Meta::__construct - * @uses \Parsely\Endpoints\User_Meta\Base_Endpoint_User_Meta::get_route - * @uses \Parsely\Endpoints\User_Meta\Base_Endpoint_User_Meta::get_subvalues_specs - * @uses \Parsely\Endpoints\User_Meta\Dashboard_Widget_Settings_Endpoint::get_subvalues_specs - * @uses \Parsely\Parsely::__construct - * @uses \Parsely\Parsely::allow_parsely_remote_requests - * @uses \Parsely\Parsely::are_credentials_managed - * @uses \Parsely\Parsely::set_managed_options - * @uses \Parsely\Utils\Utils::convert_endpoint_to_filter_key - */ - public function test_verify_that_route_is_not_registered_when_endpoint_is_disabled(): void { - parent::run_test_do_not_register_route_when_proxy_is_disabled(); - } - - /** - * Verifies that the endpoint returns the correct default settings. - * - * @since 3.13.0 - * - * @covers \Parsely\Endpoints\User_Meta\Base_Endpoint_User_Meta::__construct - * @covers \Parsely\Endpoints\User_Meta\Base_Endpoint_User_Meta::get_subvalues_specs - * @covers \Parsely\Endpoints\User_Meta\Base_Endpoint_User_Meta::get_value - * @covers \Parsely\Endpoints\User_Meta\Dashboard_Widget_Settings_Endpoint::get_subvalues_specs - * @covers \Parsely\Endpoints\User_Meta\Dashboard_Widget_Settings_Endpoint::process_request - * @uses \Parsely\Endpoints\Base_Endpoint::__construct - * @uses \Parsely\Endpoints\Base_Endpoint::register_endpoint - * @uses \Parsely\Endpoints\User_Meta\Base_Endpoint_User_Meta::is_available_to_current_user - * @uses \Parsely\Endpoints\User_Meta\Base_Endpoint_User_Meta::run - * @uses \Parsely\Endpoints\User_Meta\Dashboard_Widget_Settings_Endpoint::get_meta_key - * @uses \Parsely\Endpoints\User_Meta\Dashboard_Widget_Settings_Endpoint::get_route - * @uses \Parsely\Parsely::__construct - * @uses \Parsely\Parsely::allow_parsely_remote_requests - * @uses \Parsely\Parsely::are_credentials_managed - * @uses \Parsely\Parsely::set_managed_options - * @uses \Parsely\Utils\Utils::convert_endpoint_to_filter_key - */ - public function test_endpoint_returns_value_on_get_request(): void { - parent::run_test_endpoint_returns_value_on_get_request(); - } - - /** - * Verifies that the endpoint can correctly handle PUT requests. - * - * @since 3.13.0 - * - * @param string $test_data The data to send in the PUT request. - * @param string $expected The expected value of the setting after the PUT request. - * - * @covers \Parsely\Endpoints\User_Meta\Base_Endpoint_User_Meta::get_subvalues_specs - * @covers \Parsely\Endpoints\User_Meta\Base_Endpoint_User_Meta::get_value - * @covers \Parsely\Endpoints\User_Meta\Base_Endpoint_User_Meta::sanitize_subvalue - * @covers \Parsely\Endpoints\User_Meta\Base_Endpoint_User_Meta::sanitize_value - * @covers \Parsely\Endpoints\User_Meta\Base_Endpoint_User_Meta::set_value - * @covers \Parsely\Endpoints\User_Meta\Dashboard_Widget_Settings_Endpoint::get_subvalues_specs - * @covers \Parsely\Endpoints\User_Meta\Dashboard_Widget_Settings_Endpoint::process_request - * @uses \Parsely\Endpoints\Base_Endpoint::__construct - * @uses \Parsely\Endpoints\Base_Endpoint::register_endpoint - * @uses \Parsely\Endpoints\User_Meta\Base_Endpoint_User_Meta::__construct - * @uses \Parsely\Endpoints\User_Meta\Base_Endpoint_User_Meta::is_available_to_current_user - * @uses \Parsely\Endpoints\User_Meta\Base_Endpoint_User_Meta::run - * @uses \Parsely\Endpoints\User_Meta\Dashboard_Widget_Settings_Endpoint::get_meta_key - * @uses \Parsely\Endpoints\User_Meta\Dashboard_Widget_Settings_Endpoint::get_route - * @uses \Parsely\Parsely::__construct - * @uses \Parsely\Parsely::allow_parsely_remote_requests - * @uses \Parsely\Parsely::are_credentials_managed - * @uses \Parsely\Parsely::set_managed_options - * @uses \Parsely\Utils\Utils::convert_endpoint_to_filter_key - * - * @dataProvider provide_put_requests_data - */ - public function test_endpoint_correctly_handles_put_requests( - string $test_data, - string $expected - ): void { - $value = $this->send_put_request( $test_data ); - self::assertSame( $expected, $value ); - } - - /** - * Generates a JSON string for the passed period, metric, and extra data. - * - * @since 3.13.0 - * - * @param string|null $metric The Metric value. - * @param string|null $period The Period value. - * @param array $extra_data Any Extra key/value pairs to add. - * @return string The generated JSON string. - */ - protected function generate_json( - ?string $metric = null, - ?string $period = null, - array $extra_data = array() - ): string { - $array = $this->default_value; - unset( $array['Metric'], $array['Period'] ); - - if ( null !== $metric ) { - $array['Metric'] = $metric; - } - - if ( null !== $period ) { - $array['Period'] = $period; - } - - ksort( $array ); - - return $this->wp_json_encode( array_merge( $array, $extra_data ) ); - } -} diff --git a/tests/Integration/Endpoints/UserMeta/EditorSidebarSettingsEndpointTest.php b/tests/Integration/Endpoints/UserMeta/EditorSidebarSettingsEndpointTest.php deleted file mode 100644 index 1b16f855b7..0000000000 --- a/tests/Integration/Endpoints/UserMeta/EditorSidebarSettingsEndpointTest.php +++ /dev/null @@ -1,308 +0,0 @@ - - */ - protected $default_value = array( - 'InitialTabName' => 'tools', - 'PerformanceStats' => array( - 'Period' => '7d', - 'VisibleDataPoints' => array( 'views', 'visitors', 'avgEngaged', 'recirculation' ), - 'VisiblePanels' => array( 'overview', 'categories', 'referrers' ), - ), - 'RelatedPosts' => array( - 'FilterBy' => 'unavailable', - 'FilterValue' => '', - 'Metric' => 'views', - 'Open' => false, - 'Period' => '7d', - ), - 'SmartLinking' => array( - 'MaxLinks' => 10, - 'MaxLinkWords' => 4, - 'Open' => false, - ), - 'TitleSuggestions' => array( - 'Open' => false, - 'Persona' => 'journalist', - 'Tone' => 'neutral', - ), - ); - - /** - * Initializes all required values for the test. - * - * @since 3.13.0 - */ - public static function initialize(): void { - $route = Editor_Sidebar_Settings_Endpoint::get_route(); - - self::$route = '/wp-parsely/v1' . $route; - self::$filter_key = Utils::convert_endpoint_to_filter_key( $route ); - } - - /** - * Returns the endpoint to be used in tests. - * - * @since 3.13.0 - * - * @return Base_Endpoint_User_Meta The endpoint to be used in tests. - */ - public function get_endpoint(): Base_Endpoint_User_Meta { - return new Editor_Sidebar_Settings_Endpoint( new Parsely() ); - } - - /** - * Verifies that the route is registered. - * - * @since 3.13.0 - * - * @covers \Parsely\Endpoints\User_Meta\Base_Endpoint_User_Meta::get_route - * @covers \Parsely\Endpoints\User_Meta\Base_Endpoint_User_Meta::run - * @uses \Parsely\Endpoints\Base_Endpoint::__construct - * @uses \Parsely\Endpoints\Base_Endpoint::register_endpoint - * @uses \Parsely\Endpoints\User_Meta\Base_Endpoint_User_Meta::__construct - * @uses \Parsely\Endpoints\User_Meta\Base_Endpoint_User_Meta::get_subvalues_specs - * @uses \Parsely\Endpoints\User_Meta\Base_Endpoint_User_Meta::is_available_to_current_user - * @uses \Parsely\Endpoints\User_Meta\Editor_Sidebar_Settings_Endpoint::get_subvalues_specs - * @uses \Parsely\Parsely::__construct - * @uses \Parsely\Parsely::allow_parsely_remote_requests - * @uses \Parsely\Parsely::are_credentials_managed - * @uses \Parsely\Parsely::set_managed_options - * @uses \Parsely\Utils\Utils::convert_endpoint_to_filter_key - */ - public function test_register_routes_by_default(): void { - parent::run_test_register_routes_by_default( - array( - 'GET' => true, - 'PUT' => true, - ) - ); - } - - /** - * Verifies that the route is not registered when the endpoint filter is set - * to false. - * - * @since 3.13.0 - * - * @covers \Parsely\Endpoints\User_Meta\Base_Endpoint_User_Meta::run - * @uses \Parsely\Endpoints\Base_Endpoint::__construct - * @uses \Parsely\Endpoints\Base_Endpoint::register_endpoint - * @uses \Parsely\Endpoints\User_Meta\Base_Endpoint_User_Meta::__construct - * @uses \Parsely\Endpoints\User_Meta\Base_Endpoint_User_Meta::get_route - * @uses \Parsely\Endpoints\User_Meta\Base_Endpoint_User_Meta::get_subvalues_specs - * @uses \Parsely\Endpoints\User_Meta\Editor_Sidebar_Settings_Endpoint::get_subvalues_specs - * @uses \Parsely\Parsely::__construct - * @uses \Parsely\Parsely::allow_parsely_remote_requests - * @uses \Parsely\Parsely::are_credentials_managed - * @uses \Parsely\Parsely::set_managed_options - * @uses \Parsely\Utils\Utils::convert_endpoint_to_filter_key - */ - public function test_verify_that_route_is_not_registered_when_endpoint_is_disabled(): void { - parent::run_test_do_not_register_route_when_proxy_is_disabled(); - } - - /** - * Verifies that the endpoint returns the correct default settings. - * - * @since 3.13.0 - * - * @covers \Parsely\Endpoints\User_Meta\Base_Endpoint_User_Meta::__construct - * @covers \Parsely\Endpoints\User_Meta\Base_Endpoint_User_Meta::get_subvalues_specs - * @covers \Parsely\Endpoints\User_Meta\Base_Endpoint_User_Meta::get_value - * @covers \Parsely\Endpoints\User_Meta\Editor_Sidebar_Settings_Endpoint::get_subvalues_specs - * @covers \Parsely\Endpoints\User_Meta\Editor_Sidebar_Settings_Endpoint::process_request - * @uses \Parsely\Endpoints\Base_Endpoint::__construct - * @uses \Parsely\Endpoints\Base_Endpoint::register_endpoint - * @uses \Parsely\Endpoints\User_Meta\Base_Endpoint_User_Meta::is_available_to_current_user - * @uses \Parsely\Endpoints\User_Meta\Base_Endpoint_User_Meta::run - * @uses \Parsely\Endpoints\User_Meta\Editor_Sidebar_Settings_Endpoint::get_meta_key - * @uses \Parsely\Endpoints\User_Meta\Editor_Sidebar_Settings_Endpoint::get_route - * @uses \Parsely\Parsely::__construct - * @uses \Parsely\Parsely::allow_parsely_remote_requests - * @uses \Parsely\Parsely::are_credentials_managed - * @uses \Parsely\Parsely::set_managed_options - * @uses \Parsely\Utils\Utils::convert_endpoint_to_filter_key - */ - public function test_endpoint_returns_value_on_get_request(): void { - parent::run_test_endpoint_returns_value_on_get_request(); - } - - /** - * Verifies that the endpoint can correctly handle PUT requests. - * - * @since 3.13.0 - * - * @param string $test_data The data to send in the PUT request. - * @param string $expected The expected value of the setting after the PUT request. - * - * @covers \Parsely\Endpoints\User_Meta\Base_Endpoint_User_Meta::get_subvalues_specs - * @covers \Parsely\Endpoints\User_Meta\Base_Endpoint_User_Meta::get_value - * @covers \Parsely\Endpoints\User_Meta\Base_Endpoint_User_Meta::sanitize_subvalue - * @covers \Parsely\Endpoints\User_Meta\Base_Endpoint_User_Meta::sanitize_value - * @covers \Parsely\Endpoints\User_Meta\Base_Endpoint_User_Meta::set_value - * @covers \Parsely\Endpoints\User_Meta\Editor_Sidebar_Settings_Endpoint::get_subvalues_specs - * @covers \Parsely\Endpoints\User_Meta\Editor_Sidebar_Settings_Endpoint::process_request - * @uses \Parsely\Endpoints\Base_Endpoint::__construct - * @uses \Parsely\Endpoints\Base_Endpoint::register_endpoint - * @uses \Parsely\Endpoints\User_Meta\Base_Endpoint_User_Meta::__construct - * @uses \Parsely\Endpoints\User_Meta\Base_Endpoint_User_Meta::get_default - * @uses \Parsely\Endpoints\User_Meta\Base_Endpoint_User_Meta::get_nested_specs - * @uses \Parsely\Endpoints\User_Meta\Base_Endpoint_User_Meta::get_valid_values - * @uses \Parsely\Endpoints\User_Meta\Base_Endpoint_User_Meta::is_available_to_current_user - * @uses \Parsely\Endpoints\User_Meta\Base_Endpoint_User_Meta::run - * @uses \Parsely\Endpoints\User_Meta\Editor_Sidebar_Settings_Endpoint::get_meta_key - * @uses \Parsely\Endpoints\User_Meta\Editor_Sidebar_Settings_Endpoint::get_route - * @uses \Parsely\Parsely::__construct - * @uses \Parsely\Parsely::allow_parsely_remote_requests - * @uses \Parsely\Parsely::are_credentials_managed - * @uses \Parsely\Parsely::set_managed_options - * @uses \Parsely\Utils\Utils::convert_endpoint_to_filter_key - * @dataProvider provide_put_requests_data - */ - public function test_endpoint_correctly_handles_put_requests( - string $test_data, - string $expected - ): void { - $value = $this->send_put_request( $test_data ); - self::assertSame( $expected, $value ); - } - - /** - * Tests that the endpoint can correctly handle PUT requests with valid - * nested PerformanceStats values. - * - * @since 3.14.0 - * - * @covers \Parsely\Endpoints\User_Meta\Base_Endpoint_User_Meta::sanitize_subvalue - * @uses \Parsely\Endpoints\Base_Endpoint::__construct() - * @uses \Parsely\Endpoints\Base_Endpoint::register_endpoint() - * @uses \Parsely\Endpoints\User_Meta\Base_Endpoint_User_Meta::__construct() - * @uses \Parsely\Endpoints\User_Meta\Base_Endpoint_User_Meta::get_route() - * @uses \Parsely\Endpoints\User_Meta\Base_Endpoint_User_Meta::get_value() - * @uses \Parsely\Endpoints\User_Meta\Base_Endpoint_User_Meta::is_available_to_current_user() - * @uses \Parsely\Endpoints\User_Meta\Base_Endpoint_User_Meta::process_request() - * @uses \Parsely\Endpoints\User_Meta\Base_Endpoint_User_Meta::run() - * @uses \Parsely\Endpoints\User_Meta\Base_Endpoint_User_Meta::sanitize_value() - * @uses \Parsely\Endpoints\User_Meta\Base_Endpoint_User_Meta::set_value() - * @uses \Parsely\Endpoints\User_Meta\Editor_Sidebar_Settings_Endpoint::get_meta_key() - * @uses \Parsely\Endpoints\User_Meta\Editor_Sidebar_Settings_Endpoint::get_subvalues_specs() - * @uses \Parsely\Endpoints\User_Meta\Base_Endpoint_User_Meta::get_nested_specs - * @uses \Parsely\Endpoints\User_Meta\Base_Endpoint_User_Meta::get_valid_values - * @uses \Parsely\Parsely::__construct() - * @uses \Parsely\Parsely::allow_parsely_remote_requests() - * @uses \Parsely\Parsely::are_credentials_managed() - * @uses \Parsely\Parsely::set_managed_options() - * @uses \Parsely\Utils\Utils::convert_endpoint_to_filter_key() - */ - public function test_valid_nested_performance_stats_settings_period(): void { - $this->set_current_user_to_admin(); - - $value = $this->send_put_request( - $this->generate_json( - 'views', - '7d', - array( - 'PerformanceStats' => array( - 'Period' => '1h', - 'VisibleDataPoints' => array( 'views', 'avgEngaged', 'recirculation' ), - 'VisiblePanels' => array( 'overview', 'referrers' ), - ), - ) - ) - ); - - $expected = $this->wp_json_encode( - array_merge( - $this->default_value, - array( - 'PerformanceStats' => array( - 'Period' => '1h', - 'VisibleDataPoints' => array( 'views', 'avgEngaged', 'recirculation' ), - 'VisiblePanels' => array( 'overview', 'referrers' ), - ), - ) - ) - ); - - self::assertSame( $expected, $value ); - } - - /** - * Generates a JSON string for the passed period, metric, and extra data. - * - * @since 3.13.0 - * - * @param string|null $metric The RelatedPostsMetric value. - * @param string|null $period The RelatedPostsPeriod value. - * @param array $extra_data Any Extra key/value pairs to add. - * @return string The generated JSON string. - */ - protected function generate_json( - ?string $metric = null, - ?string $period = null, - array $extra_data = array() - ): string { - $array = $this->default_value; - assert( is_array( $array['RelatedPosts'] ) ); - - unset( $array['RelatedPosts']['Metric'], $array['RelatedPosts']['Period'] ); - - if ( null !== $metric ) { - $array['RelatedPosts']['Metric'] = $metric; - } - - if ( null !== $period ) { - $array['RelatedPosts']['Period'] = $period; - } - - $merged_array = array_merge( $array, $extra_data ); - - $this->ksortRecursive( $merged_array, SORT_NATURAL | SORT_FLAG_CASE ); - - return $this->wp_json_encode( $merged_array ); - } - - /** - * Recursively sorts an array by key using a specified sort flag. - * - * @since 3.14.3 - * - * @param array &$unsorted_array The array to be sorted, passed by reference. - * @param int $sort_flags Optional sorting flags. Defaults to SORT_REGULAR. - */ - private function ksortRecursive( array &$unsorted_array, int $sort_flags = SORT_REGULAR ): void { - ksort( $unsorted_array, $sort_flags ); - foreach ( $unsorted_array as &$value ) { - if ( is_array( $value ) ) { - $this->ksortRecursive( $value, $sort_flags ); - } - } - } -} diff --git a/wp-parsely.php b/wp-parsely.php index cf5012bef6..29997ea080 100644 --- a/wp-parsely.php +++ b/wp-parsely.php @@ -30,23 +30,11 @@ use Parsely\Content_Helper\Editor_Sidebar; use Parsely\Content_Helper\Excerpt_Generator; use Parsely\Content_Helper\Post_List_Stats; -use Parsely\Endpoints\Analytics_Post_Detail_API_Proxy; -use Parsely\Endpoints\Analytics_Posts_API_Proxy; use Parsely\Endpoints\GraphQL_Metadata; -use Parsely\Endpoints\Referrers_Post_Detail_API_Proxy; -use Parsely\Endpoints\Related_API_Proxy; use Parsely\Endpoints\Rest_Metadata; -use Parsely\Endpoints\User_Meta\Dashboard_Widget_Settings_Endpoint; -use Parsely\Endpoints\User_Meta\Editor_Sidebar_Settings_Endpoint; use Parsely\Integrations\Amp; use Parsely\Integrations\Google_Web_Stories; use Parsely\Integrations\Integrations; -use Parsely\RemoteAPI\Analytics_Post_Detail_API; -use Parsely\RemoteAPI\Analytics_Posts_API; -use Parsely\RemoteAPI\Referrers_Post_Detail_API; -use Parsely\RemoteAPI\Related_API; -use Parsely\RemoteAPI\Remote_API_Cache; -use Parsely\RemoteAPI\WordPress_Cache; use Parsely\REST_API\REST_API_Controller; use Parsely\UI\Admin_Bar; use Parsely\UI\Admin_Warning; @@ -134,10 +122,6 @@ function parsely_wp_admin_early_register(): void { function parsely_rest_api_init(): void { $rest = new Rest_Metadata( $GLOBALS['parsely'] ); $rest->run(); - - // Content Helper settings endpoints. - ( new Dashboard_Widget_Settings_Endpoint( $GLOBALS['parsely'] ) )->run(); - ( new Editor_Sidebar_Settings_Endpoint( $GLOBALS['parsely'] ) )->run(); } add_action( 'init', __NAMESPACE__ . '\\init_recommendations_block' ); @@ -225,34 +209,3 @@ function parsely_integrations( $parsely = null ): Integrations { return $parsely_integrations; } - -/** - * Instantiates and runs the specified API endpoint. - * - * @since 3.6.0 - * - * @param string $api_class_name The proxy class to instantiate. - * @param string $proxy_api_class_name The API proxy class to instantiate and run. - * @param WordPress_Cache $wp_cache The WordPress cache instance to be used. - */ -function parsely_run_rest_api_endpoint( - string $api_class_name, - string $proxy_api_class_name, - WordPress_Cache &$wp_cache -): void { - /** - * Internal Variable. - * - * @var RemoteAPI\Base_Endpoint_Remote $remote_api - */ - $remote_api = new $api_class_name( $GLOBALS['parsely'] ); - $remote_api_cache = new Remote_API_Cache( $remote_api, $wp_cache ); - - /** - * Internal Variable. - * - * @var Endpoints\Base_API_Proxy $remote_api_proxy - */ - $remote_api_proxy = new $proxy_api_class_name( $GLOBALS['parsely'], $remote_api_cache ); - $remote_api_proxy->run(); -} From f06c4895e8cfa95a94ed9a55f2daebc94711ea6c Mon Sep 17 00:00:00 2001 From: Alex Cicovic <23142906+acicovic@users.noreply.github.com> Date: Thu, 5 Sep 2024 15:53:13 +0300 Subject: [PATCH 25/49] Improve comments and whitespace --- src/rest-api/stats/class-endpoint-post.php | 11 +++---- src/rest-api/stats/class-endpoint-posts.php | 26 ++++++++-------- src/rest-api/stats/class-endpoint-related.php | 4 +-- src/rest-api/stats/trait-post-data.php | 12 ++++---- src/rest-api/stats/trait-related-posts.php | 10 +++---- src/rest-api/trait-use-post-id-parameter.php | 7 +++-- .../RestAPI/Stats/EndpointPostTest.php | 30 ++++++++----------- .../RestAPI/Stats/EndpointPostsTest.php | 20 +++++-------- .../RestAPI/Stats/EndpointRelatedTest.php | 30 +++++++++---------- .../RestAPI/Stats/StatsControllerTest.php | 21 ++++++------- 10 files changed, 84 insertions(+), 87 deletions(-) diff --git a/src/rest-api/stats/class-endpoint-post.php b/src/rest-api/stats/class-endpoint-post.php index c837133150..6864b46db4 100644 --- a/src/rest-api/stats/class-endpoint-post.php +++ b/src/rest-api/stats/class-endpoint-post.php @@ -25,8 +25,8 @@ /** * The Stats API Post endpoint. * - * Provides an endpoint for retrieving post details, referrers, and related posts for - * a given post. + * Provides an endpoint for retrieving post details, referrers, and related + * posts for a given post. * * @since 3.17.0 */ @@ -203,12 +203,12 @@ public function get_post_details( WP_REST_Request $request ) { $post_data[] = $this->extract_post_data( $data ); } - $response = array( + $response_data = array( 'params' => $request->get_params(), 'data' => $post_data, ); - return new WP_REST_Response( $response, 200 ); + return new WP_REST_Response( $response_data, 200 ); } /** @@ -242,7 +242,7 @@ public function get_post_referrers( WP_REST_Request $request ) { $this->total_views = $total_views; - // Do the analytics request. + // Get the data from the API. $analytics_request = $this->referrers_post_detail_api->get_items( array( 'url' => $permalink, @@ -308,6 +308,7 @@ public function get_related_posts( WP_REST_Request $request ) { 'params' => $request->get_params(), 'data' => $related_posts, ); + return new WP_REST_Response( $response_data, 200 ); } diff --git a/src/rest-api/stats/class-endpoint-posts.php b/src/rest-api/stats/class-endpoint-posts.php index cb97310771..3f7c718fc1 100644 --- a/src/rest-api/stats/class-endpoint-posts.php +++ b/src/rest-api/stats/class-endpoint-posts.php @@ -20,7 +20,7 @@ /** * The Stats API Posts endpoint. * - * Provides an endpoint for retrieving the top related posts. + * Provides an endpoint for retrieving posts. * * @since 3.17.0 */ @@ -35,7 +35,7 @@ class Endpoint_Posts extends Base_Endpoint { * * @since 3.17.0 * - * @var string[] + * @var array * @see https://docs.parse.ly/api-available-metrics/ */ public const SORT_METRICS = array( @@ -95,11 +95,13 @@ public function get_endpoint_name(): string { /** * Registers the routes for the objects of the controller. + * + * @since 3.17.0 */ public function register_routes(): void { /** * GET /posts - * Retrieves the top posts for the given period. + * Retrieves posts for the given criteria. */ $this->register_rest_route( '/', @@ -175,7 +177,7 @@ public function register_routes(): void { 'required' => false, ), ), - $this->get_itm_source_param_args() + $this->get_itm_source_param_args() ) ); } @@ -183,11 +185,12 @@ public function register_routes(): void { /** * API Endpoint: GET /stats/posts * - * Retrieves the top posts for the given query parameters. + * Retrieves the posts with the given query parameters. * - * @param WP_REST_Request $request The request. + * @since 3.17.0 * - * @return stdClass[]|WP_Error|WP_REST_Response + * @param WP_REST_Request $request The request. + * @return array|WP_Error|WP_REST_Response */ public function get_posts( WP_REST_Request $request ) { $params = $request->get_params(); @@ -206,11 +209,10 @@ public function get_posts( WP_REST_Request $request ) { } // TODO END. - // Do the analytics request. /** - * The raw analytics data. + * The raw analytics data, received by the API. * - * @var stdClass[]|WP_Error $analytics_request + * @var array|WP_Error $analytics_request */ $analytics_request = $this->analytics_posts_api->get_items( array( @@ -239,11 +241,11 @@ public function get_posts( WP_REST_Request $request ) { $posts[] = $this->extract_post_data( $item ); } - $response = array( + $response_data = array( 'params' => $params, 'data' => $posts, ); - return new WP_REST_Response( $response, 200 ); + return new WP_REST_Response( $response_data, 200 ); } } diff --git a/src/rest-api/stats/class-endpoint-related.php b/src/rest-api/stats/class-endpoint-related.php index d636957b8f..d5de1404f0 100644 --- a/src/rest-api/stats/class-endpoint-related.php +++ b/src/rest-api/stats/class-endpoint-related.php @@ -1,9 +1,9 @@ The extracted data. diff --git a/src/rest-api/stats/trait-related-posts.php b/src/rest-api/stats/trait-related-posts.php index dadab370a3..f7c486f5ca 100644 --- a/src/rest-api/stats/trait-related-posts.php +++ b/src/rest-api/stats/trait-related-posts.php @@ -1,9 +1,9 @@ false, ), ), - $this->get_itm_source_param_args() + $this->get_itm_source_param_args() ); } @@ -122,9 +121,8 @@ public function get_related_posts_of_url( WP_REST_Request $request, string $url // Set the itm_source parameter. $this->set_itm_source_from_request( $request ); - // Get the data from the API. /** - * The related posts request. + * The raw related posts data, received by the API. * * @var array|WP_Error $related_posts_request */ diff --git a/src/rest-api/trait-use-post-id-parameter.php b/src/rest-api/trait-use-post-id-parameter.php index 4459636e90..39047a2f72 100644 --- a/src/rest-api/trait-use-post-id-parameter.php +++ b/src/rest-api/trait-use-post-id-parameter.php @@ -1,9 +1,9 @@ set_param( 'post', $post ); diff --git a/tests/Integration/RestAPI/Stats/EndpointPostTest.php b/tests/Integration/RestAPI/Stats/EndpointPostTest.php index acba94a64b..013e6e2c1f 100644 --- a/tests/Integration/RestAPI/Stats/EndpointPostTest.php +++ b/tests/Integration/RestAPI/Stats/EndpointPostTest.php @@ -1,9 +1,9 @@ get_routes(); - // Check that the excerpt-generator/generate route is registered. + // Check that the route is registered. $expected_route = $this->get_endpoint()->get_full_endpoint( '/' ); self::assertArrayHasKey( $expected_route, $routes ); - // Check that the route is associated with the POST method. + // Check that the route is associated with the GET method. $route_data = $routes[ $expected_route ]; self::assertArrayHasKey( 'GET', $route_data[0]['methods'] ); } /** - * Test that the endpoint is not available if the API key is not set. + * Verifies that the endpoint is not available if the API Secret is not set. * * @covers \Parsely\REST_API\Stats\Endpoint_Posts::is_available_to_current_user * @uses \Parsely\Endpoints\Base_Endpoint::__construct @@ -151,7 +148,6 @@ public function test_access_error_if_api_secret_is_not_set(): void { ); } - /** * Verifies forbidden error when current user doesn't have proper * capabilities. diff --git a/tests/Integration/RestAPI/Stats/EndpointRelatedTest.php b/tests/Integration/RestAPI/Stats/EndpointRelatedTest.php index 463886c852..76b032f364 100644 --- a/tests/Integration/RestAPI/Stats/EndpointRelatedTest.php +++ b/tests/Integration/RestAPI/Stats/EndpointRelatedTest.php @@ -1,9 +1,9 @@ get_routes(); - // Check that the excerpt-generator/generate route is registered. + // Check that the route is registered. $expected_route = $this->get_endpoint()->get_full_endpoint( '/' ); self::assertArrayHasKey( $expected_route, $routes ); - // Check that the route is associated with the POST method. + // Check that the route is associated with the GET method. $route_data = $routes[ $expected_route ]; self::assertArrayHasKey( 'GET', $route_data[0]['methods'] ); } /** - * Test that the endpoint is available to everyone, even if they are not logged in. + * Verifies that the endpoint is available to everyone, even if they are not + * logged in. * * @since 3.17.0 * @@ -160,7 +158,7 @@ public function test_access_of_related_posts_is_available_to_everyone(): void { } /** - * Mock the API response of the Parse.ly API. + * Mocks the API response of the Parse.ly API. * * @since 3.17.0 * @@ -303,10 +301,12 @@ function () use ( &$dispatched ): array { } /** - * Test that the endpoint is not available if the API secret is not set. - * This test should be disabled since the endpoint does not requires the API secret. + * Verifies that the endpoint is not available if the API secret is not set. + * + * This test is disabled since the endpoint does not requires the API secret. * * @since 3.17.0 + * * @coversNothing */ public function test_is_available_to_current_user_returns_error_api_secret_not_set(): void { diff --git a/tests/Integration/RestAPI/Stats/StatsControllerTest.php b/tests/Integration/RestAPI/Stats/StatsControllerTest.php index 0635125f95..2eb6ba287a 100644 --- a/tests/Integration/RestAPI/Stats/StatsControllerTest.php +++ b/tests/Integration/RestAPI/Stats/StatsControllerTest.php @@ -16,11 +16,9 @@ use Parsely\Tests\Integration\TestCase; /** - * Stats API Controller Test + * Stats API Controller Test. * * @since 3.17.0 - * - * @coversDefaultClass \Parsely\REST_API\Stats\Stats_Controller */ class StatsControllerTest extends RestAPIControllerTest { /** @@ -33,19 +31,19 @@ class StatsControllerTest extends RestAPIControllerTest { private $stats_controller = null; /** - * Set up the test controller. + * Setup method called before each test. * * @since 3.17.0 */ - public function setUp(): void { - parent::setUp(); + public function set_up(): void { + parent::set_up(); TestCase::set_options(); - $parsely = self::createMock( Parsely::class ); - $this->stats_controller = new Stats_Controller( $parsely ); + + $this->stats_controller = new Stats_Controller( new Parsely() ); } /** - * Test the constructor sets up the correct namespace and version. + * Verifies that the constructor sets up the correct namespace and version. * * @since 3.17.0 * @@ -55,6 +53,9 @@ public function setUp(): void { * @uses \Parsely\REST_API\REST_API_Controller::get_version */ public function test_constructor_sets_up_namespace_and_version(): void { - self::assertEquals( 'wp-parsely/v2', $this->stats_controller->get_full_namespace() ); + self::assertEquals( + 'wp-parsely/v2', + $this->stats_controller->get_full_namespace() + ); } } From 32a412a57190e93711626a4a16dc2cfa5a1d64ea Mon Sep 17 00:00:00 2001 From: Alex Cicovic <23142906+acicovic@users.noreply.github.com> Date: Thu, 5 Sep 2024 18:02:37 +0300 Subject: [PATCH 26/49] class-base-endpoint.php: Fix incorrect whitespace --- src/Endpoints/class-base-endpoint.php | 38 +++++++++++++-------------- 1 file changed, 19 insertions(+), 19 deletions(-) diff --git a/src/Endpoints/class-base-endpoint.php b/src/Endpoints/class-base-endpoint.php index d9e18812c0..91af0c1075 100644 --- a/src/Endpoints/class-base-endpoint.php +++ b/src/Endpoints/class-base-endpoint.php @@ -91,25 +91,25 @@ public function __construct( Parsely $parsely ) { * @return string The capability allowing access after applying the filters. */ protected function apply_capability_filters( string $capability ): string { - /** - * Filter to change the default user capability for all private endpoints. - * - * @var string - */ - $default_user_capability = apply_filters( - 'wp_parsely_user_capability_for_all_private_apis', - $capability - ); - - /** - * Filter to change the user capability for the specific endpoint. - * - * @var string - */ - $endpoint_specific_user_capability = apply_filters( - 'wp_parsely_user_capability_for_' . Utils::convert_endpoint_to_filter_key( static::ENDPOINT ) . '_api', - $default_user_capability - ); + /** + * Filter to change the default user capability for all private endpoints. + * + * @var string + */ + $default_user_capability = apply_filters( + 'wp_parsely_user_capability_for_all_private_apis', + $capability + ); + + /** + * Filter to change the user capability for the specific endpoint. + * + * @var string + */ + $endpoint_specific_user_capability = apply_filters( + 'wp_parsely_user_capability_for_' . Utils::convert_endpoint_to_filter_key( static::ENDPOINT ) . '_api', + $default_user_capability + ); return $endpoint_specific_user_capability; } From 3f809888809974123139319afa256a7015ecfb60 Mon Sep 17 00:00:00 2001 From: Alex Cicovic <23142906+acicovic@users.noreply.github.com> Date: Thu, 5 Sep 2024 18:24:46 +0300 Subject: [PATCH 27/49] Endpoints/class-base-endpoint.php: Fix incorrect whitespace --- src/Endpoints/class-base-endpoint.php | 38 +++++++++++++-------------- 1 file changed, 19 insertions(+), 19 deletions(-) diff --git a/src/Endpoints/class-base-endpoint.php b/src/Endpoints/class-base-endpoint.php index d9e18812c0..91af0c1075 100644 --- a/src/Endpoints/class-base-endpoint.php +++ b/src/Endpoints/class-base-endpoint.php @@ -91,25 +91,25 @@ public function __construct( Parsely $parsely ) { * @return string The capability allowing access after applying the filters. */ protected function apply_capability_filters( string $capability ): string { - /** - * Filter to change the default user capability for all private endpoints. - * - * @var string - */ - $default_user_capability = apply_filters( - 'wp_parsely_user_capability_for_all_private_apis', - $capability - ); - - /** - * Filter to change the user capability for the specific endpoint. - * - * @var string - */ - $endpoint_specific_user_capability = apply_filters( - 'wp_parsely_user_capability_for_' . Utils::convert_endpoint_to_filter_key( static::ENDPOINT ) . '_api', - $default_user_capability - ); + /** + * Filter to change the default user capability for all private endpoints. + * + * @var string + */ + $default_user_capability = apply_filters( + 'wp_parsely_user_capability_for_all_private_apis', + $capability + ); + + /** + * Filter to change the user capability for the specific endpoint. + * + * @var string + */ + $endpoint_specific_user_capability = apply_filters( + 'wp_parsely_user_capability_for_' . Utils::convert_endpoint_to_filter_key( static::ENDPOINT ) . '_api', + $default_user_capability + ); return $endpoint_specific_user_capability; } From d68ad09f51b3894e5c3bf094800c3fac886bb9fb Mon Sep 17 00:00:00 2001 From: Alex Cicovic <23142906+acicovic@users.noreply.github.com> Date: Fri, 6 Sep 2024 09:22:35 +0300 Subject: [PATCH 28/49] Improve comments and whitespace --- .../settings/class-base-settings-endpoint.php | 4 ++-- tests/Integration/RestAPI/BaseEndpointTest.php | 4 ++-- .../Settings/BaseSettingsEndpointTest.php | 18 ++++++++++-------- .../EndpointDashboardWidgetSettingsTest.php | 5 +++-- .../EndpointEditorSidebarSettingsTest.php | 6 ++---- 5 files changed, 19 insertions(+), 18 deletions(-) diff --git a/src/rest-api/settings/class-base-settings-endpoint.php b/src/rest-api/settings/class-base-settings-endpoint.php index 3a4b59b3b3..e4c47bf4b2 100644 --- a/src/rest-api/settings/class-base-settings-endpoint.php +++ b/src/rest-api/settings/class-base-settings-endpoint.php @@ -83,7 +83,7 @@ abstract protected function get_subvalues_specs(): array; * @since 3.13.0 * @since 3.17.0 Moved from Base_Endpoint_User_Meta. * - * @param Base_API_Controller $controller Parsely instance. + * @param Base_API_Controller $controller The REST API controller. */ public function __construct( Base_API_Controller $controller ) { parent::__construct( $controller ); @@ -201,7 +201,7 @@ public function set_settings( WP_REST_Request $request ) { if ( ! is_array( $meta_value ) ) { // @phpstan-ignore-line return new WP_Error( 'ch_settings_invalid_format', - __( 'Settings must be an valid JSON array', 'wp-parsely' ) + __( 'Settings must be a valid JSON array', 'wp-parsely' ) ); } diff --git a/tests/Integration/RestAPI/BaseEndpointTest.php b/tests/Integration/RestAPI/BaseEndpointTest.php index ae2c3e80f9..46638f1821 100644 --- a/tests/Integration/RestAPI/BaseEndpointTest.php +++ b/tests/Integration/RestAPI/BaseEndpointTest.php @@ -98,7 +98,7 @@ public function __construct( $name = null, array $data = array(), $data_name = ' } /** - * Sets up the test environment. + * Setup method called before each test. * * @since 3.17.0 */ @@ -252,7 +252,7 @@ public function test_route_is_registered(): void { * @uses \Parsely\REST_API\Base_Endpoint::register_rest_route * @uses \Parsely\REST_API\Base_Endpoint::validate_site_id_and_secret * @uses \Parsely\REST_API\Base_Endpoint::apply_capability_filters - * @uses \Parsely\REST_API\Base_Endpoint::get_default_access_capability + * @uses \Parsely\REST_API\Base_Endpoint::get_default_access_capability * @uses \Parsely\REST_API\Content_Helper\Content_Helper_Controller::get_route_prefix * @uses \Parsely\REST_API\Stats\Stats_Controller::get_route_prefix * @uses \Parsely\Utils\Utils::convert_endpoint_to_filter_key diff --git a/tests/Integration/RestAPI/Settings/BaseSettingsEndpointTest.php b/tests/Integration/RestAPI/Settings/BaseSettingsEndpointTest.php index 8d7b18f114..3e96278930 100644 --- a/tests/Integration/RestAPI/Settings/BaseSettingsEndpointTest.php +++ b/tests/Integration/RestAPI/Settings/BaseSettingsEndpointTest.php @@ -1,6 +1,6 @@ get_endpoint()->get_full_endpoint( '/' ); self::assertArrayHasKey( $expected_route, $routes ); - // Check that the route is associated with the POST method. + // Check that the route is associated with the GET and PUT methods. $route_data = $routes[ $expected_route ]; self::assertArrayHasKey( 'GET', $route_data[0]['methods'] ); self::assertArrayHasKey( 'PUT', $route_data[0]['methods'] ); @@ -194,26 +194,28 @@ protected function send_put_request( array $data ): array { } /** - * Test that the endpoint is not available if the API secret is not set. - * This test should be disabled since the endpoint does not requires the API secret. + * Verifies that the endpoint is not available if the API Secret is not set. + * + * This test is disabled since the endpoint does not require an API secret. * * @since 3.17.0 + * * @coversNothing */ public function test_is_available_to_current_user_returns_error_api_secret_not_set(): void { - // This test is disabled since the endpoint does not requires the API secret. self::assertTrue( true ); } /** - * Test that the endpoint is not available if the site ID is not set. - * This test should be disabled since the endpoint does not requires the site ID. + * Verifies that the endpoint is not available if the Site ID is not set. + * + * This test is disabled since the endpoint does not require a site ID. * * @since 3.17.0 + * * @coversNothing */ public function test_is_available_to_current_user_returns_error_site_id_not_set(): void { - // This test is disabled since the endpoint does not requires the site ID. self::assertTrue( true ); } } diff --git a/tests/Integration/RestAPI/Settings/EndpointDashboardWidgetSettingsTest.php b/tests/Integration/RestAPI/Settings/EndpointDashboardWidgetSettingsTest.php index b1ebc2035c..7d2f6ca6b2 100644 --- a/tests/Integration/RestAPI/Settings/EndpointDashboardWidgetSettingsTest.php +++ b/tests/Integration/RestAPI/Settings/EndpointDashboardWidgetSettingsTest.php @@ -1,6 +1,6 @@ Date: Fri, 6 Sep 2024 10:59:21 +0300 Subject: [PATCH 29/49] Improve comments and whitespace --- tests/Integration/RestAPI/BaseAPIControllerTest.php | 1 - tests/Integration/RestAPI/BaseEndpointTest.php | 1 - .../RestAPI/ContentHelper/ContentHelperControllerTest.php | 1 - tests/Integration/RestAPI/RestAPIControllerTest.php | 1 - .../RestAPI/Settings/BaseSettingsEndpointTest.php | 4 ++-- tests/Integration/RestAPI/Stats/EndpointRelatedTest.php | 5 ++--- 6 files changed, 4 insertions(+), 9 deletions(-) diff --git a/tests/Integration/RestAPI/BaseAPIControllerTest.php b/tests/Integration/RestAPI/BaseAPIControllerTest.php index eef6e6d548..665cd0aea1 100644 --- a/tests/Integration/RestAPI/BaseAPIControllerTest.php +++ b/tests/Integration/RestAPI/BaseAPIControllerTest.php @@ -23,7 +23,6 @@ * @covers \Parsely\REST_API\Base_API_Controller */ class BaseAPIControllerTest extends TestCase { - /** * The test controller instance. * diff --git a/tests/Integration/RestAPI/BaseEndpointTest.php b/tests/Integration/RestAPI/BaseEndpointTest.php index 46638f1821..74096561f6 100644 --- a/tests/Integration/RestAPI/BaseEndpointTest.php +++ b/tests/Integration/RestAPI/BaseEndpointTest.php @@ -26,7 +26,6 @@ * @covers \Parsely\REST_API\Base_Endpoint */ class BaseEndpointTest extends TestCase { - /** * The test endpoint instance. * diff --git a/tests/Integration/RestAPI/ContentHelper/ContentHelperControllerTest.php b/tests/Integration/RestAPI/ContentHelper/ContentHelperControllerTest.php index 944dd6b814..c5bc60301b 100644 --- a/tests/Integration/RestAPI/ContentHelper/ContentHelperControllerTest.php +++ b/tests/Integration/RestAPI/ContentHelper/ContentHelperControllerTest.php @@ -26,7 +26,6 @@ * @covers \Parsely\REST_API\Content_Helper\Content_Helper_Controller */ class ContentHelperControllerTest extends RestAPIControllerTest { - /** * The test controller instance. * diff --git a/tests/Integration/RestAPI/RestAPIControllerTest.php b/tests/Integration/RestAPI/RestAPIControllerTest.php index f53c8961bd..833d3600ab 100644 --- a/tests/Integration/RestAPI/RestAPIControllerTest.php +++ b/tests/Integration/RestAPI/RestAPIControllerTest.php @@ -22,7 +22,6 @@ * @covers \Parsely\REST_API\REST_API_Controller */ class RestAPIControllerTest extends TestCase { - /** * The test controller instance. * diff --git a/tests/Integration/RestAPI/Settings/BaseSettingsEndpointTest.php b/tests/Integration/RestAPI/Settings/BaseSettingsEndpointTest.php index 3e96278930..aa5a4d9e4b 100644 --- a/tests/Integration/RestAPI/Settings/BaseSettingsEndpointTest.php +++ b/tests/Integration/RestAPI/Settings/BaseSettingsEndpointTest.php @@ -196,7 +196,7 @@ protected function send_put_request( array $data ): array { /** * Verifies that the endpoint is not available if the API Secret is not set. * - * This test is disabled since the endpoint does not require an API secret. + * This test is disabled since the endpoint does not require an API Secret. * * @since 3.17.0 * @@ -209,7 +209,7 @@ public function test_is_available_to_current_user_returns_error_api_secret_not_s /** * Verifies that the endpoint is not available if the Site ID is not set. * - * This test is disabled since the endpoint does not require a site ID. + * This test is disabled since the endpoint does not require a Site ID. * * @since 3.17.0 * diff --git a/tests/Integration/RestAPI/Stats/EndpointRelatedTest.php b/tests/Integration/RestAPI/Stats/EndpointRelatedTest.php index 76b032f364..4232b82f76 100644 --- a/tests/Integration/RestAPI/Stats/EndpointRelatedTest.php +++ b/tests/Integration/RestAPI/Stats/EndpointRelatedTest.php @@ -301,16 +301,15 @@ function () use ( &$dispatched ): array { } /** - * Verifies that the endpoint is not available if the API secret is not set. + * Verifies that the endpoint is not available if the API Secret is not set. * - * This test is disabled since the endpoint does not requires the API secret. + * This test is disabled since the endpoint does not require an API Secret. * * @since 3.17.0 * * @coversNothing */ public function test_is_available_to_current_user_returns_error_api_secret_not_set(): void { - // This test is disabled since the endpoint does not requires the API secret. self::assertTrue( true ); } } From 3e1efb783fda174383999396aea96e9bcef9dc3e Mon Sep 17 00:00:00 2001 From: Henrique Mouta Date: Wed, 2 Oct 2024 13:59:55 +0100 Subject: [PATCH 30/49] REST API Refactor: Apply fixes/suggestions from the other PRs (#2812) * Apply suggestions from #2731 * Apply suggestions from #2735 * Add dynamic settings endpoint calls --- .../dashboard-widget/class-dashboard-widget.php | 3 ++- .../editor-sidebar/class-editor-sidebar.php | 3 ++- src/rest-api/class-base-api-controller.php | 6 +++--- src/rest-api/class-base-endpoint.php | 12 ++++++------ .../class-content-helper-controller.php | 2 +- .../class-endpoint-excerpt-generator.php | 6 +++--- .../content-helper/class-endpoint-smart-linking.php | 2 +- .../class-endpoint-title-suggestions.php | 8 ++++---- .../class-endpoint-dashboard-widget-settings.php | 2 +- .../class-endpoint-editor-sidebar-settings.php | 2 +- src/rest-api/settings/class-settings-controller.php | 2 +- src/rest-api/stats/class-endpoint-post.php | 2 +- src/rest-api/stats/class-endpoint-posts.php | 2 +- src/rest-api/stats/class-endpoint-related.php | 2 +- src/rest-api/stats/class-stats-controller.php | 2 +- tests/Integration/RestAPI/BaseAPIControllerTest.php | 2 +- tests/Integration/RestAPI/BaseEndpointTest.php | 4 ++-- .../ContentHelper/ContentHelperControllerTest.php | 2 +- 18 files changed, 33 insertions(+), 31 deletions(-) diff --git a/src/content-helper/dashboard-widget/class-dashboard-widget.php b/src/content-helper/dashboard-widget/class-dashboard-widget.php index bab7daff5f..610cabbd10 100644 --- a/src/content-helper/dashboard-widget/class-dashboard-widget.php +++ b/src/content-helper/dashboard-widget/class-dashboard-widget.php @@ -13,6 +13,7 @@ use Parsely\Parsely; use Parsely\RemoteAPI\Analytics_Posts_API; +use Parsely\REST_API\Settings\Endpoint_Dashboard_Widget_Settings; use Parsely\Utils\Utils; use const Parsely\PARSELY_FILE; @@ -135,7 +136,7 @@ public function enqueue_assets(): void { true ); - $this->inject_inline_scripts( 'dashboard-widget' ); + $this->inject_inline_scripts( Endpoint_Dashboard_Widget_Settings::get_endpoint_name() ); wp_enqueue_style( static::get_style_id(), diff --git a/src/content-helper/editor-sidebar/class-editor-sidebar.php b/src/content-helper/editor-sidebar/class-editor-sidebar.php index d98e7bd19a..8f732bde0e 100644 --- a/src/content-helper/editor-sidebar/class-editor-sidebar.php +++ b/src/content-helper/editor-sidebar/class-editor-sidebar.php @@ -14,6 +14,7 @@ use Parsely\Dashboard_Link; use Parsely\Parsely; +use Parsely\REST_API\Settings\Endpoint_Editor_Sidebar_Settings; use Parsely\Utils\Utils; use WP_Post; @@ -161,7 +162,7 @@ public function run(): void { true ); - $this->inject_inline_scripts( 'editor-sidebar' ); + $this->inject_inline_scripts( Endpoint_Editor_Sidebar_Settings::get_endpoint_name() ); // Inject inline variables for the editor sidebar, without UTM parameters. $parsely_post_url = $this->get_parsely_post_url( null, false ); diff --git a/src/rest-api/class-base-api-controller.php b/src/rest-api/class-base-api-controller.php index 7f06c98d4e..9766122449 100644 --- a/src/rest-api/class-base-api-controller.php +++ b/src/rest-api/class-base-api-controller.php @@ -96,7 +96,7 @@ protected function get_version(): string { * * @return string The route prefix. */ - public function get_route_prefix(): string { + public static function get_route_prefix(): string { return ''; } @@ -173,10 +173,10 @@ protected function register_endpoints( array $endpoints ): void { * @return string The prefixed route. */ public function prefix_route( string $route ): string { - if ( '' === $this->get_route_prefix() ) { + if ( '' === static::get_route_prefix() ) { return $route; } - return $this->get_route_prefix() . '/' . $route; + return static::get_route_prefix() . '/' . $route; } } diff --git a/src/rest-api/class-base-endpoint.php b/src/rest-api/class-base-endpoint.php index e489174323..b732a5d805 100644 --- a/src/rest-api/class-base-endpoint.php +++ b/src/rest-api/class-base-endpoint.php @@ -78,7 +78,7 @@ public function init(): void { * @return bool */ $filter_name = 'wp_parsely_api_' . - Utils::convert_endpoint_to_filter_key( $this->get_endpoint_name() ) . + Utils::convert_endpoint_to_filter_key( static::get_endpoint_name() ) . '_endpoint_enabled'; if ( ! apply_filters( $filter_name, true ) ) { // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.DynamicHooknameFound return; @@ -98,7 +98,7 @@ public function init(): void { * * @return string */ - abstract public function get_endpoint_name(): string; + abstract public static function get_endpoint_name(): string; /** * Returns the default access capability for the endpoint. @@ -142,7 +142,7 @@ public function register_rest_route( string $route, array $methods, callable $ca $this->registered_routes[] = $route; // Create the full route for the endpoint. - $route = $this->get_endpoint_name() . '/' . $route; + $route = static::get_endpoint_name() . '/' . $route; // Register the route. register_rest_route( @@ -172,9 +172,9 @@ public function get_full_endpoint( string $route = '' ): string { $route = trim( $route, '/' ); if ( '' !== $route ) { - $route = $this->get_endpoint_name() . '/' . $route; + $route = static::get_endpoint_name() . '/' . $route; } else { - $route = $this->get_endpoint_name(); + $route = static::get_endpoint_name(); } return '/' . @@ -251,7 +251,7 @@ public function apply_capability_filters( string $capability ): string { */ $endpoint_specific_user_capability = apply_filters( 'wp_parsely_user_capability_for_' . - Utils::convert_endpoint_to_filter_key( $this->get_endpoint_name() ) . + Utils::convert_endpoint_to_filter_key( static::get_endpoint_name() ) . '_api', $default_user_capability ); diff --git a/src/rest-api/content-helper/class-content-helper-controller.php b/src/rest-api/content-helper/class-content-helper-controller.php index b93f65fef9..872e47c23d 100644 --- a/src/rest-api/content-helper/class-content-helper-controller.php +++ b/src/rest-api/content-helper/class-content-helper-controller.php @@ -27,7 +27,7 @@ class Content_Helper_Controller extends REST_API_Controller { * * @return string The namespace. */ - public function get_route_prefix(): string { + public static function get_route_prefix(): string { return 'content-helper'; } diff --git a/src/rest-api/content-helper/class-endpoint-excerpt-generator.php b/src/rest-api/content-helper/class-endpoint-excerpt-generator.php index 1adb8d544d..1c267a8195 100644 --- a/src/rest-api/content-helper/class-endpoint-excerpt-generator.php +++ b/src/rest-api/content-helper/class-endpoint-excerpt-generator.php @@ -55,7 +55,7 @@ public function __construct( Content_Helper_Controller $controller ) { * * @return string The endpoint name. */ - public function get_endpoint_name(): string { + public static function get_endpoint_name(): string { return 'excerpt-generator'; } @@ -96,13 +96,13 @@ public function register_routes(): void { 'required' => true, ), 'persona' => array( - 'description' => __( 'The persona of the content.', 'wp-parsely' ), + 'description' => __( 'The persona to use for the suggestion.', 'wp-parsely' ), 'type' => 'string', 'required' => false, 'default' => 'journalist', ), 'style' => array( - 'description' => __( 'The style of the content.', 'wp-parsely' ), + 'description' => __( 'The style to use for the suggestion.', 'wp-parsely' ), 'type' => 'string', 'required' => false, 'default' => 'neutral', diff --git a/src/rest-api/content-helper/class-endpoint-smart-linking.php b/src/rest-api/content-helper/class-endpoint-smart-linking.php index cce1c99c43..79474b24ce 100644 --- a/src/rest-api/content-helper/class-endpoint-smart-linking.php +++ b/src/rest-api/content-helper/class-endpoint-smart-linking.php @@ -59,7 +59,7 @@ public function __construct( Content_Helper_Controller $controller ) { * * @return string The endpoint name. */ - public function get_endpoint_name(): string { + public static function get_endpoint_name(): string { return 'smart-linking'; } diff --git a/src/rest-api/content-helper/class-endpoint-title-suggestions.php b/src/rest-api/content-helper/class-endpoint-title-suggestions.php index df5717bbaf..f71b13d220 100644 --- a/src/rest-api/content-helper/class-endpoint-title-suggestions.php +++ b/src/rest-api/content-helper/class-endpoint-title-suggestions.php @@ -55,7 +55,7 @@ public function __construct( Content_Helper_Controller $controller ) { * * @return string The endpoint name. */ - public function get_endpoint_name(): string { + public static function get_endpoint_name(): string { return 'title-suggestions'; } @@ -91,19 +91,19 @@ public function register_routes(): void { 'type' => 'string', ), 'limit' => array( - 'description' => __( 'The maximum number of titles to generate.', 'wp-parsely' ), + 'description' => __( 'The maximum number of titles to be suggested.', 'wp-parsely' ), 'required' => false, 'type' => 'integer', 'default' => 3, ), 'style' => array( - 'description' => __( 'The style of the titles to generate.', 'wp-parsely' ), + 'description' => __( 'The style of the titles to be suggested.', 'wp-parsely' ), 'required' => false, 'type' => 'string', 'default' => 'neutral', ), 'persona' => array( - 'description' => __( 'The persona of the titles to generate.', 'wp-parsely' ), + 'description' => __( 'The persona of the titles to be suggested.', 'wp-parsely' ), 'required' => false, 'type' => 'string', 'default' => 'journalist', diff --git a/src/rest-api/settings/class-endpoint-dashboard-widget-settings.php b/src/rest-api/settings/class-endpoint-dashboard-widget-settings.php index 00473d8f85..f23707c9b1 100644 --- a/src/rest-api/settings/class-endpoint-dashboard-widget-settings.php +++ b/src/rest-api/settings/class-endpoint-dashboard-widget-settings.php @@ -25,7 +25,7 @@ class Endpoint_Dashboard_Widget_Settings extends Base_Settings_Endpoint { * * @return string */ - public function get_endpoint_name(): string { + public static function get_endpoint_name(): string { return 'dashboard-widget'; } diff --git a/src/rest-api/settings/class-endpoint-editor-sidebar-settings.php b/src/rest-api/settings/class-endpoint-editor-sidebar-settings.php index 5f600fa3cf..850a765197 100644 --- a/src/rest-api/settings/class-endpoint-editor-sidebar-settings.php +++ b/src/rest-api/settings/class-endpoint-editor-sidebar-settings.php @@ -25,7 +25,7 @@ class Endpoint_Editor_Sidebar_Settings extends Base_Settings_Endpoint { * * @return string */ - public function get_endpoint_name(): string { + public static function get_endpoint_name(): string { return 'editor-sidebar'; } diff --git a/src/rest-api/settings/class-settings-controller.php b/src/rest-api/settings/class-settings-controller.php index 30760c938f..930856a66f 100644 --- a/src/rest-api/settings/class-settings-controller.php +++ b/src/rest-api/settings/class-settings-controller.php @@ -27,7 +27,7 @@ class Settings_Controller extends REST_API_Controller { * * @return string The namespace. */ - public function get_route_prefix(): string { + public static function get_route_prefix(): string { return 'settings'; } diff --git a/src/rest-api/stats/class-endpoint-post.php b/src/rest-api/stats/class-endpoint-post.php index 6864b46db4..24fd669f6a 100644 --- a/src/rest-api/stats/class-endpoint-post.php +++ b/src/rest-api/stats/class-endpoint-post.php @@ -83,7 +83,7 @@ public function __construct( Stats_Controller $controller ) { * * @return string The endpoint name. */ - public function get_endpoint_name(): string { + public static function get_endpoint_name(): string { return 'post'; } diff --git a/src/rest-api/stats/class-endpoint-posts.php b/src/rest-api/stats/class-endpoint-posts.php index 3f7c718fc1..8b4c45c899 100644 --- a/src/rest-api/stats/class-endpoint-posts.php +++ b/src/rest-api/stats/class-endpoint-posts.php @@ -89,7 +89,7 @@ public function __construct( Stats_Controller $controller ) { * * @return string */ - public function get_endpoint_name(): string { + public static function get_endpoint_name(): string { return 'posts'; } diff --git a/src/rest-api/stats/class-endpoint-related.php b/src/rest-api/stats/class-endpoint-related.php index d5de1404f0..f896b0546f 100644 --- a/src/rest-api/stats/class-endpoint-related.php +++ b/src/rest-api/stats/class-endpoint-related.php @@ -46,7 +46,7 @@ public function __construct( Stats_Controller $controller ) { * * @return string */ - public function get_endpoint_name(): string { + public static function get_endpoint_name(): string { return 'related'; } diff --git a/src/rest-api/stats/class-stats-controller.php b/src/rest-api/stats/class-stats-controller.php index ecae2387be..c1981984df 100644 --- a/src/rest-api/stats/class-stats-controller.php +++ b/src/rest-api/stats/class-stats-controller.php @@ -27,7 +27,7 @@ class Stats_Controller extends REST_API_Controller { * * @return string The namespace. */ - public function get_route_prefix(): string { + public static function get_route_prefix(): string { return 'stats'; } diff --git a/tests/Integration/RestAPI/BaseAPIControllerTest.php b/tests/Integration/RestAPI/BaseAPIControllerTest.php index 665cd0aea1..23f718ed0e 100644 --- a/tests/Integration/RestAPI/BaseAPIControllerTest.php +++ b/tests/Integration/RestAPI/BaseAPIControllerTest.php @@ -141,7 +141,7 @@ protected function get_namespace(): string { * * @return string The version. */ - public function get_route_prefix(): string { + public static function get_route_prefix(): string { return 'prefix'; } }; diff --git a/tests/Integration/RestAPI/BaseEndpointTest.php b/tests/Integration/RestAPI/BaseEndpointTest.php index 74096561f6..7e0ff45137 100644 --- a/tests/Integration/RestAPI/BaseEndpointTest.php +++ b/tests/Integration/RestAPI/BaseEndpointTest.php @@ -118,7 +118,7 @@ public function set_up(): void { * * @return string */ - public function get_endpoint_name(): string { + public static function get_endpoint_name(): string { return 'test'; } @@ -258,7 +258,7 @@ public function test_route_is_registered(): void { */ public function test_endpoint_is_registered_based_on_filter(): void { $filter_name = 'wp_parsely_api_' . - \Parsely\Utils\Utils::convert_endpoint_to_filter_key( $this->get_endpoint()->get_endpoint_name() ) . + \Parsely\Utils\Utils::convert_endpoint_to_filter_key( $this->get_endpoint()::get_endpoint_name() ) . '_endpoint_enabled'; // Test when the filter allows the endpoint to be enabled. diff --git a/tests/Integration/RestAPI/ContentHelper/ContentHelperControllerTest.php b/tests/Integration/RestAPI/ContentHelper/ContentHelperControllerTest.php index c5bc60301b..75a939feed 100644 --- a/tests/Integration/RestAPI/ContentHelper/ContentHelperControllerTest.php +++ b/tests/Integration/RestAPI/ContentHelper/ContentHelperControllerTest.php @@ -69,7 +69,7 @@ public function test_constructor_sets_up_namespace_and_version(): void { * @covers \Parsely\REST_API\Content_Helper\Content_Helper_Controller::ROUTE_PREFIX */ public function test_route_prefix(): void { - self::assertEquals( 'content-helper', $this->content_helper_controller->get_route_prefix() ); + self::assertEquals( 'content-helper', $this->content_helper_controller::get_route_prefix() ); } /** From 81d773d6f32dcfb84fffcbcdc1f7b12136ab4e59 Mon Sep 17 00:00:00 2001 From: Henrique Mouta Date: Thu, 10 Oct 2024 16:18:48 +0100 Subject: [PATCH 31/49] Implement the new Content API service, and remove old RemoteAPI code --- .phpcs.xml.dist | 5 +- .../class-analytics-post-detail-api.php | 45 ---- src/RemoteAPI/class-analytics-posts-api.php | 107 ---------- .../class-referrers-post-detail-api.php | 45 ---- src/RemoteAPI/class-related-api.php | 39 ---- src/RemoteAPI/class-validate-api.php | 124 ----------- src/UI/class-settings-page.php | 2 +- src/class-parsely.php | 59 +++++ src/class-validator.php | 15 +- .../class-dashboard-widget.php | 7 +- .../post-list-stats/class-post-list-stats.php | 51 ++--- src/rest-api/class-base-api-controller.php | 26 ++- src/rest-api/class-base-endpoint.php | 16 ++ src/rest-api/class-rest-api-controller.php | 51 +++++ src/rest-api/stats/class-endpoint-post.php | 51 ++--- src/rest-api/stats/class-endpoint-posts.php | 83 ++++--- src/rest-api/stats/class-endpoint-related.php | 13 +- src/rest-api/stats/trait-post-data.php | 36 ++-- src/rest-api/stats/trait-related-posts.php | 24 +-- src/services/class-base-api-service.php | 54 +++++ src/services/class-base-service-endpoint.php | 200 +++++++++++++++++ .../content-api/class-content-api-service.php | 202 ++++++++++++++++++ .../class-content-api-base-endpoint.php | 83 +++++++ .../class-endpoint-analytics-post-details.php | 44 ++++ .../class-endpoint-analytics-posts.php | 150 +++++++++++++ .../class-endpoint-referrers-post-detail.php | 44 ++++ .../endpoints/class-endpoint-related.php | 43 ++++ .../endpoints/class-endpoint-validate.php | 100 +++++++++ .../trait-cached-service-endpoint.php | 22 ++ wp-parsely.php | 6 +- 30 files changed, 1241 insertions(+), 506 deletions(-) delete mode 100644 src/RemoteAPI/class-analytics-post-detail-api.php delete mode 100644 src/RemoteAPI/class-analytics-posts-api.php delete mode 100644 src/RemoteAPI/class-referrers-post-detail-api.php delete mode 100644 src/RemoteAPI/class-related-api.php delete mode 100644 src/RemoteAPI/class-validate-api.php create mode 100644 src/services/class-base-api-service.php create mode 100644 src/services/class-base-service-endpoint.php create mode 100644 src/services/content-api/class-content-api-service.php create mode 100644 src/services/content-api/endpoints/class-content-api-base-endpoint.php create mode 100644 src/services/content-api/endpoints/class-endpoint-analytics-post-details.php create mode 100644 src/services/content-api/endpoints/class-endpoint-analytics-posts.php create mode 100644 src/services/content-api/endpoints/class-endpoint-referrers-post-detail.php create mode 100644 src/services/content-api/endpoints/class-endpoint-related.php create mode 100644 src/services/content-api/endpoints/class-endpoint-validate.php create mode 100644 src/services/trait-cached-service-endpoint.php diff --git a/.phpcs.xml.dist b/.phpcs.xml.dist index f863936f2a..5d6bf6fa71 100644 --- a/.phpcs.xml.dist +++ b/.phpcs.xml.dist @@ -79,9 +79,10 @@ - + - /tests/ + + tests/ diff --git a/src/RemoteAPI/class-analytics-post-detail-api.php b/src/RemoteAPI/class-analytics-post-detail-api.php deleted file mode 100644 index 8d946f5211..0000000000 --- a/src/RemoteAPI/class-analytics-post-detail-api.php +++ /dev/null @@ -1,45 +0,0 @@ -apply_capability_filters( - Base_Endpoint::DEFAULT_ACCESS_CAPABILITY - ) - ); - } -} diff --git a/src/RemoteAPI/class-analytics-posts-api.php b/src/RemoteAPI/class-analytics-posts-api.php deleted file mode 100644 index 9b70421ccb..0000000000 --- a/src/RemoteAPI/class-analytics-posts-api.php +++ /dev/null @@ -1,107 +0,0 @@ -, - * } - * - * @phpstan-type Analytics_Post array{ - * title?: string, - * url?: string, - * link?: string, - * author?: string, - * authors?: string[], - * section?: string, - * tags?: string[], - * metrics?: Analytics_Post_Metrics, - * full_content_word_count?: int, - * image_url?: string, - * metadata?: string, - * pub_date?: string, - * thumb_url_medium?: string, - * } - * - * @phpstan-type Analytics_Post_Metrics array{ - * avg_engaged?: float, - * views?: int, - * visitors?: int, - * } - */ -class Analytics_Posts_API extends Base_Endpoint_Remote { - public const MAX_RECORDS_LIMIT = 2000; - public const ANALYTICS_API_DAYS_LIMIT = 7; - - protected const API_BASE_URL = Parsely::PUBLIC_API_BASE_URL; - protected const ENDPOINT = '/analytics/posts'; - protected const QUERY_FILTER = 'wp_parsely_analytics_posts_endpoint_args'; - - /** - * Returns whether the endpoint is available for access by the current - * user. - * - * @since 3.14.0 - * @since 3.16.0 Added the `$request` parameter. - * - * @param WP_REST_Request|null $request The request object. - * @return bool - */ - public function is_available_to_current_user( $request = null ): bool { - return current_user_can( - // phpcs:ignore WordPress.WP.Capabilities.Undetermined - $this->apply_capability_filters( - Base_Endpoint::DEFAULT_ACCESS_CAPABILITY - ) - ); - } - - /** - * Calls Parse.ly Analytics API to get posts info. - * - * Main purpose of this function is to enforce typing. - * - * @param Analytics_Post_API_Params $api_params Parameters of the API. - * @return Analytics_Post[]|WP_Error|null - */ - public function get_posts_analytics( $api_params ) { - return $this->get_items( $api_params, true ); // @phpstan-ignore-line - } - - /** - * Returns the request's options for the remote API call. - * - * @since 3.9.0 - * - * @return array The array of options. - */ - public function get_request_options(): array { - return array( - 'timeout' => 30, // phpcs:ignore WordPressVIPMinimum.Performance.RemoteRequestTimeout.timeout_timeout - ); - } -} diff --git a/src/RemoteAPI/class-referrers-post-detail-api.php b/src/RemoteAPI/class-referrers-post-detail-api.php deleted file mode 100644 index 45233630e8..0000000000 --- a/src/RemoteAPI/class-referrers-post-detail-api.php +++ /dev/null @@ -1,45 +0,0 @@ -apply_capability_filters( - Base_Endpoint::DEFAULT_ACCESS_CAPABILITY - ) - ); - } -} diff --git a/src/RemoteAPI/class-related-api.php b/src/RemoteAPI/class-related-api.php deleted file mode 100644 index ea130bf025..0000000000 --- a/src/RemoteAPI/class-related-api.php +++ /dev/null @@ -1,39 +0,0 @@ -apply_capability_filters( - Base_Endpoint::DEFAULT_ACCESS_CAPABILITY - ) - ); - } - - /** - * Gets the URL for the Parse.ly API credentials validation endpoint. - * - * @since 3.11.0 - * - * @param array $query The query arguments to send to the remote API. - * @return string - */ - public function get_api_url( array $query ): string { - $query = array( - 'apikey' => $query['apikey'], - 'secret' => $query['secret'], - ); - - return add_query_arg( $query, static::API_BASE_URL . static::ENDPOINT ); - } - - /** - * Queries the Parse.ly API credentials validation endpoint. - * The API will return a 200 response if the credentials are valid and a 401 response if they are not. - * - * @param array $query The query arguments to send to the remote API. - * @return object|WP_Error The response from the remote API, or a WP_Error object if the response is an error. - */ - private function api_validate_credentials( array $query ) { - /** - * GET request options. - * - * @var WP_HTTP_Request_Args $options - */ - $options = $this->get_request_options(); - $response = wp_safe_remote_get( $this->get_api_url( $query ), $options ); - - if ( is_wp_error( $response ) ) { - return $response; - } - - $body = wp_remote_retrieve_body( $response ); - $decoded = json_decode( $body ); - - if ( ! is_object( $decoded ) ) { - return new WP_Error( - 400, - __( - 'Unable to decode upstream API response', - 'wp-parsely' - ) - ); - } - - if ( ! property_exists( $decoded, 'success' ) || false === $decoded->success ) { - return new WP_Error( - $decoded->code ?? 400, - $decoded->message ?? __( 'Unable to read data from upstream API', 'wp-parsely' ) - ); - } - - return $decoded; - } - - /** - * Returns the response from the Parse.ly API credentials validation endpoint. - * - * @since 3.11.0 - * - * @param array $query The query arguments to send to the remote API. - * @param bool $associative (optional) When TRUE, returned objects will be converted into - * associative arrays. - * @return array|object|WP_Error - */ - public function get_items( array $query, bool $associative = false ) { - $api_request = $this->api_validate_credentials( $query ); - return $associative ? Utils::convert_to_associative_array( $api_request ) : $api_request; - } -} diff --git a/src/UI/class-settings-page.php b/src/UI/class-settings-page.php index 3e5563bc03..993f84ff66 100644 --- a/src/UI/class-settings-page.php +++ b/src/UI/class-settings-page.php @@ -1198,7 +1198,7 @@ public function validate_options( $input ) { * @param ParselySettingOptions $input Options from the settings page. * @return ParselySettingOptions Validated inputs. */ - private function validate_basic_section( $input ) { + private function validate_basic_section( $input ): array { $are_credentials_managed = $this->parsely->are_credentials_managed; $options = $this->parsely->get_options(); diff --git a/src/class-parsely.php b/src/class-parsely.php index e7c673fedd..55ce5c5c02 100644 --- a/src/class-parsely.php +++ b/src/class-parsely.php @@ -10,6 +10,8 @@ namespace Parsely; +use Parsely\REST_API\REST_API_Controller; +use Parsely\Services\ContentAPI\Content_API_Service; use Parsely\UI\Metadata_Renderer; use Parsely\UI\Settings_Page; use Parsely\Utils\Utils; @@ -80,6 +82,20 @@ class Parsely { public const PUBLIC_API_BASE_URL = 'https://api.parsely.com/v2'; public const PUBLIC_SUGGESTIONS_API_BASE_URL = 'https://content-suggestions-api.parsely.net/prod'; + /** + * The Content API service. + * + * @var Content_API_Service $content_api_service + */ + private $content_api_service; + + /** + * The Parse.ly internal REST API controller. + * + * @var REST_API_Controller|null $rest_api_controller + */ + private $rest_api_controller; + /** * Declare some class properties * @@ -206,6 +222,7 @@ public function __construct() { $this->are_credentials_managed = $this->are_credentials_managed(); $this->set_managed_options(); + $this->init_external_services(); $this->allow_parsely_remote_requests(); } @@ -237,6 +254,48 @@ public function run(): void { add_action( 'save_post', array( $this, 'update_metadata_endpoint' ) ); } + /** + * Initializes external services. + * + * This method initializes all the relevant external services for the plugin, such as the + * Content API service, and the Suggestions API service. + * + * @since 3.17.0 + */ + private function init_external_services(): void { + $this->content_api_service = new Content_API_Service( $this ); + } + + /** + * Returns the Content API service. + * + * This method returns the Content API service, which is used to interact with the Parse.ly Content API. + * + * @since 3.17.0 + * + * @return Content_API_Service + */ + public function get_content_api(): Content_API_Service { + return $this->content_api_service; + } + + /** + * Gets the REST API controller. + * + * If the controller is not set, a new instance is created. + * + * @since 3.17.0 + * + * @return REST_API_Controller + */ + public function get_rest_api_controller(): REST_API_Controller { + if ( ! isset( $this->rest_api_controller ) ) { + $this->rest_api_controller = new REST_API_Controller( $this ); + } + + return $this->rest_api_controller; + } + /** * Gets the full URL of the JavaScript tracker file for the site. If an API * key is not set, return an empty string. diff --git a/src/class-validator.php b/src/class-validator.php index 26c915c857..5174dce193 100644 --- a/src/class-validator.php +++ b/src/class-validator.php @@ -44,7 +44,7 @@ public static function validate_metadata_secret( string $metadata_secret ): bool * @param Parsely $parsely The Parsely instance. * @param string $site_id The Site ID to be validated. * @param string $api_secret The API Secret to be validated. - * @return true|WP_Error True if the API Credentials are valid, WP_Error otherwise. + * @return bool|WP_Error True if the API Credentials are valid, WP_Error otherwise. */ public static function validate_api_credentials( Parsely $parsely, string $site_id, string $api_secret ) { // If the API secret is empty, the validation endpoint will always fail. @@ -54,21 +54,16 @@ public static function validate_api_credentials( Parsely $parsely, string $site_ return true; } - $query_args = array( - 'apikey' => $site_id, - 'secret' => $api_secret, - ); + $content_api = $parsely->get_content_api(); + $is_valid = $content_api->validate_credentials( $site_id, $api_secret ); - $validate_api = new RemoteAPI\Validate_API( $parsely ); - $request = $validate_api->get_items( $query_args ); - - if ( is_wp_error( $request ) ) { + if ( is_wp_error( $is_valid ) ) { return new WP_Error( self::INVALID_API_CREDENTIALS, __( 'Invalid API Credentials', 'wp-parsely' ) ); } - return true; + return $is_valid; } } diff --git a/src/content-helper/dashboard-widget/class-dashboard-widget.php b/src/content-helper/dashboard-widget/class-dashboard-widget.php index bab7daff5f..0df02878f4 100644 --- a/src/content-helper/dashboard-widget/class-dashboard-widget.php +++ b/src/content-helper/dashboard-widget/class-dashboard-widget.php @@ -11,8 +11,6 @@ namespace Parsely\Content_Helper; use Parsely\Parsely; -use Parsely\RemoteAPI\Analytics_Posts_API; - use Parsely\Utils\Utils; use const Parsely\PARSELY_FILE; @@ -89,11 +87,12 @@ public function run(): void { */ public function can_enable_widget(): bool { $screen = get_current_screen(); - $posts_api = new Analytics_Posts_API( $GLOBALS['parsely'] ); return $this->can_enable_feature( null !== $screen && 'dashboard' === $screen->id, - $posts_api->is_available_to_current_user() + $this->parsely->get_rest_api_controller()->is_available_to_current_user( + '/stats/posts' + ) ); } diff --git a/src/content-helper/post-list-stats/class-post-list-stats.php b/src/content-helper/post-list-stats/class-post-list-stats.php index 42b2452e82..91b5f2432e 100644 --- a/src/content-helper/post-list-stats/class-post-list-stats.php +++ b/src/content-helper/post-list-stats/class-post-list-stats.php @@ -11,10 +11,10 @@ namespace Parsely\Content_Helper; use DateTime; -use Parsely\Content_Helper\Content_Helper_Feature; use Parsely\Parsely; use Parsely\RemoteAPI\Base_Endpoint_Remote; -use Parsely\RemoteAPI\Analytics_Posts_API; +use Parsely\Services\ContentAPI\Content_API_Service; +use Parsely\Services\ContentAPI\Endpoints\Endpoint_Analytics_Posts; use Parsely\Utils\Utils; use WP_Screen; @@ -26,8 +26,8 @@ * @since 3.7.0 * @since 3.9.0 Renamed FQCN from `Parsely\UI\Admin_Columns_Parsely_Stats` to `Parsely\Content_Helper\Post_List_Stats`. * - * @phpstan-import-type Analytics_Post_API_Params from Analytics_Posts_API - * @phpstan-import-type Analytics_Post from Analytics_Posts_API + * @phpstan-import-type Analytics_Posts_API_Params from Endpoint_Analytics_Posts + * @phpstan-import-type Analytics_Post from Endpoint_Analytics_Posts * @phpstan-import-type Remote_API_Error from Base_Endpoint_Remote * * @phpstan-type Parsely_Post_Stats array{ @@ -42,12 +42,13 @@ * } */ class Post_List_Stats extends Content_Helper_Feature { + /** - * Instance of Parsely Analytics Posts API. + * Instance of Content API Service. * - * @var Analytics_Posts_API + * @var Content_API_Service */ - private $analytics_api; + private $content_api; /** * Internal Variable. @@ -73,6 +74,7 @@ class Post_List_Stats extends Content_Helper_Feature { */ public function __construct( Parsely $parsely ) { $this->parsely = $parsely; + $this->content_api = $parsely->get_content_api(); } /** @@ -114,12 +116,10 @@ public static function get_style_id(): string { * @since 3.7.0 */ public function run(): void { - $this->analytics_api = new Analytics_Posts_API( $this->parsely ); - if ( ! $this->can_enable_feature( $this->parsely->site_id_is_set(), $this->parsely->api_secret_is_set(), - $this->analytics_api->is_available_to_current_user() + $this->parsely->get_rest_api_controller()->is_available_to_current_user( '/stats/posts' ) ) ) { return; } @@ -225,7 +225,7 @@ public function enqueue_parsely_stats_script_with_data(): void { return; // Avoid calling the API if column is hidden. } - $parsely_stats_response = $this->get_parsely_stats_response( $this->analytics_api ); + $parsely_stats_response = $this->get_parsely_stats_response(); if ( null === $parsely_stats_response ) { return; @@ -267,10 +267,9 @@ public function is_parsely_stats_column_hidden(): bool { * * @since 3.7.0 * - * @param Analytics_Posts_API $analytics_api Instance of Analytics_Posts_API. * @return Parsely_Posts_Stats_Response|null */ - public function get_parsely_stats_response( $analytics_api ) { + public function get_parsely_stats_response(): ?array { if ( ! $this->is_tracked_as_post_type() ) { return null; } @@ -296,12 +295,12 @@ public function get_parsely_stats_response( $analytics_api ) { return null; } - $response = $analytics_api->get_posts_analytics( + $response = $this->content_api->get_posts( array( - 'period_start' => Analytics_Posts_API::ANALYTICS_API_DAYS_LIMIT . 'd', + 'period_start' => 'max_days', 'pub_date_start' => $date_params['pub_date_start'] ?? '', 'pub_date_end' => $date_params['pub_date_end'] ?? '', - 'limit' => Analytics_Posts_API::MAX_RECORDS_LIMIT, + 'limit' => 'max', 'sort' => 'avg_engaged', // Note: API sends different stats on different sort options. ) ); @@ -322,20 +321,14 @@ public function get_parsely_stats_response( $analytics_api ) { ); } - if ( null === $response ) { - return array( - 'data' => array(), - 'error' => null, - ); - } - /** - * Variable. - * - * @var array + * @var array $parsely_stats_map */ $parsely_stats_map = array(); + /** + * @var Analytics_Post $post_analytics + */ foreach ( $response as $post_analytics ) { $key = $this->get_unique_stats_key_from_analytics( $post_analytics ); @@ -349,9 +342,7 @@ public function get_parsely_stats_response( $analytics_api ) { $engaged_seconds = isset( $metrics['avg_engaged'] ) ? round( $metrics['avg_engaged'] * 60, 2 ) : 0; /** - * Variable. - * - * @var Parsely_Post_Stats + * @var Parsely_Post_Stats $stats */ $stats = array( 'page_views' => Utils::get_formatted_number( (string) $views ) . ' ' . _n( 'page view', 'page views', $views, 'wp-parsely' ), @@ -375,7 +366,7 @@ public function get_parsely_stats_response( $analytics_api ) { * * @since 3.7.0 * - * @return Analytics_Post_API_Params|null + * @return Analytics_Posts_API_Params|null */ private function get_publish_date_params_for_analytics_api() { $published_times = $this->utc_published_times; diff --git a/src/rest-api/class-base-api-controller.php b/src/rest-api/class-base-api-controller.php index 7f06c98d4e..0439757b7a 100644 --- a/src/rest-api/class-base-api-controller.php +++ b/src/rest-api/class-base-api-controller.php @@ -26,7 +26,7 @@ abstract class Base_API_Controller { * * @since 3.17.0 * - * @var Base_Endpoint[] + * @var array */ private $endpoints; @@ -147,7 +147,7 @@ public function get_endpoints(): array { * @param Base_Endpoint $endpoint The endpoint to register. */ protected function register_endpoint( Base_Endpoint $endpoint ): void { - $this->endpoints[] = $endpoint; + $this->endpoints[ $endpoint->get_endpoint_slug() ] = $endpoint; $endpoint->init(); } @@ -179,4 +179,26 @@ public function prefix_route( string $route ): string { return $this->get_route_prefix() . '/' . $route; } + + /** + * Returns a specific endpoint by name. + * + * @since 3.17.0 + * + * @param string $endpoint + * @return Base_Endpoint|null + */ + protected function get_endpoint( string $endpoint ): ?Base_Endpoint { + return $this->endpoints[ $endpoint ] ?? null; + } + + /** + * Checks if a specific endpoint is available to the current user. + * + * @since 3.17.0 + * + * @param string $endpoint The endpoint to check. + * @return bool True if the controller is available to the current user, false otherwise. + */ + public abstract function is_available_to_current_user( string $endpoint ): bool; } diff --git a/src/rest-api/class-base-endpoint.php b/src/rest-api/class-base-endpoint.php index e489174323..b9fb4ad60f 100644 --- a/src/rest-api/class-base-endpoint.php +++ b/src/rest-api/class-base-endpoint.php @@ -183,6 +183,22 @@ public function get_full_endpoint( string $route = '' ): string { $this->api_controller->prefix_route( $route ); } + /** + * Returns the endpoint slug. + * + * The slug is the endpoint name prefixed with the route prefix, from + * the API controller. + * + * Used as an identifier for the endpoint, when registering routes. + * + * @since 3.17.0 + * + * @return string + */ + public function get_endpoint_slug(): string { + return $this->api_controller->prefix_route( '' ) . $this->get_endpoint_name(); + } + /** * Returns the registered routes. * diff --git a/src/rest-api/class-rest-api-controller.php b/src/rest-api/class-rest-api-controller.php index 007c49e54e..de83801c8d 100644 --- a/src/rest-api/class-rest-api-controller.php +++ b/src/rest-api/class-rest-api-controller.php @@ -73,4 +73,55 @@ public function init(): void { $this->controllers = $controllers; } + + /** + * Determines if the specified endpoint is available to the current user. + * + * @since 3.17.0 + * + * @param string $endpoint The endpoint to check. + * @return bool True if the endpoint is available to the current user, false otherwise. + */ + public function is_available_to_current_user( string $endpoint ): bool { + // Remove any forward or trailing slashes. + $endpoint = trim( $endpoint, '/' ); + + // Get the controller for the endpoint. + $controller = $this->get_controller_for_endpoint( $endpoint ); + if ( null === $controller ) { + return false; + } + + // Get the endpoint object. + $endpoint_obj = $controller->get_endpoint( $endpoint ); + if ( null === $endpoint_obj ) { + return false; + } + + // Check if the endpoint is available to the current user. + $is_available = $endpoint_obj->is_available_to_current_user(); + if ( is_wp_error( $is_available ) ) { + return false; + } + + return $is_available; + } + + /** + * Gets the controller for the specified endpoint. + * + * @since 3.17.0 + * + * @param string $endpoint The endpoint to get the controller for. + * @return Base_API_Controller|null The controller for the specified endpoint. + */ + private function get_controller_for_endpoint( string $endpoint ): ?Base_API_Controller { + foreach ( $this->controllers as $controller ) { + if ( null !== $controller->get_endpoint( $endpoint ) ) { + return $controller; + } + } + + return null; + } } diff --git a/src/rest-api/stats/class-endpoint-post.php b/src/rest-api/stats/class-endpoint-post.php index 6864b46db4..b0e8c9d888 100644 --- a/src/rest-api/stats/class-endpoint-post.php +++ b/src/rest-api/stats/class-endpoint-post.php @@ -10,11 +10,9 @@ namespace Parsely\REST_API\Stats; -use Parsely\RemoteAPI\Analytics_Post_Detail_API; -use Parsely\RemoteAPI\Referrers_Post_Detail_API; -use Parsely\RemoteAPI\Related_API; use Parsely\REST_API\Base_Endpoint; use Parsely\REST_API\Use_Post_ID_Parameter_Trait; +use Parsely\Services\ContentAPI\Content_API_Service; use Parsely\Utils\Utils; use stdClass; use WP_Error; @@ -36,22 +34,13 @@ class Endpoint_Post extends Base_Endpoint { use Related_Posts_Trait; /** - * The API for fetching post details. + * The Parse.ly Content API service. * * @since 3.17.0 * - * @var Analytics_Post_Detail_API $analytics_post_detail_api + * @var Content_API_Service $content_api */ - public $analytics_post_detail_api; - - /** - * The API for fetching post referrers. - * - * @since 3.17.0 - * - * @var Referrers_Post_Detail_API $referrers_post_detail_api - */ - public $referrers_post_detail_api; + public $content_api; /** * The total views of the post. @@ -71,9 +60,7 @@ class Endpoint_Post extends Base_Endpoint { */ public function __construct( Stats_Controller $controller ) { parent::__construct( $controller ); - $this->analytics_post_detail_api = new Analytics_Post_Detail_API( $this->parsely ); - $this->referrers_post_detail_api = new Referrers_Post_Detail_API( $this->parsely ); - $this->related_posts_api = new Related_API( $this->parsely ); + $this->content_api = $this->parsely->get_content_api(); } /** @@ -177,16 +164,18 @@ public function get_post_details( WP_REST_Request $request ) { $post = $request->get_param( 'post' ); $permalink = get_permalink( $post->ID ); + if ( ! is_string( $permalink ) ) { + return new WP_Error( 'invalid_post', __( 'Invalid post.', 'wp-parsely' ), array( 'status' => 404 ) ); + } + // Set the itm_source parameter. $this->set_itm_source_from_request( $request ); // Get the data from the API. - $analytics_request = $this->analytics_post_detail_api->get_items( - array( - 'url' => $permalink, - 'period_start' => $request->get_param( 'period_start' ), - 'period_end' => $request->get_param( 'period_end' ), - ) + $analytics_request = $this->content_api->get_post_details( + $permalink, + $request->get_param( 'period_start' ), + $request->get_param( 'period_end' ) ); if ( is_wp_error( $analytics_request ) ) { @@ -230,6 +219,10 @@ public function get_post_referrers( WP_REST_Request $request ) { $post = $request->get_param( 'post' ); $permalink = get_permalink( $post->ID ); + if ( ! is_string( $permalink ) ) { + return new WP_Error( 'invalid_post', __( 'Invalid post.', 'wp-parsely' ), array( 'status' => 404 ) ); + } + // Set the itm_source parameter. $this->set_itm_source_from_request( $request ); @@ -243,12 +236,10 @@ public function get_post_referrers( WP_REST_Request $request ) { $this->total_views = $total_views; // Get the data from the API. - $analytics_request = $this->referrers_post_detail_api->get_items( - array( - 'url' => $permalink, - 'period_start' => $request->get_param( 'period_start' ), - 'period_end' => $request->get_param( 'period_end' ), - ) + $analytics_request = $this->content_api->get_post_referrers( + $permalink, + $request->get_param( 'period_start' ), + $request->get_param( 'period_end' ) ); if ( is_wp_error( $analytics_request ) ) { diff --git a/src/rest-api/stats/class-endpoint-posts.php b/src/rest-api/stats/class-endpoint-posts.php index 3f7c718fc1..dca7789c3e 100644 --- a/src/rest-api/stats/class-endpoint-posts.php +++ b/src/rest-api/stats/class-endpoint-posts.php @@ -10,8 +10,8 @@ namespace Parsely\REST_API\Stats; -use Parsely\RemoteAPI\Analytics_Posts_API; use Parsely\REST_API\Base_Endpoint; +use Parsely\Services\ContentAPI\Content_API_Service; use WP_Error; use WP_REST_Request; use WP_REST_Response; @@ -62,13 +62,13 @@ class Endpoint_Posts extends Base_Endpoint { ); /** - * The Analytics Posts API. + * The Parse.ly Content API service. * * @since 3.17.0 * - * @var Analytics_Posts_API + * @var Content_API_Service */ - public $analytics_posts_api; + public $content_api; /** * Constructor. @@ -79,7 +79,7 @@ class Endpoint_Posts extends Base_Endpoint { */ public function __construct( Stats_Controller $controller ) { parent::__construct( $controller ); - $this->analytics_posts_api = new Analytics_Posts_API( $this->parsely ); + $this->content_api = $this->parsely->get_content_api(); } /** @@ -149,27 +149,25 @@ public function register_routes(): void { 'default' => 1, ), 'author' => array( - 'description' => 'The author to filter by.', - 'type' => 'array', - 'items' => array( - 'type' => 'string', - ), + 'description' => 'Comma-separated list of authors to filter by.', + 'type' => 'string', 'required' => false, - 'maxItems' => 5, + 'validate_callback' => array( $this, 'validate_max_length_is_5' ), + 'sanitize_callback' => array( $this, 'sanitize_string_to_array' ), ), 'section' => array( - 'description' => 'The section to filter by.', + 'description' => 'Comma-separated list of sections to filter by.', 'type' => 'string', 'required' => false, + 'validate_callback' => array( $this, 'validate_max_length_is_5' ), + 'sanitize_callback' => array( $this, 'sanitize_string_to_array' ), ), 'tag' => array( - 'description' => 'The tag to filter by.', - 'type' => 'array', - 'items' => array( - 'type' => 'string', - ), + 'description' => 'Comma-separated list of tags to filter by.', + 'type' => 'string', 'required' => false, - 'maxItems' => 5, + 'validate_callback' => array( $this, 'validate_max_length_is_5' ), + 'sanitize_callback' => array( $this, 'sanitize_string_to_array' ), ), 'segment' => array( 'description' => 'The segment to filter by.', @@ -182,6 +180,42 @@ public function register_routes(): void { ); } + /** + * Sanitizes a string to an array, splitting it by commas. + * + * @since 3.17.0 + * + * @param string|array $string The string to sanitize. + * @return array + */ + public function sanitize_string_to_array( $string ): array { + if ( is_array( $string ) ) { + return $string; + } + + return explode( ',', $string ); + } + + /** + * Validates that the parameter has at most 5 items. + * + * @since 3.17.0 + * + * @param string|array $string_or_array The string or array to validate. + * @return true|WP_Error + */ + public function validate_max_length_is_5( $string_or_array ) { + if ( is_string( $string_or_array ) ) { + $string_or_array = $this->sanitize_string_to_array( $string_or_array ); + } + + if ( count( $string_or_array ) > 5 ) { + return new WP_Error( 'invalid_param', 'The parameter must have at most 5 items.' ); + } + + return true; + } + /** * API Endpoint: GET /stats/posts * @@ -198,23 +232,12 @@ public function get_posts( WP_REST_Request $request ) { // Setup the itm_source if it is provided. $this->set_itm_source_from_request( $request ); - // TODO: Needed before the Public API refactor. - // Convert array of authors to a string with the first element. - if ( isset( $params['author'] ) && is_array( $params['author'] ) ) { - $params['author'] = $params['author'][0]; - } - // Convert array of tags to a string with the first element. - if ( isset( $params['tag'] ) && is_array( $params['tag'] ) ) { - $params['tag'] = $params['tag'][0]; - } - // TODO END. - /** * The raw analytics data, received by the API. * * @var array|WP_Error $analytics_request */ - $analytics_request = $this->analytics_posts_api->get_items( + $analytics_request = $this->content_api->get_posts( array( 'period_start' => $params['period_start'] ?? null, 'period_end' => $params['period_end'] ?? null, diff --git a/src/rest-api/stats/class-endpoint-related.php b/src/rest-api/stats/class-endpoint-related.php index d5de1404f0..8aa1660a94 100644 --- a/src/rest-api/stats/class-endpoint-related.php +++ b/src/rest-api/stats/class-endpoint-related.php @@ -10,8 +10,8 @@ namespace Parsely\REST_API\Stats; -use Parsely\RemoteAPI\Related_API; use Parsely\REST_API\Base_Endpoint; +use Parsely\Services\ContentAPI\Content_API_Service; use WP_Error; use WP_REST_Request; use WP_REST_Response; @@ -27,6 +27,15 @@ class Endpoint_Related extends Base_Endpoint { use Post_Data_Trait; use Related_Posts_Trait; + /** + * The Parse.ly Content API service. + * + * @since 3.17.0 + * + * @var Content_API_Service $content_api + */ + public $content_api; + /** * Constructor. * @@ -36,7 +45,7 @@ class Endpoint_Related extends Base_Endpoint { */ public function __construct( Stats_Controller $controller ) { parent::__construct( $controller ); - $this->related_posts_api = new Related_API( $this->parsely ); + $this->content_api = $this->parsely->get_content_api(); } /** diff --git a/src/rest-api/stats/trait-post-data.php b/src/rest-api/stats/trait-post-data.php index 784e64b58d..374333c632 100644 --- a/src/rest-api/stats/trait-post-data.php +++ b/src/rest-api/stats/trait-post-data.php @@ -83,45 +83,45 @@ private function get_itm_source_param_args(): array { * @since 3.10.0 * @since 3.17.0 Moved from the `Base_API_Proxy` class. * - * @param stdClass $item The object to extract the data from. + * @param array $item The object to extract the data from. * @return array The extracted data. */ - protected function extract_post_data( stdClass $item ): array { + protected function extract_post_data( array $item ): array { $data = array(); - if ( isset( $item->author ) ) { - $data['author'] = $item->author; + if ( isset( $item['author'] ) ) { + $data['author'] = $item['author']; } - if ( isset( $item->metrics->views ) ) { - $data['views'] = number_format_i18n( $item->metrics->views ); + if ( isset( $item['metrics']['views'] ) ) { + $data['views'] = number_format_i18n( $item['metrics']['views'] ); } - if ( isset( $item->metrics->visitors ) ) { - $data['visitors'] = number_format_i18n( $item->metrics->visitors ); + if ( isset( $item['metrics']['visitors'] ) ) { + $data['visitors'] = number_format_i18n( $item['metrics']['visitors'] ); } // The avg_engaged metric can be in different locations depending on the // endpoint and passed sort/url parameters. - $avg_engaged = $item->metrics->avg_engaged ?? $item->avg_engaged ?? null; + $avg_engaged = $item['metrics']['avg_engaged'] ?? $item['avg_engaged'] ?? null; if ( null !== $avg_engaged ) { $data['avgEngaged'] = Utils::get_formatted_duration( (float) $avg_engaged ); } - if ( isset( $item->pub_date ) ) { - $data['date'] = wp_date( Utils::get_date_format(), strtotime( $item->pub_date ) ); + if ( isset( $item['pub_date'] ) ) { + $data['date'] = wp_date( Utils::get_date_format(), strtotime( $item['pub_date'] ) ); } - if ( isset( $item->title ) ) { - $data['title'] = $item->title; + if ( isset( $item['title'] ) ) { + $data['title'] = $item['title']; } - if ( isset( $item->url ) ) { + if ( isset( $item['url'] ) ) { $site_id = $this->parsely->get_site_id(); // phpcs:ignore WordPressVIPMinimum.Functions.RestrictedFunctions.url_to_postid_url_to_postid - $post_id = url_to_postid( $item->url ); // 0 if the post cannot be found. + $post_id = url_to_postid( $item['url'] ); // 0 if the post cannot be found. - $post_url = Parsely::get_url_with_itm_source( $item->url, null ); + $post_url = Parsely::get_url_with_itm_source( $item['url'], null ); if ( Utils::parsely_is_https_supported() ) { $post_url = str_replace( 'http://', 'https://', $post_url ); } @@ -136,8 +136,8 @@ protected function extract_post_data( stdClass $item ): array { $thumbnail_url = get_the_post_thumbnail_url( $post_id, 'thumbnail' ); if ( false !== $thumbnail_url ) { $data['thumbnailUrl'] = $thumbnail_url; - } elseif ( isset( $item->thumb_url_medium ) ) { - $data['thumbnailUrl'] = $item->thumb_url_medium; + } elseif ( isset( $item['thumb_url_medium'] ) ) { + $data['thumbnailUrl'] = $item['thumb_url_medium']; } } diff --git a/src/rest-api/stats/trait-related-posts.php b/src/rest-api/stats/trait-related-posts.php index f7c486f5ca..0d98deb66a 100644 --- a/src/rest-api/stats/trait-related-posts.php +++ b/src/rest-api/stats/trait-related-posts.php @@ -11,7 +11,7 @@ namespace Parsely\REST_API\Stats; use Parsely\Parsely; -use Parsely\RemoteAPI\Related_API; +use Parsely\Services\ContentAPI\Content_API_Service; use stdClass; use WP_Error; use WP_REST_Request; @@ -22,15 +22,6 @@ * @since 3.17.0 */ trait Related_Posts_Trait { - /** - * The API for fetching related posts. - * - * @since 3.17.0 - * - * @var Related_API $related_posts_api - */ - public $related_posts_api; - /** * The itm_source of for the post URL. * @@ -126,7 +117,8 @@ public function get_related_posts_of_url( WP_REST_Request $request, string $url * * @var array|WP_Error $related_posts_request */ - $related_posts_request = $this->related_posts_api->get_items( + $related_posts_request = $this->content_api->get_related_posts_with_url( + $url, array( 'url' => $url, 'sort' => $request->get_param( 'sort' ), @@ -147,12 +139,12 @@ public function get_related_posts_of_url( WP_REST_Request $request, string $url $itm_source = $this->itm_source; $related_posts = array_map( - static function ( stdClass $item ) use ( $itm_source ) { + static function ( array $item ) use ( $itm_source ) { return (object) array( - 'image_url' => $item->image_url, - 'thumb_url_medium' => $item->thumb_url_medium, - 'title' => $item->title, - 'url' => Parsely::get_url_with_itm_source( $item->url, $itm_source ), + 'image_url' => $item['image_url'], + 'thumb_url_medium' => $item['thumb_url_medium'], + 'title' => $item['title'], + 'url' => Parsely::get_url_with_itm_source( $item['url'], $itm_source ), ); }, $related_posts_request diff --git a/src/services/class-base-api-service.php b/src/services/class-base-api-service.php new file mode 100644 index 0000000000..ac580d61ec --- /dev/null +++ b/src/services/class-base-api-service.php @@ -0,0 +1,54 @@ + + */ + protected $endpoints; + + /** + * The Parsely instance. + * + * @var Parsely + */ + private $parsely; + + public function __construct( Parsely $parsely ) { + $this->parsely = $parsely; + $this->register_endpoints(); + } + + /** + * Registers an endpoint with the service. + * + * @since 3.17.0 + * + * @param Base_Service_Endpoint $endpoint The endpoint to register. + */ + protected function register_endpoint( Base_Service_Endpoint $endpoint ): void { + $this->endpoints[ $endpoint->get_endpoint() ] = $endpoint; + } + + protected function get_endpoint( string $endpoint ): Base_Service_Endpoint { + return $this->endpoints[ $endpoint ]; + } + + protected abstract function get_base_url(): string; + protected abstract function register_endpoints(): void; + + public function get_api_url(): string { + return $this->get_base_url(); + } + + public function get_parsely(): Parsely { + return $this->parsely; + } +} diff --git a/src/services/class-base-service-endpoint.php b/src/services/class-base-service-endpoint.php new file mode 100644 index 0000000000..4fac1347fe --- /dev/null +++ b/src/services/class-base-service-endpoint.php @@ -0,0 +1,200 @@ +api_service = $api_service; + } + + protected function get_headers() { + return array( + 'Content-Type' => 'application/json', + ); + } + + protected function get_request_options( string $method ): array { + $options = array( + 'headers' => $this->get_headers(), + 'method' => $method, + ); + + return $options; + } + + /** + * Returns the common query arguments to send to the remote API. + * + * This can be used for setting common query arguments that are shared + * across multiple endpoints, such as the API key. + * + * @since 3.17.0 + * + * @param array $args Additional query arguments to send to the remote API. + * @return array The query arguments to send to the remote API. + */ + protected function get_query_args( array $args = array() ): array { + return $args; + } + + /** + * Executes the API request. + * + * @since 3.17.0 + * + * @param array $args The arguments to pass to the API request. + * @return WP_Error|array The response from the API. + */ + public abstract function call( array $args = array() ); + + /** + * Returns the endpoint for the API request. + * + * This should be the path to the endpoint, not the full URL. + * Override this method in the child class to return the endpoint. + * + * @since 3.17.0 + * + * @return string The endpoint for the API request. + */ + public abstract function get_endpoint(): string; + + /** + * Sends a request to the remote API. + * + * @since 3.17.0 + * + * @param string $method The HTTP method to use for the request. + * @param array $query_args The query arguments to send to the remote API. + * @param array $data The data to send in the request body. + * @return WP_Error|array The response from the remote API. + */ + protected function request( string $method, array $query_args = array(), array $data = array() ) { + // Get the URL to send the request to. + $request_url = $this->get_endpoint_url( $query_args ); + + // Build the request options. + $request_options = $this->get_request_options( $method ); + + if ( count( $data ) > 0 ) { + $data = $this->truncate_array_content( $data ); + + $request_options['body'] = wp_json_encode( $data ); + if ( false === $request_options['body'] ) { + return new WP_Error( 400, __( 'Unable to encode request body', 'wp-parsely' ) ); + } + } + + $response = wp_safe_remote_request( $request_url, $request_options ); + + return $this->process_response( $response ); + } + + /** + * Returns the full URL for the API request, including the endpoint and query arguments. + * + * @since 3.17.0 + * + * @param array $query_args The query arguments to send to the remote API. + * @return string The full URL for the API request. + */ + protected function get_endpoint_url( array $query_args = array() ): string { + // Get the base URL from the API service. + $base_url = $this->api_service->get_api_url(); + + // Append the endpoint to the base URL. + $base_url .= $this->get_endpoint(); + + // Append any necessary query arguments. + $endpoint = add_query_arg( $this->get_query_args( $query_args ), $base_url ); + + return $endpoint; + } + + /** + * Processes the response from the remote API. + * + * @param array|WP_Error $response The response from the remote API. + * @return array|WP_Error The processed response. + */ + protected function process_response( $response ) { + if ( is_wp_error( $response ) ) { + /** @var WP_Error $response */ + return $response; + } + + $body = wp_remote_retrieve_body( $response ); + $decoded = json_decode( $body, true ); + + if ( ! is_array( $decoded ) ) { + return new WP_Error( 400, __( 'Unable to decode upstream API response', 'wp-parsely' ) ); + } + + return $decoded; + } + + public function get_parsely(): Parsely { + return $this->api_service->get_parsely(); + } + + /** + * Truncates the content of an array to a maximum length. + * + * @since 3.14.1 + * + * @param string|array|mixed $content The content to truncate. + * @return string|array|mixed The truncated content. + */ + private function truncate_array_content( $content ) { + if ( is_array( $content ) ) { + // If the content is an array, iterate over its elements. + foreach ( $content as $key => $value ) { + // Recursively process/truncate each element of the array. + $content[ $key ] = $this->truncate_array_content( $value ); + } + return $content; + } elseif ( is_string( $content ) ) { + // If the content is a string, truncate it. + if ( static::TRUNCATE_CONTENT ) { + // Check if the string length exceeds the maximum and truncate if necessary. + if ( mb_strlen( $content ) > self::TRUNCATE_CONTENT_LENGTH ) { + return mb_substr( $content, 0, self::TRUNCATE_CONTENT_LENGTH ); + } + } + return $content; + } + return $content; + } + +} diff --git a/src/services/content-api/class-content-api-service.php b/src/services/content-api/class-content-api-service.php new file mode 100644 index 0000000000..30a8ba95d5 --- /dev/null +++ b/src/services/content-api/class-content-api-service.php @@ -0,0 +1,202 @@ +register_endpoint( $endpoint ); + } + } + + /** + * Returns the post’s metadata, as well as total views and visitors in the metrics field. + * + * By default, this returns the total pageviews on the link for the last 90 days. + * + * @since 3.17.0 + * + * @link https://docs.parse.ly/api-analytics-endpoint/#2-get-analytics-post-detail + * + * @param string $url The URL of the post. + * @param string|null $period_start The start date of the period to get the data for. + * @param string|null $period_end The end date of the period to get the data for. + * @return array|WP_Error Returns the post details or a WP_Error object in case of an error. + */ + public function get_post_details( + string $url, + string $period_start = null, + string $period_end = null + ) { + /** @var Endpoints\Endpoint_Analytics_Post_Details $endpoint */ + $endpoint = $this->get_endpoint( '/analytics/post/detail' ); + + $args = array( + 'url' => $url, + 'period_start' => $period_start, + 'period_end' => $period_end, + ); + + return $endpoint->call( $args ); + } + + /** + * Returns the referrers for a given post URL. + * + * @since 3.17.0 + * + * @link https://docs.parse.ly/api-referrers-endpoint/#3-get-referrers-post-detail + * + * @param string $url The URL of the post. + * @param string|null $period_start The start date of the period to get the data for. + * @param string|null $period_end The end date of the period to get the data for. + * @return array|WP_Error Returns the referrers or a WP_Error object in case of an error. + */ + public function get_post_referrers( + string $url, + string $period_start = null, + string $period_end = null + ) { + /** @var Endpoints\Endpoint_Referrers_Post_Detail $endpoint */ + $endpoint = $this->get_endpoint( '/referrers/post/detail' ); + + $args = array( + 'url' => $url, + 'period_start' => $period_start, + 'period_end' => $period_end, + ); + + return $endpoint->call( $args ); + } + + /** + * Returns the related posts for a given URL. + * + * @since 3.17.0 + * + * @link https://docs.parse.ly/content-recommendations/#h-get-related + * + * @param string $url The URL of the post. + * @param array $params The parameters to pass to the API request. + * @return array|WP_Error Returns the related posts or a WP_Error object in case of an error. + */ + public function get_related_posts_with_url( string $url, array $params = array() ) { + /** @var Endpoints\Endpoint_Related $endpoint */ + $endpoint = $this->get_endpoint( '/related' ); + + $args = array( + 'url' => $url, + ); + + // Merge the optional params. + $args = array_merge( $params, $args ); + + return $endpoint->call( $args ); + } + + /** + * Returns the related posts for a given UUID. + * + * @since 3.17.0 + * + * @link https://docs.parse.ly/content-recommendations/#h-get-related + * + * @param string $uuid The UUID of the user. + * @param array $params The parameters to pass to the API request. + * @return array|WP_Error Returns the related posts or a WP_Error object in case of an error. + */ + public function get_related_posts_with_uuid( string $uuid, array $params = array() ) { + /** @var Endpoints\Endpoint_Related $endpoint */ + $endpoint = $this->get_endpoint( '/related' ); + + $args = array( + 'uuid' => $uuid, + ); + + // Merge the optional params. + $args = array_merge( $params, $args ); + + return $endpoint->call( $args ); + } + + /** + * Returns the posts analytics. + * + * @since 3.17.0 + * + * @link https://docs.parse.ly/api-analytics-endpoint/#1-get-analytics-posts + * + * @param array $params The parameters to pass to the API request. + * @return array|WP_Error Returns the posts analytics or a WP_Error object in case of an error. + */ + public function get_posts( array $params = array() ) { + /** @var Endpoints\Endpoint_Analytics_Posts $endpoint */ + $endpoint = $this->get_endpoint( '/analytics/posts' ); + + return $endpoint->call( $params ); + } + + /** + * Validates the Parse.ly API credentials. + * + * The API will return a 200 response if the credentials are valid and a 401 response if they are not. + * + * @since 3.17.0 + * + * @param string $api_key The API key to validate. + * @param string $secret_key The secret key to validate. + * @return bool|WP_Error Returns true if the credentials are valid, false otherwise. + */ + public function validate_credentials( string $api_key, string $secret_key ) { + /** @var Endpoints\Endpoint_Validate $endpoint */ + $endpoint = $this->get_endpoint( '/validate/secret' ); + + $args = array( + 'apikey' => $api_key, + 'secret' => $secret_key, + ); + + $response = $endpoint->call( $args ); + + if ( is_wp_error( $response ) ) { + /** @var WP_Error $response */ + return $response; + } + + if ( true === $response['success'] ) { + return true; + } + + return false; + } +} diff --git a/src/services/content-api/endpoints/class-content-api-base-endpoint.php b/src/services/content-api/endpoints/class-content-api-base-endpoint.php new file mode 100644 index 0000000000..eced1d621c --- /dev/null +++ b/src/services/content-api/endpoints/class-content-api-base-endpoint.php @@ -0,0 +1,83 @@ +, + * } + * + * @phpstan-type Content_API_Error_Response array{ + * code?: int, + * message?: string, + * } + * + * @phpstan-type Content_API_Response = Content_API_Valid_Response|Content_API_Error_Response + */ +abstract class Content_API_Base_Endpoint extends Base_Service_Endpoint { + /** + * Returns the common query arguments to send to the remote API. + * + * This method append the API key and secret to the query arguments. + * + * @since 3.17.0 + * + * @param array $args Additional query arguments to send to the remote API. + * @return array The query arguments to send to the remote API. + */ + protected function get_query_args( array $args = array() ): array { + $query_args = parent::get_query_args( $args ); + + $query_args['timeout'] = 30; // phpcs:ignore WordPressVIPMinimum.Performance.RemoteRequestTimeout.timeout_timeout + + // Set up the API key and secret. + $query_args['apikey'] = $this->get_parsely()->get_site_id(); + if ( $this->get_parsely()->api_secret_is_set() ) { + $query_args['secret'] = $this->get_parsely()->get_api_secret(); + } + + return $query_args; + } + + /** + * Processes the response from the remote API. + * + * @param array|WP_Error $response The response from the remote API. + * + * @return array|WP_Error The processed response. + */ + protected function process_response( $response ) { + $response = parent::process_response( $response ); + + if ( is_wp_error( $response ) || ! is_array( $response ) ) { + /** @var WP_Error $response */ + return $response; + } + + if ( ! isset( $response['data'] ) ) { + /** @var Content_API_Error_Response $response */ + return new WP_Error( + $response['code'] ?? 400, + $response['message'] ?? __( 'Unable to read data from upstream API', 'wp-parsely' ), + array( 'status' => $response['code'] ?? 400 ) + ); + } + + if ( ! is_array( $response['data'] ) ) { + return new WP_Error( 400, __( 'Unable to parse data from upstream API', 'wp-parsely' ) ); + } + + /** @var Content_API_Valid_Response $response */ + return $response['data']; + } + + +} diff --git a/src/services/content-api/endpoints/class-endpoint-analytics-post-details.php b/src/services/content-api/endpoints/class-endpoint-analytics-post-details.php new file mode 100644 index 0000000000..ebc1f301c9 --- /dev/null +++ b/src/services/content-api/endpoints/class-endpoint-analytics-post-details.php @@ -0,0 +1,44 @@ + $args The arguments to pass to the API request. + * @return WP_Error|array The response from the API request. + */ + public function call( array $args = array() ) { + $query_args = array( + 'url' => $args['url'], + 'period_start' => $args['period_start'], + 'period_end' => $args['period_end'], + ); + + // Filter out the empty values. + $query_args = array_filter( $query_args ); + + return $this->request( 'GET', $query_args ); + } + +} diff --git a/src/services/content-api/endpoints/class-endpoint-analytics-posts.php b/src/services/content-api/endpoints/class-endpoint-analytics-posts.php new file mode 100644 index 0000000000..3b76f4ea87 --- /dev/null +++ b/src/services/content-api/endpoints/class-endpoint-analytics-posts.php @@ -0,0 +1,150 @@ +, + * } + * + * @phpstan-type Analytics_Post array{ + * title?: string, + * url?: string, + * link?: string, + * author?: string, + * authors?: string[], + * section?: string, + * tags?: string[], + * metrics?: Analytics_Post_Metrics, + * full_content_word_count?: int, + * image_url?: string, + * metadata?: string, + * pub_date?: string, + * thumb_url_medium?: string, + * } + * + * @phpstan-type Analytics_Post_Metrics array{ + * avg_engaged?: float, + * views?: int, + * visitors?: int, + * } + */ +class Endpoint_Analytics_Posts extends Content_API_Base_Endpoint { + public const MAX_RECORDS_LIMIT = 2000; + public const ANALYTICS_API_DAYS_LIMIT = 7; + + /** + * Returns the endpoint for the API request. + * + * @since 3.17.0 + * + * @return string + */ + public function get_endpoint(): string { + return '/analytics/posts'; + } + + /** + * Returns the endpoint URL for the API request. + * + * This method appends the author, tag, and section parameters to the endpoint URL, + * if they are set. Since Parsely API needs the multiple values for these parameters to share + * the same key, we need to append them manually. + * + * @since 3.17.0 + * + * @param array $args The arguments to pass to the API request. + * @return string The endpoint URL for the API request. + */ + protected function get_endpoint_url( array $args = array() ): string { + // Store the author, tag, and section parameters. + /** @var array $authors */ + $authors = $args['author'] ?? array(); + + /** @var array $tags */ + $tags = $args['tag'] ?? array(); + + /** @var array $sections */ + $sections = $args['section'] ?? array(); + + // Remove the author, tag, and section parameters from the query args. + unset( $args['author'] ); + unset( $args['tag'] ); + unset( $args['section'] ); + + // Generate the endpoint URL. + $endpoint_url = parent::get_endpoint_url( $args ); + + // Append the author, tag, and section parameters to the endpoint URL. + $endpoint_url = $this->append_multiple_params_to_url( $endpoint_url, $authors, 'author' ); + $endpoint_url = $this->append_multiple_params_to_url( $endpoint_url, $tags, 'tag' ); + $endpoint_url = $this->append_multiple_params_to_url( $endpoint_url, $sections, 'section' ); + + return $endpoint_url; + } + + /** + * Executes the API request. + * + * @param array $args The arguments to pass to the API request. + * @return WP_Error|array The response from the API request. + */ + public function call( array $args = array() ) { + // Filter out the empty values. + $query_args = array_filter( $args ); + + // If the period_start is set to 'max_days', set it to the maximum days limit. + if ( $query_args['period_start'] === 'max_days' ) { + $query_args['period_start'] = self::ANALYTICS_API_DAYS_LIMIT . 'd'; + } + + // If the limit is set to 'max' or greater than the maximum records limit, + // set it to the maximum records limit. + if ( $query_args['limit'] === 'max' || $query_args['limit'] > self::MAX_RECORDS_LIMIT) { + $query_args['limit'] = self::MAX_RECORDS_LIMIT; + } + + return $this->request( 'GET', $query_args ); + } + + + /** + * Appends multiple parameters to the URL. + * + * This is required because the Parsely API requires the multiple values for the author, tag, + * and section parameters to share the same key. + * + * @param string $url The URL to append the parameters to. + * @param array $params The parameters to append. + * @param string $param_name The name of the parameter. + * @return string The URL with the appended parameters. + */ + protected function append_multiple_params_to_url( string $url, array $params, string $param_name ): string { + foreach ( $params as $param ) { + $param = rawurlencode( $param ); + if ( strpos( $url, $param_name . '=' ) === false ) { + $url = add_query_arg( $param_name, $param, $url ); + } else { + $url .= '&' . $param_name . '=' . $param; + } + } + return $url; + } + +} diff --git a/src/services/content-api/endpoints/class-endpoint-referrers-post-detail.php b/src/services/content-api/endpoints/class-endpoint-referrers-post-detail.php new file mode 100644 index 0000000000..d10ea16768 --- /dev/null +++ b/src/services/content-api/endpoints/class-endpoint-referrers-post-detail.php @@ -0,0 +1,44 @@ + $args The arguments to pass to the API request. + * @return WP_Error|array The response from the API request. + */ + public function call( array $args = array() ) { + $query_args = array( + 'url' => $args['url'], + 'period_start' => $args['period_start'], + 'period_end' => $args['period_end'], + ); + + // Filter out the empty values. + $query_args = array_filter( $query_args ); + + return $this->request( 'GET', $query_args ); + } + +} diff --git a/src/services/content-api/endpoints/class-endpoint-related.php b/src/services/content-api/endpoints/class-endpoint-related.php new file mode 100644 index 0000000000..68cad37b20 --- /dev/null +++ b/src/services/content-api/endpoints/class-endpoint-related.php @@ -0,0 +1,43 @@ + $args The arguments to pass to the API request. + * @return WP_Error|array The response from the API request. + */ + public function call( array $args = array() ) { + // Filter out the empty values. + $args = array_filter( $args ); + + // When the URL is provided, the UUID cannot be provided. + if ( isset( $args['uuid'] ) && isset( $args['url'] ) ) { + unset( $args['uuid'] ); + } + + return $this->request( 'GET', $args ); + } + +} diff --git a/src/services/content-api/endpoints/class-endpoint-validate.php b/src/services/content-api/endpoints/class-endpoint-validate.php new file mode 100644 index 0000000000..c0ddc46f93 --- /dev/null +++ b/src/services/content-api/endpoints/class-endpoint-validate.php @@ -0,0 +1,100 @@ + $args The query arguments to send to the remote API. + * @return array The query arguments for the API request. + */ + public function get_query_args( array $args = array() ): array { + return $args; + } + + /** + * Queries the Parse.ly API credentials validation endpoint. + * The API will return a 200 response if the credentials are valid and a 403 response if they are not. + * + * @since 3.17.0 + * + * @param string $api_key The API key to validate. + * @param string $secret_key The secret key to validate. + * @return array|WP_Error The response from the remote API, or a WP_Error object if the response is an error. + */ + private function api_validate_credentials( string $api_key, string $secret_key ) { + $query = array( + 'apikey' => $api_key, + 'secret' => $secret_key, + ); + + $response = $this->request( 'GET', $query ); + + if ( is_wp_error( $response ) ) { + return $response; + } + + + if ( false === $response['success'] ) { + return new WP_Error( + $response['code'] ?? 403, + $response['message'] ?? __( 'Unable to validate the API credentials', 'wp-parsely' ) + ); + } + + return $response; + } + + /** + * Processes the response from the remote API. + * + * @since 3.17.0 + * + * @param array|WP_Error $response The response from the remote API. + * @return array|WP_Error The processed response. + */ + protected function process_response( $response ) { + return Base_Service_Endpoint::process_response( $response ); + } + + /** + * Executes the API request. + * + * @param array $args The arguments to pass to the API request. + * @return WP_Error|array The response from the API request. + */ + public function call( array $args = array() ) { + /** @var string $api_key */ + $api_key = $args['apikey']; + /** @var string $secret */ + $secret = $args['secret']; + + return $this->api_validate_credentials( $api_key, $secret ); + } + +} diff --git a/src/services/trait-cached-service-endpoint.php b/src/services/trait-cached-service-endpoint.php new file mode 100644 index 0000000000..9b186e0ef1 --- /dev/null +++ b/src/services/trait-cached-service-endpoint.php @@ -0,0 +1,22 @@ + $query_args The query arguments to send to the remote API. + * @param array $data The data to send in the request body. + * @return WP_Error|array The response from the remote API. + */ + protected function request( string $method, array $query_args = array(), array $data = array() ) { + // Get the URL to send the request to. + die(); + } +} diff --git a/wp-parsely.php b/wp-parsely.php index 29997ea080..f1db9e67cc 100644 --- a/wp-parsely.php +++ b/wp-parsely.php @@ -57,6 +57,10 @@ require_once __DIR__ . '/vendor/autoload.php'; } +// Load Telemetry classes. +require_once __DIR__ . '/src/Telemetry/telemetry-init.php'; + + add_action( 'plugins_loaded', __NAMESPACE__ . '\\parsely_initialize_plugin' ); /** * Registers the basic classes to initialize the plugin. @@ -108,7 +112,7 @@ function parsely_wp_admin_early_register(): void { $network_admin_sites_list->run(); // Initialize the REST API Controller. - $rest_api_controller = new REST_API_Controller( $GLOBALS['parsely'] ); + $rest_api_controller = $GLOBALS['parsely']->get_rest_api_controller(); $rest_api_controller->init(); } From 1a7ee36cb538224486139a3ad801b4a96edf475f Mon Sep 17 00:00:00 2001 From: Henrique Mouta Date: Thu, 10 Oct 2024 17:05:30 +0100 Subject: [PATCH 32/49] Implement caching mechanism for service endpoints --- src/services/class-base-api-service.php | 14 ++- src/services/class-base-service-endpoint.php | 2 +- .../class-cached-service-endpoint.php | 96 +++++++++++++++++++ .../content-api/class-content-api-service.php | 31 ++++-- .../trait-cached-service-endpoint.php | 22 ----- 5 files changed, 135 insertions(+), 30 deletions(-) create mode 100644 src/services/class-cached-service-endpoint.php delete mode 100644 src/services/trait-cached-service-endpoint.php diff --git a/src/services/class-base-api-service.php b/src/services/class-base-api-service.php index ac580d61ec..b7c80a24de 100644 --- a/src/services/class-base-api-service.php +++ b/src/services/class-base-api-service.php @@ -37,11 +37,23 @@ protected function register_endpoint( Base_Service_Endpoint $endpoint ): void { $this->endpoints[ $endpoint->get_endpoint() ] = $endpoint; } + /** + * Registers a cached endpoint with the service. + * + * @since 3.17.0 + * + * @param Base_Service_Endpoint $endpoint The endpoint to register. + * @param int $ttl The time-to-live for the cache, in seconds. + */ + protected function register_cached_endpoint( Base_Service_Endpoint $endpoint, int $ttl ): void { + $this->endpoints[ $endpoint->get_endpoint() ] = new Cached_Service_Endpoint( $endpoint, $ttl ); + } + protected function get_endpoint( string $endpoint ): Base_Service_Endpoint { return $this->endpoints[ $endpoint ]; } - protected abstract function get_base_url(): string; + public abstract function get_base_url(): string; protected abstract function register_endpoints(): void; public function get_api_url(): string { diff --git a/src/services/class-base-service-endpoint.php b/src/services/class-base-service-endpoint.php index 4fac1347fe..53dc8b51ff 100644 --- a/src/services/class-base-service-endpoint.php +++ b/src/services/class-base-service-endpoint.php @@ -13,7 +13,7 @@ abstract class Base_Service_Endpoint { * * @var Base_API_Service */ - private $api_service; + protected $api_service; /** * Flag to truncate the content of the request body. diff --git a/src/services/class-cached-service-endpoint.php b/src/services/class-cached-service-endpoint.php new file mode 100644 index 0000000000..96206993c1 --- /dev/null +++ b/src/services/class-cached-service-endpoint.php @@ -0,0 +1,96 @@ +service_endpoint = $service_endpoint; + $this->cache_ttl = $cache_ttl; + + parent::__construct( $service_endpoint->api_service ); + } + + /** + * Returns the cache key for the API request. + * + * @param array $args The arguments to pass to the API request. + * + * @return string The cache key for the API request. + */ + private function get_cache_key( array $args ): string { + $api_service = $this->service_endpoint->api_service; + + $cache_key = 'parsely_api_' . + wp_hash( $api_service->get_base_url() ) . '_' . + wp_hash( $this->get_endpoint() ) . '_' . + wp_hash( (string) wp_json_encode( $args ) ); + + return $cache_key; + } + + /** + * Executes the API request, caching the response. + * + * If the response is already cached, it will be returned from the cache, + * otherwise the API request will be made and the response will be cached. + * + * @since 3.17.0 + * + * @param array $args The arguments to pass to the API request. + * @return WP_Error|array The response from the API. + */ + public function call( array $args = array() ) { + $cache_key = $this->get_cache_key( $args ); + $cache = wp_cache_get( $cache_key, self::CACHE_GROUP ); + + if ( false !== $cache ) { + // @phpstan-ignore-next-line + return $cache; + } + + $response = $this->service_endpoint->call( $args ); + + if ( ! is_wp_error( $response ) ) { + wp_cache_set( $cache_key, $response, self::CACHE_GROUP, $this->cache_ttl ); // phpcs:ignore + } + + return $response; + } + + /** + * Returns the endpoint for the API request. + * + * @since 3.17.0 + * + * @return string The endpoint for the API request. + */ + public function get_endpoint(): string { + return $this->service_endpoint->get_endpoint(); + } +} diff --git a/src/services/content-api/class-content-api-service.php b/src/services/content-api/class-content-api-service.php index 30a8ba95d5..e610c4a915 100644 --- a/src/services/content-api/class-content-api-service.php +++ b/src/services/content-api/class-content-api-service.php @@ -3,8 +3,8 @@ namespace Parsely\Services\ContentAPI; -use Automattic\Jetpack\Import\Endpoints\End; use Parsely\Services\Base_API_Service; +use Parsely\Services\Base_Service_Endpoint; use WP_Error; class Content_API_Service extends Base_API_Service { @@ -16,7 +16,7 @@ class Content_API_Service extends Base_API_Service { * * @return string */ - protected function get_base_url(): string { + public function get_base_url(): string { return "https://api.parsely.com/v2"; } @@ -26,17 +26,36 @@ protected function get_base_url(): string { * @since 3.17.0 */ protected function register_endpoints(): void { + /** + * The endpoints for the Parse.ly Content API. + * + * @var Base_Service_Endpoint[] $endpoints + */ $endpoints = array( - new Endpoints\Endpoint_Analytics_Post_Details( $this ), - new Endpoints\Endpoint_Referrers_Post_Detail( $this ), - new Endpoints\Endpoint_Related( $this ), - new Endpoints\Endpoint_Analytics_Posts( $this ), new Endpoints\Endpoint_Validate( $this ), ); foreach ( $endpoints as $endpoint ) { $this->register_endpoint( $endpoint ); } + + /** + * The cached endpoints. + * + * The second element in the array is the time-to-live for the cache, in seconds. + * + * @var array $cached_endpoints + */ + $cached_endpoints = array( + array( new Endpoints\Endpoint_Analytics_Posts( $this ), 300 ), // 5 minutes. + array( new Endpoints\Endpoint_Related( $this ), 600 ), // 10 minutes. + array( new Endpoints\Endpoint_Referrers_Post_Detail( $this ), 300 ), // 5 minutes. + array( new Endpoints\Endpoint_Analytics_Post_Details( $this ), 300 ), // 5 minutes. + ); + + foreach ( $cached_endpoints as $cached_endpoint ) { + $this->register_cached_endpoint( $cached_endpoint[0], $cached_endpoint[1] ); + } } /** diff --git a/src/services/trait-cached-service-endpoint.php b/src/services/trait-cached-service-endpoint.php deleted file mode 100644 index 9b186e0ef1..0000000000 --- a/src/services/trait-cached-service-endpoint.php +++ /dev/null @@ -1,22 +0,0 @@ - $query_args The query arguments to send to the remote API. - * @param array $data The data to send in the request body. - * @return WP_Error|array The response from the remote API. - */ - protected function request( string $method, array $query_args = array(), array $data = array() ) { - // Get the URL to send the request to. - die(); - } -} From 5352a979e48e8b11cac23c0e898d684c11b4246b Mon Sep 17 00:00:00 2001 From: Henrique Mouta Date: Fri, 11 Oct 2024 16:32:58 +0100 Subject: [PATCH 33/49] Implement the Suggestions API service --- src/Endpoints/class-base-endpoint.php | 194 ---------------- src/RemoteAPI/class-base-endpoint-remote.php | 153 ------------- src/RemoteAPI/class-remote-api-cache.php | 91 -------- src/RemoteAPI/class-wordpress-cache.php | 52 ----- .../class-content-suggestions-base-api.php | 207 ------------------ .../class-suggest-brief-api.php | 73 ------ .../class-suggest-headline-api.php | 69 ------ .../class-suggest-linked-reference-api.php | 88 -------- src/RemoteAPI/interface-cache.php | 46 ---- src/RemoteAPI/interface-remote-api.php | 41 ---- src/class-parsely.php | 40 +++- .../post-list-stats/class-post-list-stats.php | 8 +- .../class-endpoint-excerpt-generator.php | 50 ++++- .../class-endpoint-smart-linking.php | 17 +- .../class-endpoint-title-suggestions.php | 18 +- src/services/class-base-service-endpoint.php | 19 +- .../class-content-api-base-endpoint.php | 8 +- .../class-suggestions-api-service.php | 104 +++++++++ .../class-endpoint-suggest-brief.php | 90 ++++++++ .../class-endpoint-suggest-headline.php | 92 ++++++++ ...lass-endpoint-suggest-linked-reference.php | 113 ++++++++++ .../class-suggestions-api-base-endpoint.php | 97 ++++++++ 22 files changed, 616 insertions(+), 1054 deletions(-) delete mode 100644 src/Endpoints/class-base-endpoint.php delete mode 100644 src/RemoteAPI/class-base-endpoint-remote.php delete mode 100644 src/RemoteAPI/class-remote-api-cache.php delete mode 100644 src/RemoteAPI/class-wordpress-cache.php delete mode 100644 src/RemoteAPI/content-suggestions/class-content-suggestions-base-api.php delete mode 100644 src/RemoteAPI/content-suggestions/class-suggest-brief-api.php delete mode 100644 src/RemoteAPI/content-suggestions/class-suggest-headline-api.php delete mode 100644 src/RemoteAPI/content-suggestions/class-suggest-linked-reference-api.php delete mode 100644 src/RemoteAPI/interface-cache.php delete mode 100644 src/RemoteAPI/interface-remote-api.php create mode 100644 src/services/suggestions-api/class-suggestions-api-service.php create mode 100644 src/services/suggestions-api/endpoints/class-endpoint-suggest-brief.php create mode 100644 src/services/suggestions-api/endpoints/class-endpoint-suggest-headline.php create mode 100644 src/services/suggestions-api/endpoints/class-endpoint-suggest-linked-reference.php create mode 100644 src/services/suggestions-api/endpoints/class-suggestions-api-base-endpoint.php diff --git a/src/Endpoints/class-base-endpoint.php b/src/Endpoints/class-base-endpoint.php deleted file mode 100644 index 91af0c1075..0000000000 --- a/src/Endpoints/class-base-endpoint.php +++ /dev/null @@ -1,194 +0,0 @@ -parsely = $parsely; - } - - /** - * Returns the user capability allowing access to the endpoint, after having - * applied capability filters. - * - * `DEFAULT_ACCESS_CAPABILITY` is not passed here by default, to allow for - * a more explicit declaration in child classes. - * - * @since 3.14.0 - * - * @param string $capability The original capability allowing access. - * @return string The capability allowing access after applying the filters. - */ - protected function apply_capability_filters( string $capability ): string { - /** - * Filter to change the default user capability for all private endpoints. - * - * @var string - */ - $default_user_capability = apply_filters( - 'wp_parsely_user_capability_for_all_private_apis', - $capability - ); - - /** - * Filter to change the user capability for the specific endpoint. - * - * @var string - */ - $endpoint_specific_user_capability = apply_filters( - 'wp_parsely_user_capability_for_' . Utils::convert_endpoint_to_filter_key( static::ENDPOINT ) . '_api', - $default_user_capability - ); - - return $endpoint_specific_user_capability; - } - - /** - * Registers the endpoint's WP REST route. - * - * @since 3.11.0 Moved from Base_Endpoint_Remote into Base_Endpoint. - * - * @param string $endpoint The endpoint's route. - * @param string $callback The callback function to call when the endpoint is hit. - * @param array $methods The HTTP methods to allow for the endpoint. - */ - public function register_endpoint( - string $endpoint, - string $callback, - array $methods = array( 'GET' ) - ): void { - if ( ! apply_filters( 'wp_parsely_enable_' . Utils::convert_endpoint_to_filter_key( $endpoint ) . '_api_proxy', true ) ) { - return; - } - - $get_items_args = array( - 'query' => array( - 'default' => array(), - 'sanitize_callback' => function ( array $query ) { - $sanitized_query = array(); - foreach ( $query as $key => $value ) { - $sanitized_query[ sanitize_key( $key ) ] = sanitize_text_field( $value ); - } - - return $sanitized_query; - }, - ), - ); - - $rest_route_args = array( - array( - 'methods' => $methods, - 'callback' => array( $this, $callback ), - 'permission_callback' => array( $this, 'is_available_to_current_user' ), - 'args' => $get_items_args, - 'show_in_index' => static::is_available_to_current_user(), - ), - ); - - register_rest_route( 'wp-parsely/v1', $endpoint, $rest_route_args ); - } - - /** - * Registers the endpoint's WP REST route with arguments. - * - * @since 3.16.0 - * - * @param string $endpoint The endpoint's route. - * @param string $callback The callback function to call when the endpoint is hit. - * @param array $methods The HTTP methods to allow for the endpoint. - * @param array $args The arguments for the endpoint. - */ - public function register_endpoint_with_args( - string $endpoint, - string $callback, - array $methods = array( 'GET' ), - array $args = array() - ): void { - if ( ! apply_filters( 'wp_parsely_enable_' . Utils::convert_endpoint_to_filter_key( $endpoint ) . '_api_proxy', true ) ) { - return; - } - - $rest_route_args = array( - array( - 'methods' => $methods, - 'callback' => array( $this, $callback ), - 'permission_callback' => array( $this, 'is_available_to_current_user' ), - 'args' => $args, - 'show_in_index' => static::is_available_to_current_user(), - ), - ); - - register_rest_route( 'wp-parsely/v1', $endpoint, $rest_route_args ); - } -} diff --git a/src/RemoteAPI/class-base-endpoint-remote.php b/src/RemoteAPI/class-base-endpoint-remote.php deleted file mode 100644 index e9ec20eb38..0000000000 --- a/src/RemoteAPI/class-base-endpoint-remote.php +++ /dev/null @@ -1,153 +0,0 @@ - $query The query arguments to send to the remote API. - * @throws UnexpectedValueException If the endpoint constant is not defined. - * @throws UnexpectedValueException If the query filter constant is not defined. - * @return string - */ - public function get_api_url( array $query ): string { - $this->validate_required_constraints(); - - $query['apikey'] = $this->parsely->get_site_id(); - if ( $this->parsely->api_secret_is_set() ) { - $query['secret'] = $this->parsely->get_api_secret(); - } - $query = array_filter( $query ); - - // Sort by key so the query args are in alphabetical order. - ksort( $query ); - - // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.DynamicHooknameFound -- Hook names are defined in child classes. - $query = apply_filters( static::QUERY_FILTER, $query ); - return add_query_arg( $query, static::API_BASE_URL . static::ENDPOINT ); - } - - /** - * Gets items from the specified endpoint. - * - * @since 3.2.0 - * @since 3.7.0 Added $associative param. - * - * @param array $query The query arguments to send to the remote API. - * @param bool $associative When TRUE, returned objects will be converted into associative arrays. - * @return array|object|WP_Error - */ - public function get_items( array $query, bool $associative = false ) { - $full_api_url = $this->get_api_url( $query ); - - /** - * GET request options. - * - * @var WP_HTTP_Request_Args $options - */ - $options = $this->get_request_options(); - $response = wp_safe_remote_get( $full_api_url, $options ); - - if ( is_wp_error( $response ) ) { - return $response; - } - - $body = wp_remote_retrieve_body( $response ); - $decoded = json_decode( $body ); - - if ( ! is_object( $decoded ) ) { - return new WP_Error( 400, __( 'Unable to decode upstream API response', 'wp-parsely' ) ); - } - - if ( ! property_exists( $decoded, 'data' ) ) { - return new WP_Error( - $decoded->code ?? 400, - $decoded->message ?? __( 'Unable to read data from upstream API', 'wp-parsely' ), - array( 'status' => $decoded->code ?? 400 ) - ); - } - - if ( ! is_array( $decoded->data ) ) { - return new WP_Error( 400, __( 'Unable to parse data from upstream API', 'wp-parsely' ) ); - } - - $data = $decoded->data; - - return $associative ? Utils::convert_to_associative_array( $data ) : $data; - } - - /** - * Returns the request's options for the remote API call. - * - * @since 3.9.0 - * - * @return array The array of options. - */ - public function get_request_options(): array { - return array(); - } - - /** - * Validates that required constants are defined. - * - * @since 3.14.0 - * - * @throws UnexpectedValueException If any required constant is not defined. - */ - protected function validate_required_constraints(): void { - if ( static::ENDPOINT === '' ) { - throw new UnexpectedValueException( 'ENDPOINT constant must be defined in child class.' ); - } - if ( static::QUERY_FILTER === '' ) { - throw new UnexpectedValueException( 'QUERY_FILTER constant must be defined in child class.' ); - } - } -} diff --git a/src/RemoteAPI/class-remote-api-cache.php b/src/RemoteAPI/class-remote-api-cache.php deleted file mode 100644 index 9b60c8a46f..0000000000 --- a/src/RemoteAPI/class-remote-api-cache.php +++ /dev/null @@ -1,91 +0,0 @@ -remote_api = $remote_api; - $this->cache = $cache; - } - - /** - * Implements caching for the Remote API interface. - * - * @param array $query The query arguments to send to the remote API. - * @param bool $associative Always `false`, just present to make definition compatible - * with interface. - * @return array|object|WP_Error The response from the remote API, or false if the - * response is empty. - */ - public function get_items( array $query, bool $associative = false ) { - $cache_key = 'parsely_api_' . - wp_hash( $this->remote_api->get_endpoint() ) . '_' . - wp_hash( (string) wp_json_encode( $query ) ); - - /** - * Variable. - * - * @var array|false - */ - $items = $this->cache->get( $cache_key, self::CACHE_GROUP ); - - if ( false === $items ) { - $items = $this->remote_api->get_items( $query ); - $this->cache->set( $cache_key, $items, self::CACHE_GROUP, self::OBJECT_CACHE_TTL ); - } - - return $items; - } - - /** - * Returns whether the endpoint is available for access by the current - * user. - * - * @since 3.7.0 - * @since 3.14.0 Renamed from `is_user_allowed_to_make_api_call()`. - * @since 3.16.0 Added the `$request` parameter. - * - * @param WP_REST_Request|null $request The request object. - * @return bool - */ - public function is_available_to_current_user( $request = null ): bool { - return $this->remote_api->is_available_to_current_user( $request ); - } -} diff --git a/src/RemoteAPI/class-wordpress-cache.php b/src/RemoteAPI/class-wordpress-cache.php deleted file mode 100644 index 59c63e32b5..0000000000 --- a/src/RemoteAPI/class-wordpress-cache.php +++ /dev/null @@ -1,52 +0,0 @@ -apply_capability_filters( - self::DEFAULT_ACCESS_CAPABILITY - ) - ); - } - - /** - * Returns the request's options for the remote API call. - * - * @since 3.12.0 - * - * @return array The array of options. - */ - public function get_request_options(): array { - $options = array( - 'headers' => array( 'Content-Type' => 'application/json; charset=utf-8' ), - 'data_format' => 'body', - 'timeout' => 60, //phpcs:ignore WordPressVIPMinimum.Performance.RemoteRequestTimeout.timeout_timeout - 'body' => '{}', - ); - - // Add API key to request headers. - if ( $this->parsely->api_secret_is_set() ) { - $options['headers']['X-APIKEY-SECRET'] = $this->parsely->get_api_secret(); - } - - return $options; - } - - /** - * Gets the URL for a particular Parse.ly API Content Suggestion endpoint. - * - * @since 3.14.0 - * - * @param array $query The query arguments to send to the remote API. - * @throws UnexpectedValueException If the endpoint constant is not defined. - * @throws UnexpectedValueException If the query filter constant is not defined. - * @return string - */ - public function get_api_url( array $query = array() ): string { - $this->validate_required_constraints(); - - $query['apikey'] = $this->parsely->get_site_id(); - - // Remove empty entries and sort by key so the query args are in - // alphabetical order. - $query = array_filter( $query ); - ksort( $query ); - - // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.DynamicHooknameFound -- Hook names are defined in child classes. - $query = apply_filters( static::QUERY_FILTER, $query ); - return add_query_arg( $query, static::API_BASE_URL . static::ENDPOINT ); - } - - /** - * Sends a POST request to the Parse.ly Content Suggestion API. - * - * This method sends a POST request to the Parse.ly Content Suggestion API and returns the - * response. The response is either a WP_Error object in case of an error, or a decoded JSON - * object in case of a successful request. - * - * @since 3.13.0 - * - * @param array $query An associative array containing the query - * parameters for the API request. - * @param array> $body An associative array containing the body - * parameters for the API request. - * @return WP_Error|object Returns a WP_Error object in case of an error, or a decoded JSON - * object in case of a successful request. - */ - protected function post_request( array $query = array(), array $body = array() ) { - $full_api_url = $this->get_api_url( $query ); - - /** - * GET request options. - * - * @var WP_HTTP_Request_Args $options - */ - $options = $this->get_request_options(); - if ( count( $body ) > 0 ) { - $body = $this->truncate_array_content( $body ); - - $options['body'] = wp_json_encode( $body ); - if ( false === $options['body'] ) { - return new WP_Error( 400, __( 'Unable to encode request body', 'wp-parsely' ) ); - } - } - - $response = wp_safe_remote_post( $full_api_url, $options ); - if ( is_wp_error( $response ) ) { - return $response; - } - - // Handle any errors returned by the API. - if ( 200 !== $response['response']['code'] ) { - $error = json_decode( wp_remote_retrieve_body( $response ), true ); - - if ( ! is_array( $error ) ) { - return new WP_Error( - 400, - __( 'Unable to decode upstream API error', 'wp-parsely' ) - ); - } - - return new WP_Error( $error['error'], $error['detail'] ); - } - - $body = wp_remote_retrieve_body( $response ); - $decoded = json_decode( $body ); - - if ( ! is_object( $decoded ) ) { - return new WP_Error( 400, __( 'Unable to decode upstream API response', 'wp-parsely' ) ); - } - - return $decoded; - } - - /** - * Truncates the content of an array to a maximum length. - * - * @since 3.14.1 - * - * @param string|array|mixed $content The content to truncate. - * @return string|array|mixed The truncated content. - */ - public function truncate_array_content( $content ) { - if ( is_array( $content ) ) { - // If the content is an array, iterate over its elements. - foreach ( $content as $key => $value ) { - // Recursively process/truncate each element of the array. - $content[ $key ] = $this->truncate_array_content( $value ); - } - return $content; - } elseif ( is_string( $content ) ) { - // If the content is a string, truncate it. - if ( static::TRUNCATE_CONTENT ) { - // Check if the string length exceeds the maximum and truncate if necessary. - if ( mb_strlen( $content ) > self::TRUNCATE_CONTENT_LENGTH ) { - return mb_substr( $content, 0, self::TRUNCATE_CONTENT_LENGTH ); - } - } - return $content; - } - return $content; - } -} diff --git a/src/RemoteAPI/content-suggestions/class-suggest-brief-api.php b/src/RemoteAPI/content-suggestions/class-suggest-brief-api.php deleted file mode 100644 index 7a9ffa7fe9..0000000000 --- a/src/RemoteAPI/content-suggestions/class-suggest-brief-api.php +++ /dev/null @@ -1,73 +0,0 @@ - array( - 'persona' => $persona, - 'style' => $style, - ), - 'title' => $title, - 'text' => wp_strip_all_tags( $content ), - ); - - $decoded = $this->post_request( array(), $body ); - - if ( is_wp_error( $decoded ) ) { - return $decoded; - } - - if ( ! property_exists( $decoded, 'result' ) || - ! is_string( $decoded->result[0] ) ) { - return new WP_Error( - 400, - __( 'Unable to parse meta description from upstream API', 'wp-parsely' ) - ); - } - - return $decoded->result[0]; - } -} diff --git a/src/RemoteAPI/content-suggestions/class-suggest-headline-api.php b/src/RemoteAPI/content-suggestions/class-suggest-headline-api.php deleted file mode 100644 index 3466976636..0000000000 --- a/src/RemoteAPI/content-suggestions/class-suggest-headline-api.php +++ /dev/null @@ -1,69 +0,0 @@ -|WP_Error The response from the remote API, or a WP_Error - * object if the response is an error. - */ - public function get_titles( - string $content, - int $limit, - string $persona = 'journalist', - string $tone = 'neutral' - ) { - $body = array( - 'output_config' => array( - 'persona' => $persona, - 'style' => $tone, - 'max_items' => $limit, - ), - 'text' => wp_strip_all_tags( $content ), - ); - - $decoded = $this->post_request( array(), $body ); - - if ( is_wp_error( $decoded ) ) { - return $decoded; - } - - if ( ! property_exists( $decoded, 'result' ) || ! is_array( $decoded->result ) ) { - return new WP_Error( - 400, - __( 'Unable to parse titles from upstream API', 'wp-parsely' ) - ); - } - - return $decoded->result; - } -} diff --git a/src/RemoteAPI/content-suggestions/class-suggest-linked-reference-api.php b/src/RemoteAPI/content-suggestions/class-suggest-linked-reference-api.php deleted file mode 100644 index 073d401e24..0000000000 --- a/src/RemoteAPI/content-suggestions/class-suggest-linked-reference-api.php +++ /dev/null @@ -1,88 +0,0 @@ - array( - 'max_link_words' => $max_link_words, - 'max_items' => $max_links, - ), - 'text' => wp_strip_all_tags( $content ), - ); - - if ( count( $url_exclusion_list ) > 0 ) { - $body['url_exclusion_list'] = $url_exclusion_list; - } - - $decoded = $this->post_request( array(), $body ); - - if ( is_wp_error( $decoded ) ) { - return $decoded; - } - - if ( ! property_exists( $decoded, 'result' ) || - ! is_array( $decoded->result ) ) { - return new WP_Error( - 400, - __( 'Unable to parse suggested links from upstream API', 'wp-parsely' ) - ); - } - - // Convert the links to Smart_Link objects. - $links = array(); - foreach ( $decoded->result as $link ) { - $link = apply_filters( 'wp_parsely_suggest_linked_reference_link', $link ); - $link_obj = new Smart_Link( - esc_url( $link->canonical_url ), - esc_attr( $link->title ), - wp_kses_post( $link->text ), - $link->offset - ); - $links[] = $link_obj; - } - - return $links; - } -} diff --git a/src/RemoteAPI/interface-cache.php b/src/RemoteAPI/interface-cache.php deleted file mode 100644 index 9b20fa5fc6..0000000000 --- a/src/RemoteAPI/interface-cache.php +++ /dev/null @@ -1,46 +0,0 @@ - $query The query arguments to send to the remote API. - * @param bool $associative (optional) When TRUE, returned objects will be converted into - * associative arrays. - * @return array|object|WP_Error - */ - public function get_items( array $query, bool $associative = false ); - - /** - * Returns whether the endpoint is available for access by the current - * user. - * - * @since 3.14.0 Renamed from `is_user_allowed_to_make_api_call()`. - * @since 3.16.0 Added the `$request` parameter. - * - * @param WP_REST_Request|null $request The request object. - * @return bool - */ - public function is_available_to_current_user( $request = null ): bool; -} diff --git a/src/class-parsely.php b/src/class-parsely.php index 55ce5c5c02..c56c447283 100644 --- a/src/class-parsely.php +++ b/src/class-parsely.php @@ -12,6 +12,7 @@ use Parsely\REST_API\REST_API_Controller; use Parsely\Services\ContentAPI\Content_API_Service; +use Parsely\Services\SuggestionsAPI\Suggestions_API_Service; use Parsely\UI\Metadata_Renderer; use Parsely\UI\Settings_Page; use Parsely\Utils\Utils; @@ -85,10 +86,17 @@ class Parsely { /** * The Content API service. * - * @var Content_API_Service $content_api_service + * @var ?Content_API_Service $content_api_service */ private $content_api_service; + /** + * The Suggestions API service. + * + * @var ?Suggestions_API_Service $suggestions_api_service + */ + private $suggestions_api_service; + /** * The Parse.ly internal REST API controller. * @@ -222,7 +230,6 @@ public function __construct() { $this->are_credentials_managed = $this->are_credentials_managed(); $this->set_managed_options(); - $this->init_external_services(); $this->allow_parsely_remote_requests(); } @@ -255,28 +262,37 @@ public function run(): void { } /** - * Initializes external services. + * Returns the Content API service. * - * This method initializes all the relevant external services for the plugin, such as the - * Content API service, and the Suggestions API service. + * This method returns the Content API service, which is used to interact with the Parse.ly Content API. * * @since 3.17.0 + * + * @return Content_API_Service */ - private function init_external_services(): void { - $this->content_api_service = new Content_API_Service( $this ); + public function get_content_api(): Content_API_Service { + if ( ! isset( $this->content_api_service ) ) { + $this->content_api_service = new Content_API_Service( $this ); + } + + return $this->content_api_service; } /** - * Returns the Content API service. + * Returns the Suggestions API service. * - * This method returns the Content API service, which is used to interact with the Parse.ly Content API. + * This method returns the Suggestions API service, which is used to interact with the Parse.ly Suggestions API. * * @since 3.17.0 * - * @return Content_API_Service + * @return Suggestions_API_Service */ - public function get_content_api(): Content_API_Service { - return $this->content_api_service; + public function get_suggestions_api(): Suggestions_API_Service { + if ( ! isset( $this->suggestions_api_service ) ) { + $this->suggestions_api_service = new Suggestions_API_Service( $this ); + } + + return $this->suggestions_api_service; } /** diff --git a/src/content-helper/post-list-stats/class-post-list-stats.php b/src/content-helper/post-list-stats/class-post-list-stats.php index 91b5f2432e..e7919ca8b4 100644 --- a/src/content-helper/post-list-stats/class-post-list-stats.php +++ b/src/content-helper/post-list-stats/class-post-list-stats.php @@ -12,7 +12,6 @@ use DateTime; use Parsely\Parsely; -use Parsely\RemoteAPI\Base_Endpoint_Remote; use Parsely\Services\ContentAPI\Content_API_Service; use Parsely\Services\ContentAPI\Endpoints\Endpoint_Analytics_Posts; use Parsely\Utils\Utils; @@ -28,7 +27,12 @@ * * @phpstan-import-type Analytics_Posts_API_Params from Endpoint_Analytics_Posts * @phpstan-import-type Analytics_Post from Endpoint_Analytics_Posts - * @phpstan-import-type Remote_API_Error from Base_Endpoint_Remote + * + * @phpstan-type Remote_API_Error array{ + * code: int, + * message: string, + * htmlMessage: string, + * } * * @phpstan-type Parsely_Post_Stats array{ * page_views: string, diff --git a/src/rest-api/content-helper/class-endpoint-excerpt-generator.php b/src/rest-api/content-helper/class-endpoint-excerpt-generator.php index 1adb8d544d..2c7670be0a 100644 --- a/src/rest-api/content-helper/class-endpoint-excerpt-generator.php +++ b/src/rest-api/content-helper/class-endpoint-excerpt-generator.php @@ -11,8 +11,8 @@ namespace Parsely\REST_API\Content_Helper; -use Parsely\RemoteAPI\ContentSuggestions\Suggest_Brief_API; use Parsely\REST_API\Base_Endpoint; +use Parsely\Services\SuggestionsAPI\Suggestions_API_Service; use WP_Error; use WP_REST_Request; use WP_REST_Response; @@ -28,13 +28,13 @@ class Endpoint_Excerpt_Generator extends Base_Endpoint { use Content_Helper_Feature; /** - * The Suggest Brief API instance. + * The Suggestions API service. * * @since 3.17.0 * - * @var Suggest_Brief_API $suggest_brief_api + * @var Suggestions_API_Service $suggestions_api */ - protected $suggest_brief_api; + protected $suggestions_api; /** * Initializes the class. @@ -45,7 +45,7 @@ class Endpoint_Excerpt_Generator extends Base_Endpoint { */ public function __construct( Content_Helper_Controller $controller ) { parent::__construct( $controller ); - $this->suggest_brief_api = new Suggest_Brief_API( $this->parsely ); + $this->suggestions_api = $controller->get_parsely()->get_suggestions_api(); } /** @@ -107,6 +107,18 @@ public function register_routes(): void { 'required' => false, 'default' => 'neutral', ), + 'max_items' => array( + 'description' => __( 'The maximum number of items to generate.', 'wp-parsely' ), + 'type' => 'integer', + 'required' => false, + 'default' => 1, + ), + 'max_characters' => array( + 'description' => __( 'The maximum number of characters to generate.', 'wp-parsely' ), + 'type' => 'integer', + 'required' => false, + 'default' => 160, + ), ) ); } @@ -150,12 +162,38 @@ public function generate_excerpt( WP_REST_Request $request ) { */ $style = $request->get_param( 'style' ); - $response = $this->suggest_brief_api->get_suggestion( $post_title, $post_content, $persona, $style ); + /** + * The maximum number of items to generate. + * + * @var int $max_items + */ + $max_items = $request->get_param( 'max_items' ); + + /** + * The maximum number of characters to generate. + * + * @var int $max_characters + */ + $max_characters = $request->get_param( 'max_characters' ); + + $response = $this->suggestions_api->get_brief_suggestions( + $post_title, + $post_content, + array( + 'persona' => $persona, + 'style' => $style, + 'max_items' => $max_items, + 'max_characters' => $max_characters, + ) + ); if ( is_wp_error( $response ) ) { return $response; } + // TODO: For now, only return the first suggestion. When the UI is ready to handle multiple suggestions, we can + // TODO: return the entire array. + $response = $response[0] ?? ''; return new WP_REST_Response( array( 'data' => $response ), 200 ); } } diff --git a/src/rest-api/content-helper/class-endpoint-smart-linking.php b/src/rest-api/content-helper/class-endpoint-smart-linking.php index cce1c99c43..9882697fd1 100644 --- a/src/rest-api/content-helper/class-endpoint-smart-linking.php +++ b/src/rest-api/content-helper/class-endpoint-smart-linking.php @@ -12,9 +12,9 @@ namespace Parsely\REST_API\Content_Helper; use Parsely\Models\Smart_Link; -use Parsely\RemoteAPI\ContentSuggestions\Suggest_Linked_Reference_API; use Parsely\REST_API\Base_Endpoint; use Parsely\REST_API\Use_Post_ID_Parameter_Trait; +use Parsely\Services\SuggestionsAPI\Suggestions_API_Service; use WP_Error; use WP_Post; use WP_REST_Request; @@ -32,13 +32,13 @@ class Endpoint_Smart_Linking extends Base_Endpoint { use Use_Post_ID_Parameter_Trait; /** - * The Suggest Linked Reference API instance. + * The Suggestions API service. * * @since 3.17.0 * - * @var Suggest_Linked_Reference_API $suggest_linked_reference_api + * @var Suggestions_API_Service $suggestions_api */ - private $suggest_linked_reference_api; + protected $suggestions_api; /** * Initializes the class. @@ -49,7 +49,7 @@ class Endpoint_Smart_Linking extends Base_Endpoint { */ public function __construct( Content_Helper_Controller $controller ) { parent::__construct( $controller ); - $this->suggest_linked_reference_api = new Suggest_Linked_Reference_API( $this->parsely ); + $this->suggestions_api = $controller->get_parsely()->get_suggestions_api(); } /** @@ -225,10 +225,11 @@ public function generate_smart_links( WP_REST_Request $request ) { */ $url_exclusion_list = $request->get_param( 'url_exclusion_list' ) ?? array(); - $response = $this->suggest_linked_reference_api->get_links( + $response = $this->suggestions_api->get_smart_links( $post_content, - 4, // TODO: will be removed after API refactoring. - $max_links, + array( + 'max_items' => $max_links, + ), $url_exclusion_list ); diff --git a/src/rest-api/content-helper/class-endpoint-title-suggestions.php b/src/rest-api/content-helper/class-endpoint-title-suggestions.php index df5717bbaf..c2d25e37e6 100644 --- a/src/rest-api/content-helper/class-endpoint-title-suggestions.php +++ b/src/rest-api/content-helper/class-endpoint-title-suggestions.php @@ -11,8 +11,8 @@ namespace Parsely\REST_API\Content_Helper; -use Parsely\RemoteAPI\ContentSuggestions\Suggest_Headline_API; use Parsely\REST_API\Base_Endpoint; +use Parsely\Services\SuggestionsAPI\Suggestions_API_Service; use WP_Error; use WP_REST_Request; use WP_REST_Response; @@ -28,13 +28,13 @@ class Endpoint_Title_Suggestions extends Base_Endpoint { use Content_Helper_Feature; /** - * The Suggest Headline API. + * The Suggestions API service. * * @since 3.17.0 * - * @var Suggest_Headline_API $suggest_headline_api + * @var Suggestions_API_Service $suggestions_api */ - private $suggest_headline_api; + protected $suggestions_api; /** * Initializes the class. @@ -45,7 +45,7 @@ class Endpoint_Title_Suggestions extends Base_Endpoint { */ public function __construct( Content_Helper_Controller $controller ) { parent::__construct( $controller ); - $this->suggest_headline_api = new Suggest_Headline_API( $this->parsely ); + $this->suggestions_api = $controller->get_parsely()->get_suggestions_api(); } /** @@ -120,7 +120,7 @@ public function register_routes(): void { * @since 3.17.0 * * @param WP_REST_Request $request The request object. - * @return WP_REST_Response|\WP_Error The response object or a WP_Error object on failure. + * @return WP_REST_Response|WP_Error The response object or a WP_Error object on failure. */ public function generate_titles( WP_REST_Request $request ) { /** @@ -155,7 +155,11 @@ public function generate_titles( WP_REST_Request $request ) { $limit = 3; } - $response = $this->suggest_headline_api->get_titles( $post_content, $limit, $persona, $style ); + $response = $this->suggestions_api->get_title_suggestions( $post_content, array( + 'persona' => $persona, + 'style' => $style, + 'max_items' => $limit, + ) ); if ( is_wp_error( $response ) ) { return $response; diff --git a/src/services/class-base-service-endpoint.php b/src/services/class-base-service-endpoint.php index 53dc8b51ff..29cd93344c 100644 --- a/src/services/class-base-service-endpoint.php +++ b/src/services/class-base-service-endpoint.php @@ -7,6 +7,22 @@ use Parsely\Services\Cached_Service_Endpoint; use WP_Error; +/** + * Base class for API service endpoints. + * + * @since 3.17.0 + * + * @phpstan-type WP_HTTP_Response array{ + * headers: array, + * body: string, + * response: array{ + * code: int|false, + * message: string|false, + * }, + * cookies: array, + * http_response: \WP_HTTP_Requests_Response|null, + * } + */ abstract class Base_Service_Endpoint { /** * The API service that this endpoint belongs to. @@ -116,6 +132,7 @@ protected function request( string $method, array $query_args = array(), array $ } } + /** @var WP_HTTP_Response|WP_Error $response */ $response = wp_safe_remote_request( $request_url, $request_options ); return $this->process_response( $response ); @@ -145,7 +162,7 @@ protected function get_endpoint_url( array $query_args = array() ): string { /** * Processes the response from the remote API. * - * @param array|WP_Error $response The response from the remote API. + * @param WP_HTTP_Response|WP_Error $response The response from the remote API. * @return array|WP_Error The processed response. */ protected function process_response( $response ) { diff --git a/src/services/content-api/endpoints/class-content-api-base-endpoint.php b/src/services/content-api/endpoints/class-content-api-base-endpoint.php index eced1d621c..2b9b99a267 100644 --- a/src/services/content-api/endpoints/class-content-api-base-endpoint.php +++ b/src/services/content-api/endpoints/class-content-api-base-endpoint.php @@ -21,6 +21,8 @@ * } * * @phpstan-type Content_API_Response = Content_API_Valid_Response|Content_API_Error_Response + * + * @phpstan-import-type WP_HTTP_Response from Base_Service_Endpoint */ abstract class Content_API_Base_Endpoint extends Base_Service_Endpoint { /** @@ -36,8 +38,6 @@ abstract class Content_API_Base_Endpoint extends Base_Service_Endpoint { protected function get_query_args( array $args = array() ): array { $query_args = parent::get_query_args( $args ); - $query_args['timeout'] = 30; // phpcs:ignore WordPressVIPMinimum.Performance.RemoteRequestTimeout.timeout_timeout - // Set up the API key and secret. $query_args['apikey'] = $this->get_parsely()->get_site_id(); if ( $this->get_parsely()->api_secret_is_set() ) { @@ -50,14 +50,14 @@ protected function get_query_args( array $args = array() ): array { /** * Processes the response from the remote API. * - * @param array|WP_Error $response The response from the remote API. + * @param WP_HTTP_Response|WP_Error $response The response from the remote API. * * @return array|WP_Error The processed response. */ protected function process_response( $response ) { $response = parent::process_response( $response ); - if ( is_wp_error( $response ) || ! is_array( $response ) ) { + if ( is_wp_error( $response ) ) { /** @var WP_Error $response */ return $response; } diff --git a/src/services/suggestions-api/class-suggestions-api-service.php b/src/services/suggestions-api/class-suggestions-api-service.php new file mode 100644 index 0000000000..67f8c9aea5 --- /dev/null +++ b/src/services/suggestions-api/class-suggestions-api-service.php @@ -0,0 +1,104 @@ +register_endpoint( $endpoint ); + } + } + + /** + * Gets the first brief (meta description) for a given content using the + * Parse.ly Content Suggestion API. + * + * @since 3.13.0 + * @since 3.17.0 Updated to use the new API service. + * + * @param string $title The title of the content. + * @param string $content The query arguments to send to the remote API. + * @param Endpoint_Suggest_Brief_Options $options The options to pass to the API request. + * @return array|WP_Error The response from the remote API, or a WP_Error + * object if the response is an error. + */ + public function get_brief_suggestions( string $title, string $content, array $options = array() ) { + /** @var Endpoints\Endpoint_Suggest_Brief $endpoint */ + $endpoint = $this->get_endpoint( '/suggest-brief' ); + + return $endpoint->get_suggestion( $title, $content, $options ); + } + + /** + * Generates titles (headlines) for a given content using the + * Parse.ly Content Suggestion API. + * + * @since 3.13.0 + * @since 3.17.0 Updated to use the new API service. + * + * @param string $content The query arguments to send to the remote API. + * @param Endpoint_Suggest_Headline_Options $options The options to pass to the API request. + * @return array|WP_Error The response from the remote API, or a WP_Error + * object if the response is an error. + */ + public function get_title_suggestions( string $content, array $options = array() ) { + /** @var Endpoints\Endpoint_Suggest_Headline $endpoint */ + $endpoint = $this->get_endpoint( '/suggest-headline' ); + + return $endpoint->get_headlines( $content, $options ); + } + + /** + * Gets suggested smart links for the given content. + * + * @since 3.14.0 + * @since 3.17.0 Updated to use the new API service. + * + * @param string $content The content to generate links for. + * @param array $options The options to pass to the API request. + * @param string[] $url_exclusion_list A list of URLs to exclude from the suggestions. + * + * @return Smart_Link[]|WP_Error The response from the remote API, or a WP_Error + * object if the response is an error. + */ + public function get_smart_links( string $content, array $options = array(), array $url_exclusion_list = array() ) { + /** @var Endpoints\Endpoint_Suggest_Linked_Reference $endpoint */ + $endpoint = $this->get_endpoint( '/suggest-linked-reference' ); + + return $endpoint->get_links( $content, $options, $url_exclusion_list ); + } +} + diff --git a/src/services/suggestions-api/endpoints/class-endpoint-suggest-brief.php b/src/services/suggestions-api/endpoints/class-endpoint-suggest-brief.php new file mode 100644 index 0000000000..075f1f09d9 --- /dev/null +++ b/src/services/suggestions-api/endpoints/class-endpoint-suggest-brief.php @@ -0,0 +1,90 @@ +|WP_Error The response from the remote API, or a WP_Error + * object if the response is an error. + */ + public function get_suggestion( + string $title, + string $content, + array $options = array() + ) { + $request_body = array( + 'output_config' => array( + 'persona' => $options['persona'] ?? 'journalist', + 'style' => $options['style'] ?? 'neutral', + 'max_characters' => $options['max_characters'] ?? 160, + 'max_items' => $options['max_items'] ?? 1, + ), + 'title' => $title, + 'text' => wp_strip_all_tags( $content ), + ); + + /** @var array|WP_Error $response */ + $response = $this->request( 'POST', array(), $request_body ); + + return $response; + } + + /** + * Executes the API request. + * + * @since 3.17.0 + * + * @param array $args The arguments to pass to the API request. + * @return WP_Error|array The response from the API. + */ + public function call( array $args = array() ) { + /** @var string $title */ + $title = $args['title'] ?? ''; + /** @var string $content */ + $content = $args['content'] ?? ''; + /** @var Endpoint_Suggest_Brief_Options $options */ + $options = $args['options'] ?? array(); + + return $this->get_suggestion( $title, $content, $options ); + } + + +} diff --git a/src/services/suggestions-api/endpoints/class-endpoint-suggest-headline.php b/src/services/suggestions-api/endpoints/class-endpoint-suggest-headline.php new file mode 100644 index 0000000000..8331315290 --- /dev/null +++ b/src/services/suggestions-api/endpoints/class-endpoint-suggest-headline.php @@ -0,0 +1,92 @@ + + * } + */ +class Endpoint_Suggest_Headline extends Base_Suggestions_API_Endpoint { + + /** + * Returns the endpoint for the API request. + * + * @since 3.17.0 + * + * @return string The endpoint for the API request. + */ + public function get_endpoint(): string { + return '/suggest-headline'; + } + + /** + * Generates titles (headlines) for a given content using the + * Parse.ly Content Suggestion API. + * + * @since 3.13.0 + * @since 3.17.0 Updated to use the new API service. + * + * @param string $content The query arguments to send to the remote API. + * @param Endpoint_Suggest_Headline_Options $options The options to pass to the API request. + * @return array|WP_Error The response from the remote API, or a WP_Error + * object if the response is an error. + */ + public function get_headlines( + string $content, + array $options = array() + ) { + $request_body = array( + 'output_config' => array( + 'persona' => $options['persona'] ?? 'journalist', + 'style' => $options['style'] ?? 'neutral', + 'max_items' => $options['max_items'] ?? 1, + ), + 'text' => wp_strip_all_tags( $content ), + ); + + /** @var array|WP_Error $response */ + $response = $this->request( 'POST', array(), $request_body ); + + return $response; + } + + /** + * Executes the API request. + * + * @since 3.17.0 + * + * @param array $args The arguments to pass to the API request. + * @return WP_Error|array The response from the API. + */ + public function call( array $args = array() ) { + /** @var string $title */ + $title = $args['title'] ?? ''; + /** @var string $content */ + $content = $args['content'] ?? ''; + /** @var Endpoint_Suggest_Headline_Options $options */ + $options = $args['options'] ?? array(); + + return $this->get_headlines( $content, $options ); + } + + +} diff --git a/src/services/suggestions-api/endpoints/class-endpoint-suggest-linked-reference.php b/src/services/suggestions-api/endpoints/class-endpoint-suggest-linked-reference.php new file mode 100644 index 0000000000..0db88dea4c --- /dev/null +++ b/src/services/suggestions-api/endpoints/class-endpoint-suggest-linked-reference.php @@ -0,0 +1,113 @@ + + * } + */ +class Endpoint_Suggest_Linked_Reference extends Base_Suggestions_API_Endpoint { + + /** + * Returns the endpoint for the API request. + * + * @since 3.17.0 + * + * @return string The endpoint for the API request. + */ + public function get_endpoint(): string { + return '/suggest-linked-reference'; + } + + /** + * Gets suggested smart links for the given content. + * + * @since 3.14.0 + * @since 3.17.0 Updated to use the new API service. + * + * @param string $content The content to generate links for. + * @param Endpoint_Suggest_Linked_Reference_Options $options The options to pass to the API request. + * @param string[] $url_exclusion_list A list of URLs to exclude from the suggestions. + * + * @return Smart_Link[]|WP_Error The response from the remote API, or a WP_Error + * object if the response is an error. + */ + public function get_links( + string $content, + array $options = array(), + array $url_exclusion_list = array() + ) { + $request_body = array( + 'output_config' => array( + 'max_link_words' => $options['max_link_words'] ?? 4, + 'max_items' => $options['max_items'] ?? 10, + ), + 'text' => wp_strip_all_tags( $content ), + ); + + if ( count( $url_exclusion_list ) > 0 ) { + $request_body['url_exclusion_list'] = $url_exclusion_list; + } + + $response = $this->request( 'POST', array(), $request_body ); + + if ( is_wp_error( $response ) ) { + return $response; + } + + // Convert the links to Smart_Link objects. + $links = array(); + foreach ( $response as $link ) { + $link = apply_filters( 'wp_parsely_suggest_linked_reference_link', $link ); + $link_obj = new Smart_Link( + esc_url( $link['canonical_url'] ), + esc_attr( $link['title'] ), + wp_kses_post( $link['text'] ), + $link['offset'] + ); + $links[] = $link_obj; + } + + return $links; + } + + /** + * Executes the API request. + * + * @since 3.17.0 + * + * @param array $args The arguments to pass to the API request. + * @return WP_Error|array The response from the API. + */ + public function call( array $args = array() ) { + /** @var string $content */ + $content = $args['content'] ?? ''; + /** @var Endpoint_Suggest_Linked_Reference_Options $options */ + $options = $args['options'] ?? array(); + /** @var string[] $url_exclusion_list */ + $url_exclusion_list = $args['url_exclusion_list'] ?? array(); + + return $this->get_links( $content, $options, $url_exclusion_list ); + } + + +} diff --git a/src/services/suggestions-api/endpoints/class-suggestions-api-base-endpoint.php b/src/services/suggestions-api/endpoints/class-suggestions-api-base-endpoint.php new file mode 100644 index 0000000000..691d6d83ae --- /dev/null +++ b/src/services/suggestions-api/endpoints/class-suggestions-api-base-endpoint.php @@ -0,0 +1,97 @@ + $method, + 'headers' => array( 'Content-Type' => 'application/json; charset=utf-8' ), + 'data_format' => 'body', + 'timeout' => 60, //phpcs:ignore WordPressVIPMinimum.Performance.RemoteRequestTimeout.timeout_timeout + 'body' => '{}', + ); + + // Add API key to request headers. + if ( $this->get_parsely()->api_secret_is_set() ) { + $options['headers']['X-APIKEY-SECRET'] = $this->get_parsely()->get_api_secret(); + } + + return $options; + } + + /** + * Returns the common query arguments to send to the remote API. + * + * This method append the API key and secret to the query arguments. + * + * @since 3.17.0 + * + * @param array $args Additional query arguments to send to the remote API. + * @return array The query arguments to send to the remote API. + */ + protected function get_query_args( array $args = array() ): array { + $query_args = parent::get_query_args( $args ); + + // Set up the API key and secret. + $query_args['apikey'] = $this->get_parsely()->get_site_id(); + + return $query_args; + } + + /** + * Processes the response from the remote API. + * + * @param WP_HTTP_Response|WP_Error $response The response from the remote API. + * + * @return array|WP_Error The processed response. + */ + protected function process_response( $response ) { + if ( is_wp_error( $response ) ) { + /** @var WP_Error $response */ + return $response; + } + + // Handle any errors returned by the API. + if ( 200 !== $response['response']['code'] ) { + $error = json_decode( wp_remote_retrieve_body( $response ), true ); + + if ( ! is_array( $error ) ) { + return new WP_Error( + 400, + __( 'Unable to decode upstream API error', 'wp-parsely' ) + ); + } + + return new WP_Error( $error['error'], $error['detail'] ); + } + + $body = wp_remote_retrieve_body( $response ); + $decoded = json_decode( $body, true ); + + if ( ! is_array( $decoded ) ) { + return new WP_Error( 400, __( 'Unable to decode upstream API response', 'wp-parsely' ) ); + } + + if ( ! is_array( $decoded['result'] ) ) { + return new WP_Error( 400, __( 'Unable to parse data from upstream API', 'wp-parsely' ) ); + } + + return $decoded['result']; + } + +} From 2338a2c0ddb7f8dca0cadea15db0b73275477213 Mon Sep 17 00:00:00 2001 From: Henrique Mouta Date: Thu, 17 Oct 2024 13:09:48 +0100 Subject: [PATCH 34/49] Address PHPCS & PHPStan errors and warnings --- .phpcs.xml.dist | 3 +- phpstan.neon | 2 +- src/class-parsely.php | 12 +-- src/class-validator.php | 2 +- .../class-dashboard-widget.php | 2 +- .../post-list-stats/class-post-list-stats.php | 2 +- src/rest-api/class-base-api-controller.php | 6 +- .../class-endpoint-excerpt-generator.php | 16 ++-- .../class-endpoint-smart-linking.php | 2 +- .../class-endpoint-title-suggestions.php | 13 ++-- src/rest-api/stats/class-endpoint-post.php | 5 +- src/rest-api/stats/class-endpoint-posts.php | 37 +++++---- src/rest-api/stats/trait-related-posts.php | 4 +- src/services/class-base-api-service.php | 75 ++++++++++++++++++- src/services/class-base-service-endpoint.php | 59 ++++++++++++--- .../class-cached-service-endpoint.php | 44 +++++++++-- .../content-api/class-content-api-service.php | 32 +++++--- .../class-content-api-base-endpoint.php | 11 ++- .../class-endpoint-analytics-post-details.php | 8 +- .../class-endpoint-analytics-posts.php | 16 ++-- .../class-endpoint-referrers-post-detail.php | 8 +- .../endpoints/class-endpoint-related.php | 8 +- .../endpoints/class-endpoint-validate.php | 14 +++- .../class-suggestions-api-service.php | 31 +++++--- .../class-endpoint-suggest-brief.php | 25 ++++--- .../class-endpoint-suggest-headline.php | 20 +++-- ...lass-endpoint-suggest-linked-reference.php | 18 +++-- .../class-suggestions-api-base-endpoint.php | 22 +++++- 28 files changed, 364 insertions(+), 133 deletions(-) diff --git a/.phpcs.xml.dist b/.phpcs.xml.dist index 5d6bf6fa71..7aa088a3bf 100644 --- a/.phpcs.xml.dist +++ b/.phpcs.xml.dist @@ -81,8 +81,7 @@ - - tests/ + / diff --git a/phpstan.neon b/phpstan.neon index 0b43b3f9cd..8f3366c936 100644 --- a/phpstan.neon +++ b/phpstan.neon @@ -14,7 +14,7 @@ parameters: - vendor/php-stubs/wordpress-stubs/wordpress-stubs.php - vendor/php-stubs/wordpress-tests-stubs/wordpress-tests-stubs.php type_coverage: - return_type: 91 + return_type: 90 param_type: 79.2 property_type: 0 # We can't use property types until PHP 7.4 becomes the plugin's minimum version. print_suggestions: false diff --git a/src/class-parsely.php b/src/class-parsely.php index c56c447283..b62f5f6326 100644 --- a/src/class-parsely.php +++ b/src/class-parsely.php @@ -61,12 +61,12 @@ * } * * @phpstan-type WP_HTTP_Request_Args array{ - * method: string, - * timeout: float, - * blocking: bool, - * headers: array, - * body: string, - * data_format: string, + * method?: string, + * timeout?: float, + * blocking?: bool, + * headers?: array, + * body?: string, + * data_format?: string, * } * * @phpstan-import-type Metadata_Attributes from Metadata diff --git a/src/class-validator.php b/src/class-validator.php index 5174dce193..2f0bfe0da0 100644 --- a/src/class-validator.php +++ b/src/class-validator.php @@ -55,7 +55,7 @@ public static function validate_api_credentials( Parsely $parsely, string $site_ } $content_api = $parsely->get_content_api(); - $is_valid = $content_api->validate_credentials( $site_id, $api_secret ); + $is_valid = $content_api->validate_credentials( $site_id, $api_secret ); if ( is_wp_error( $is_valid ) ) { return new WP_Error( diff --git a/src/content-helper/dashboard-widget/class-dashboard-widget.php b/src/content-helper/dashboard-widget/class-dashboard-widget.php index 0df02878f4..afa86d4d3f 100644 --- a/src/content-helper/dashboard-widget/class-dashboard-widget.php +++ b/src/content-helper/dashboard-widget/class-dashboard-widget.php @@ -86,7 +86,7 @@ public function run(): void { * @return bool Whether the Dashboard Widget can be enabled. */ public function can_enable_widget(): bool { - $screen = get_current_screen(); + $screen = get_current_screen(); return $this->can_enable_feature( null !== $screen && 'dashboard' === $screen->id, diff --git a/src/content-helper/post-list-stats/class-post-list-stats.php b/src/content-helper/post-list-stats/class-post-list-stats.php index e7919ca8b4..92d4cee52f 100644 --- a/src/content-helper/post-list-stats/class-post-list-stats.php +++ b/src/content-helper/post-list-stats/class-post-list-stats.php @@ -77,7 +77,7 @@ class Post_List_Stats extends Content_Helper_Feature { * @param Parsely $parsely Instance of Parsely class. */ public function __construct( Parsely $parsely ) { - $this->parsely = $parsely; + $this->parsely = $parsely; $this->content_api = $parsely->get_content_api(); } diff --git a/src/rest-api/class-base-api-controller.php b/src/rest-api/class-base-api-controller.php index 0439757b7a..e3235bdeda 100644 --- a/src/rest-api/class-base-api-controller.php +++ b/src/rest-api/class-base-api-controller.php @@ -185,8 +185,8 @@ public function prefix_route( string $route ): string { * * @since 3.17.0 * - * @param string $endpoint - * @return Base_Endpoint|null + * @param string $endpoint The endpoint name/path. + * @return Base_Endpoint|null The endpoint object, or null if not found. */ protected function get_endpoint( string $endpoint ): ?Base_Endpoint { return $this->endpoints[ $endpoint ] ?? null; @@ -200,5 +200,5 @@ protected function get_endpoint( string $endpoint ): ?Base_Endpoint { * @param string $endpoint The endpoint to check. * @return bool True if the controller is available to the current user, false otherwise. */ - public abstract function is_available_to_current_user( string $endpoint ): bool; + abstract public function is_available_to_current_user( string $endpoint ): bool; } diff --git a/src/rest-api/content-helper/class-endpoint-excerpt-generator.php b/src/rest-api/content-helper/class-endpoint-excerpt-generator.php index 2c7670be0a..6d9461472e 100644 --- a/src/rest-api/content-helper/class-endpoint-excerpt-generator.php +++ b/src/rest-api/content-helper/class-endpoint-excerpt-generator.php @@ -85,29 +85,29 @@ public function register_routes(): void { array( 'POST' ), array( $this, 'generate_excerpt' ), array( - 'text' => array( + 'text' => array( 'description' => __( 'The text to generate the excerpt from.', 'wp-parsely' ), 'type' => 'string', 'required' => true, ), - 'title' => array( + 'title' => array( 'description' => __( 'The title of the content.', 'wp-parsely' ), 'type' => 'string', 'required' => true, ), - 'persona' => array( + 'persona' => array( 'description' => __( 'The persona of the content.', 'wp-parsely' ), 'type' => 'string', 'required' => false, 'default' => 'journalist', ), - 'style' => array( + 'style' => array( 'description' => __( 'The style of the content.', 'wp-parsely' ), 'type' => 'string', 'required' => false, 'default' => 'neutral', ), - 'max_items' => array( + 'max_items' => array( 'description' => __( 'The maximum number of items to generate.', 'wp-parsely' ), 'type' => 'integer', 'required' => false, @@ -180,9 +180,9 @@ public function generate_excerpt( WP_REST_Request $request ) { $post_title, $post_content, array( - 'persona' => $persona, - 'style' => $style, - 'max_items' => $max_items, + 'persona' => $persona, + 'style' => $style, + 'max_items' => $max_items, 'max_characters' => $max_characters, ) ); diff --git a/src/rest-api/content-helper/class-endpoint-smart-linking.php b/src/rest-api/content-helper/class-endpoint-smart-linking.php index 9882697fd1..7c03e0e021 100644 --- a/src/rest-api/content-helper/class-endpoint-smart-linking.php +++ b/src/rest-api/content-helper/class-endpoint-smart-linking.php @@ -228,7 +228,7 @@ public function generate_smart_links( WP_REST_Request $request ) { $response = $this->suggestions_api->get_smart_links( $post_content, array( - 'max_items' => $max_links, + 'max_items' => $max_links, ), $url_exclusion_list ); diff --git a/src/rest-api/content-helper/class-endpoint-title-suggestions.php b/src/rest-api/content-helper/class-endpoint-title-suggestions.php index c2d25e37e6..5efc8aa68d 100644 --- a/src/rest-api/content-helper/class-endpoint-title-suggestions.php +++ b/src/rest-api/content-helper/class-endpoint-title-suggestions.php @@ -155,11 +155,14 @@ public function generate_titles( WP_REST_Request $request ) { $limit = 3; } - $response = $this->suggestions_api->get_title_suggestions( $post_content, array( - 'persona' => $persona, - 'style' => $style, - 'max_items' => $limit, - ) ); + $response = $this->suggestions_api->get_title_suggestions( + $post_content, + array( + 'persona' => $persona, + 'style' => $style, + 'max_items' => $limit, + ) + ); if ( is_wp_error( $response ) ) { return $response; diff --git a/src/rest-api/stats/class-endpoint-post.php b/src/rest-api/stats/class-endpoint-post.php index b0e8c9d888..92deb87356 100644 --- a/src/rest-api/stats/class-endpoint-post.php +++ b/src/rest-api/stats/class-endpoint-post.php @@ -60,7 +60,7 @@ class Endpoint_Post extends Base_Endpoint { */ public function __construct( Stats_Controller $controller ) { parent::__construct( $controller ); - $this->content_api = $this->parsely->get_content_api(); + $this->content_api = $this->parsely->get_content_api(); } /** @@ -183,10 +183,11 @@ public function get_post_details( WP_REST_Request $request ) { } $post_data = array(); + /** * The analytics data object. * - * @var array $analytics_request + * @var array> $analytics_request */ foreach ( $analytics_request as $data ) { $post_data[] = $this->extract_post_data( $data ); diff --git a/src/rest-api/stats/class-endpoint-posts.php b/src/rest-api/stats/class-endpoint-posts.php index dca7789c3e..03a9df722e 100644 --- a/src/rest-api/stats/class-endpoint-posts.php +++ b/src/rest-api/stats/class-endpoint-posts.php @@ -79,7 +79,7 @@ class Endpoint_Posts extends Base_Endpoint { */ public function __construct( Stats_Controller $controller ) { parent::__construct( $controller ); - $this->content_api = $this->parsely->get_content_api(); + $this->content_api = $this->parsely->get_content_api(); } /** @@ -149,23 +149,23 @@ public function register_routes(): void { 'default' => 1, ), 'author' => array( - 'description' => 'Comma-separated list of authors to filter by.', - 'type' => 'string', - 'required' => false, + 'description' => 'Comma-separated list of authors to filter by.', + 'type' => 'string', + 'required' => false, 'validate_callback' => array( $this, 'validate_max_length_is_5' ), 'sanitize_callback' => array( $this, 'sanitize_string_to_array' ), ), 'section' => array( - 'description' => 'Comma-separated list of sections to filter by.', - 'type' => 'string', - 'required' => false, + 'description' => 'Comma-separated list of sections to filter by.', + 'type' => 'string', + 'required' => false, 'validate_callback' => array( $this, 'validate_max_length_is_5' ), 'sanitize_callback' => array( $this, 'sanitize_string_to_array' ), ), 'tag' => array( - 'description' => 'Comma-separated list of tags to filter by.', - 'type' => 'string', - 'required' => false, + 'description' => 'Comma-separated list of tags to filter by.', + 'type' => 'string', + 'required' => false, 'validate_callback' => array( $this, 'validate_max_length_is_5' ), 'sanitize_callback' => array( $this, 'sanitize_string_to_array' ), ), @@ -185,15 +185,15 @@ public function register_routes(): void { * * @since 3.17.0 * - * @param string|array $string The string to sanitize. - * @return array + * @param string|array $str The string to sanitize. + * @return array The sanitized array. */ - public function sanitize_string_to_array( $string ): array { - if ( is_array( $string ) ) { - return $string; + public function sanitize_string_to_array( $str ): array { + if ( is_array( $str ) ) { + return $str; } - return explode( ',', $string ); + return explode( ',', $str ); } /** @@ -260,6 +260,11 @@ public function get_posts( WP_REST_Request $request ) { // Process the data. $posts = array(); + /** + * The analytics data object. + * + * @var array> $analytics_request + */ foreach ( $analytics_request as $item ) { $posts[] = $this->extract_post_data( $item ); } diff --git a/src/rest-api/stats/trait-related-posts.php b/src/rest-api/stats/trait-related-posts.php index 0d98deb66a..949f8e93b2 100644 --- a/src/rest-api/stats/trait-related-posts.php +++ b/src/rest-api/stats/trait-related-posts.php @@ -115,7 +115,7 @@ public function get_related_posts_of_url( WP_REST_Request $request, string $url /** * The raw related posts data, received by the API. * - * @var array|WP_Error $related_posts_request + * @var array|WP_Error $related_posts_request */ $related_posts_request = $this->content_api->get_related_posts_with_url( $url, @@ -140,7 +140,7 @@ public function get_related_posts_of_url( WP_REST_Request $request, string $url $related_posts = array_map( static function ( array $item ) use ( $itm_source ) { - return (object) array( + return array( 'image_url' => $item['image_url'], 'thumb_url_medium' => $item['thumb_url_medium'], 'title' => $item['title'], diff --git a/src/services/class-base-api-service.php b/src/services/class-base-api-service.php index b7c80a24de..3560a5eee6 100644 --- a/src/services/class-base-api-service.php +++ b/src/services/class-base-api-service.php @@ -1,15 +1,31 @@ */ protected $endpoints; @@ -17,10 +33,20 @@ abstract class Base_API_Service { /** * The Parsely instance. * + * @since 3.17.0 + * * @var Parsely */ private $parsely; + + /** + * Initializes the class. + * + * @since 3.17.0 + * + * @param Parsely $parsely The Parsely instance. + */ public function __construct( Parsely $parsely ) { $this->parsely = $parsely; $this->register_endpoints(); @@ -43,23 +69,64 @@ protected function register_endpoint( Base_Service_Endpoint $endpoint ): void { * @since 3.17.0 * * @param Base_Service_Endpoint $endpoint The endpoint to register. - * @param int $ttl The time-to-live for the cache, in seconds. + * @param int $ttl The time-to-live for the cache, in seconds. */ protected function register_cached_endpoint( Base_Service_Endpoint $endpoint, int $ttl ): void { $this->endpoints[ $endpoint->get_endpoint() ] = new Cached_Service_Endpoint( $endpoint, $ttl ); } + /** + * Gets an endpoint by name. + * + * @since 3.17.0 + * + * @param string $endpoint The name of the endpoint. + * @return Base_Service_Endpoint The endpoint. + */ protected function get_endpoint( string $endpoint ): Base_Service_Endpoint { return $this->endpoints[ $endpoint ]; } - public abstract function get_base_url(): string; - protected abstract function register_endpoints(): void; + /** + * Returns the base URL for the API service. + * + * This method should be overridden in the child class to return the base URL + * for the API service. + * + * @since 3.17.0 + * + * @return string + */ + abstract public function get_base_url(): string; + + /** + * Registers the endpoints for the service. + * + * This method should be overridden in the child class to register the + * endpoints for the service. + * + * @since 3.17.0 + */ + abstract protected function register_endpoints(): void; + /** + * Returns the API URL for the service. + * + * @since 3.17.0 + * + * @return string + */ public function get_api_url(): string { return $this->get_base_url(); } + /** + * Returns the Parsely instance. + * + * @since 3.17.0 + * + * @return Parsely + */ public function get_parsely(): Parsely { return $this->parsely; } diff --git a/src/services/class-base-service-endpoint.php b/src/services/class-base-service-endpoint.php index 29cd93344c..8818384295 100644 --- a/src/services/class-base-service-endpoint.php +++ b/src/services/class-base-service-endpoint.php @@ -1,10 +1,16 @@ , * http_response: \WP_HTTP_Requests_Response|null, * } + * + * @phpstan-import-type WP_HTTP_Request_Args from Parsely */ abstract class Base_Service_Endpoint { /** @@ -36,6 +44,7 @@ abstract class Base_Service_Endpoint { * If set to true, the content of the request body will be truncated to a maximum length. * * @since 3.14.1 + * @since 3.17.0 Moved to the Base_Service_Endpoint class. * * @var bool */ @@ -45,25 +54,48 @@ abstract class Base_Service_Endpoint { * The maximum length of the content of the request body. * * @since 3.14.1 + * @since 3.17.0 Moved to the Base_Service_Endpoint class. * * @var int */ protected const TRUNCATE_CONTENT_LENGTH = 25000; + /** + * Initializes the class. + * + * @since 3.17.0 + * + * @param Base_API_Service $api_service The API service that this endpoint belongs to. + */ public function __construct( Base_API_Service $api_service ) { $this->api_service = $api_service; } - protected function get_headers() { + /** + * Returns the headers to send with the request. + * + * @since 3.17.0 + * + * @return array The headers to send with the request. + */ + protected function get_headers(): array { return array( 'Content-Type' => 'application/json', ); } + /** + * Returns the request options for the remote API request. + * + * @since 3.17.0 + * + * @param string $method The HTTP method to use for the request. + * @return WP_HTTP_Request_Args The request options for the remote API request. + */ protected function get_request_options( string $method ): array { $options = array( - 'headers' => $this->get_headers(), - 'method' => $method, + 'headers' => $this->get_headers(), + 'method' => $method, ); return $options; @@ -92,7 +124,7 @@ protected function get_query_args( array $args = array() ): array { * @param array $args The arguments to pass to the API request. * @return WP_Error|array The response from the API. */ - public abstract function call( array $args = array() ); + abstract public function call( array $args = array() ); /** * Returns the endpoint for the API request. @@ -104,14 +136,14 @@ public abstract function call( array $args = array() ); * * @return string The endpoint for the API request. */ - public abstract function get_endpoint(): string; + abstract public function get_endpoint(): string; /** * Sends a request to the remote API. * * @since 3.17.0 * - * @param string $method The HTTP method to use for the request. + * @param string $method The HTTP method to use for the request. * @param array $query_args The query arguments to send to the remote API. * @param array $data The data to send in the request body. * @return WP_Error|array The response from the remote API. @@ -162,6 +194,8 @@ protected function get_endpoint_url( array $query_args = array() ): string { /** * Processes the response from the remote API. * + * @since 3.17.0 + * * @param WP_HTTP_Response|WP_Error $response The response from the remote API. * @return array|WP_Error The processed response. */ @@ -171,7 +205,7 @@ protected function process_response( $response ) { return $response; } - $body = wp_remote_retrieve_body( $response ); + $body = wp_remote_retrieve_body( $response ); $decoded = json_decode( $body, true ); if ( ! is_array( $decoded ) ) { @@ -181,6 +215,13 @@ protected function process_response( $response ) { return $decoded; } + /** + * Returns the Parsely instance. + * + * @since 3.17.0 + * + * @return Parsely The Parsely instance. + */ public function get_parsely(): Parsely { return $this->api_service->get_parsely(); } @@ -189,6 +230,7 @@ public function get_parsely(): Parsely { * Truncates the content of an array to a maximum length. * * @since 3.14.1 + * @since 3.17.0 Moved to the Base_Service_Endpoint class. * * @param string|array|mixed $content The content to truncate. * @return string|array|mixed The truncated content. @@ -213,5 +255,4 @@ private function truncate_array_content( $content ) { } return $content; } - } diff --git a/src/services/class-cached-service-endpoint.php b/src/services/class-cached-service-endpoint.php index 96206993c1..75ac2b3e31 100644 --- a/src/services/class-cached-service-endpoint.php +++ b/src/services/class-cached-service-endpoint.php @@ -1,17 +1,40 @@ service_endpoint = $service_endpoint; - $this->cache_ttl = $cache_ttl; + $this->cache_ttl = $cache_ttl; parent::__construct( $service_endpoint->api_service ); } @@ -39,17 +66,18 @@ public function __construct( Base_Service_Endpoint $service_endpoint, int $cache /** * Returns the cache key for the API request. * - * @param array $args The arguments to pass to the API request. + * @since 3.17.0 * + * @param array $args The arguments to pass to the API request. * @return string The cache key for the API request. */ private function get_cache_key( array $args ): string { $api_service = $this->service_endpoint->api_service; $cache_key = 'parsely_api_' . - wp_hash( $api_service->get_base_url() ) . '_' . - wp_hash( $this->get_endpoint() ) . '_' . - wp_hash( (string) wp_json_encode( $args ) ); + wp_hash( $api_service->get_base_url() ) . '_' . + wp_hash( $this->get_endpoint() ) . '_' . + wp_hash( (string) wp_json_encode( $args ) ); return $cache_key; } @@ -67,7 +95,7 @@ private function get_cache_key( array $args ): string { */ public function call( array $args = array() ) { $cache_key = $this->get_cache_key( $args ); - $cache = wp_cache_get( $cache_key, self::CACHE_GROUP ); + $cache = wp_cache_get( $cache_key, self::CACHE_GROUP ); if ( false !== $cache ) { // @phpstan-ignore-next-line diff --git a/src/services/content-api/class-content-api-service.php b/src/services/content-api/class-content-api-service.php index e610c4a915..bddcb724e2 100644 --- a/src/services/content-api/class-content-api-service.php +++ b/src/services/content-api/class-content-api-service.php @@ -1,4 +1,11 @@ |WP_Error Returns the post details or a WP_Error object in case of an error. @@ -81,9 +95,9 @@ public function get_post_details( $endpoint = $this->get_endpoint( '/analytics/post/detail' ); $args = array( - 'url' => $url, + 'url' => $url, 'period_start' => $period_start, - 'period_end' => $period_end, + 'period_end' => $period_end, ); return $endpoint->call( $args ); @@ -96,7 +110,7 @@ public function get_post_details( * * @link https://docs.parse.ly/api-referrers-endpoint/#3-get-referrers-post-detail * - * @param string $url The URL of the post. + * @param string $url The URL of the post. * @param string|null $period_start The start date of the period to get the data for. * @param string|null $period_end The end date of the period to get the data for. * @return array|WP_Error Returns the referrers or a WP_Error object in case of an error. @@ -110,9 +124,9 @@ public function get_post_referrers( $endpoint = $this->get_endpoint( '/referrers/post/detail' ); $args = array( - 'url' => $url, + 'url' => $url, 'period_start' => $period_start, - 'period_end' => $period_end, + 'period_end' => $period_end, ); return $endpoint->call( $args ); @@ -125,7 +139,7 @@ public function get_post_referrers( * * @link https://docs.parse.ly/content-recommendations/#h-get-related * - * @param string $url The URL of the post. + * @param string $url The URL of the post. * @param array $params The parameters to pass to the API request. * @return array|WP_Error Returns the related posts or a WP_Error object in case of an error. */ @@ -150,7 +164,7 @@ public function get_related_posts_with_url( string $url, array $params = array() * * @link https://docs.parse.ly/content-recommendations/#h-get-related * - * @param string $uuid The UUID of the user. + * @param string $uuid The UUID of the user. * @param array $params The parameters to pass to the API request. * @return array|WP_Error Returns the related posts or a WP_Error object in case of an error. */ diff --git a/src/services/content-api/endpoints/class-content-api-base-endpoint.php b/src/services/content-api/endpoints/class-content-api-base-endpoint.php index 2b9b99a267..7e347a184c 100644 --- a/src/services/content-api/endpoints/class-content-api-base-endpoint.php +++ b/src/services/content-api/endpoints/class-content-api-base-endpoint.php @@ -1,4 +1,11 @@ $response['code'] ?? 400 ) ); } @@ -78,6 +85,4 @@ protected function process_response( $response ) { /** @var Content_API_Valid_Response $response */ return $response['data']; } - - } diff --git a/src/services/content-api/endpoints/class-endpoint-analytics-post-details.php b/src/services/content-api/endpoints/class-endpoint-analytics-post-details.php index ebc1f301c9..14c411babe 100644 --- a/src/services/content-api/endpoints/class-endpoint-analytics-post-details.php +++ b/src/services/content-api/endpoints/class-endpoint-analytics-post-details.php @@ -1,4 +1,11 @@ request( 'GET', $query_args ); } - } diff --git a/src/services/content-api/endpoints/class-endpoint-analytics-posts.php b/src/services/content-api/endpoints/class-endpoint-analytics-posts.php index 3b76f4ea87..09d5b6fe38 100644 --- a/src/services/content-api/endpoints/class-endpoint-analytics-posts.php +++ b/src/services/content-api/endpoints/class-endpoint-analytics-posts.php @@ -1,4 +1,11 @@ self::MAX_RECORDS_LIMIT) { + if ( 'max' === $query_args['limit'] || $query_args['limit'] > self::MAX_RECORDS_LIMIT ) { $query_args['limit'] = self::MAX_RECORDS_LIMIT; } @@ -130,9 +137,9 @@ public function call( array $args = array() ) { * This is required because the Parsely API requires the multiple values for the author, tag, * and section parameters to share the same key. * - * @param string $url The URL to append the parameters to. + * @param string $url The URL to append the parameters to. * @param array $params The parameters to append. - * @param string $param_name The name of the parameter. + * @param string $param_name The name of the parameter. * @return string The URL with the appended parameters. */ protected function append_multiple_params_to_url( string $url, array $params, string $param_name ): string { @@ -146,5 +153,4 @@ protected function append_multiple_params_to_url( string $url, array $params, st } return $url; } - } diff --git a/src/services/content-api/endpoints/class-endpoint-referrers-post-detail.php b/src/services/content-api/endpoints/class-endpoint-referrers-post-detail.php index d10ea16768..1d2164ac91 100644 --- a/src/services/content-api/endpoints/class-endpoint-referrers-post-detail.php +++ b/src/services/content-api/endpoints/class-endpoint-referrers-post-detail.php @@ -1,4 +1,11 @@ request( 'GET', $query_args ); } - } diff --git a/src/services/content-api/endpoints/class-endpoint-related.php b/src/services/content-api/endpoints/class-endpoint-related.php index 68cad37b20..db1b62f637 100644 --- a/src/services/content-api/endpoints/class-endpoint-related.php +++ b/src/services/content-api/endpoints/class-endpoint-related.php @@ -1,4 +1,11 @@ request( 'GET', $args ); } - } diff --git a/src/services/content-api/endpoints/class-endpoint-validate.php b/src/services/content-api/endpoints/class-endpoint-validate.php index c0ddc46f93..4eb38bd150 100644 --- a/src/services/content-api/endpoints/class-endpoint-validate.php +++ b/src/services/content-api/endpoints/class-endpoint-validate.php @@ -1,4 +1,11 @@ |WP_Error $response The response from the remote API. + * @param WP_HTTP_Response|WP_Error $response The response from the remote API. * @return array|WP_Error The processed response. */ protected function process_response( $response ) { @@ -92,9 +101,8 @@ public function call( array $args = array() ) { /** @var string $api_key */ $api_key = $args['apikey']; /** @var string $secret */ - $secret = $args['secret']; + $secret = $args['secret']; return $this->api_validate_credentials( $api_key, $secret ); } - } diff --git a/src/services/suggestions-api/class-suggestions-api-service.php b/src/services/suggestions-api/class-suggestions-api-service.php index 67f8c9aea5..5bb6de940b 100644 --- a/src/services/suggestions-api/class-suggestions-api-service.php +++ b/src/services/suggestions-api/class-suggestions-api-service.php @@ -1,4 +1,11 @@ |WP_Error The response from the remote API, or a WP_Error * object if the response is an error. */ - public function get_brief_suggestions( string $title, string $content, array $options = array() ) { + public function get_brief_suggestions( string $title, string $content, $options = array() ) { /** @var Endpoints\Endpoint_Suggest_Brief $endpoint */ $endpoint = $this->get_endpoint( '/suggest-brief' ); @@ -69,12 +81,12 @@ public function get_brief_suggestions( string $title, string $content, array $op * @since 3.13.0 * @since 3.17.0 Updated to use the new API service. * - * @param string $content The query arguments to send to the remote API. + * @param string $content The query arguments to send to the remote API. * @param Endpoint_Suggest_Headline_Options $options The options to pass to the API request. * @return array|WP_Error The response from the remote API, or a WP_Error * object if the response is an error. */ - public function get_title_suggestions( string $content, array $options = array() ) { + public function get_title_suggestions( string $content, $options = array() ) { /** @var Endpoints\Endpoint_Suggest_Headline $endpoint */ $endpoint = $this->get_endpoint( '/suggest-headline' ); @@ -87,18 +99,17 @@ public function get_title_suggestions( string $content, array $options = array() * @since 3.14.0 * @since 3.17.0 Updated to use the new API service. * - * @param string $content The content to generate links for. - * @param array $options The options to pass to the API request. - * @param string[] $url_exclusion_list A list of URLs to exclude from the suggestions. + * @param string $content The content to generate links for. + * @param Endpoint_Suggest_Linked_Reference_Options $options The options to pass to the API request. + * @param string[] $url_exclusion_list A list of URLs to exclude from the suggestions. * * @return Smart_Link[]|WP_Error The response from the remote API, or a WP_Error * object if the response is an error. */ - public function get_smart_links( string $content, array $options = array(), array $url_exclusion_list = array() ) { + public function get_smart_links( string $content, $options = array(), array $url_exclusion_list = array() ) { /** @var Endpoints\Endpoint_Suggest_Linked_Reference $endpoint */ $endpoint = $this->get_endpoint( '/suggest-linked-reference' ); return $endpoint->get_links( $content, $options, $url_exclusion_list ); } } - diff --git a/src/services/suggestions-api/endpoints/class-endpoint-suggest-brief.php b/src/services/suggestions-api/endpoints/class-endpoint-suggest-brief.php index 075f1f09d9..f10b3c6e31 100644 --- a/src/services/suggestions-api/endpoints/class-endpoint-suggest-brief.php +++ b/src/services/suggestions-api/endpoints/class-endpoint-suggest-brief.php @@ -1,4 +1,11 @@ |WP_Error The response from the remote API, or a WP_Error * object if the response is an error. @@ -48,14 +53,14 @@ public function get_endpoint(): string { public function get_suggestion( string $title, string $content, - array $options = array() + $options = array() ) { $request_body = array( 'output_config' => array( - 'persona' => $options['persona'] ?? 'journalist', - 'style' => $options['style'] ?? 'neutral', + 'persona' => $options['persona'] ?? 'journalist', + 'style' => $options['style'] ?? 'neutral', 'max_characters' => $options['max_characters'] ?? 160, - 'max_items' => $options['max_items'] ?? 1, + 'max_items' => $options['max_items'] ?? 1, ), 'title' => $title, 'text' => wp_strip_all_tags( $content ), @@ -85,6 +90,4 @@ public function call( array $args = array() ) { return $this->get_suggestion( $title, $content, $options ); } - - } diff --git a/src/services/suggestions-api/endpoints/class-endpoint-suggest-headline.php b/src/services/suggestions-api/endpoints/class-endpoint-suggest-headline.php index 8331315290..8a378530c4 100644 --- a/src/services/suggestions-api/endpoints/class-endpoint-suggest-headline.php +++ b/src/services/suggestions-api/endpoints/class-endpoint-suggest-headline.php @@ -1,4 +1,11 @@ * } */ -class Endpoint_Suggest_Headline extends Base_Suggestions_API_Endpoint { - +class Endpoint_Suggest_Headline extends Suggestions_API_Base_Endpoint { /** * Returns the endpoint for the API request. * @@ -45,19 +51,19 @@ public function get_endpoint(): string { * @since 3.13.0 * @since 3.17.0 Updated to use the new API service. * - * @param string $content The query arguments to send to the remote API. + * @param string $content The query arguments to send to the remote API. * @param Endpoint_Suggest_Headline_Options $options The options to pass to the API request. * @return array|WP_Error The response from the remote API, or a WP_Error * object if the response is an error. */ public function get_headlines( string $content, - array $options = array() + $options = array() ) { $request_body = array( 'output_config' => array( - 'persona' => $options['persona'] ?? 'journalist', - 'style' => $options['style'] ?? 'neutral', + 'persona' => $options['persona'] ?? 'journalist', + 'style' => $options['style'] ?? 'neutral', 'max_items' => $options['max_items'] ?? 1, ), 'text' => wp_strip_all_tags( $content ), @@ -87,6 +93,4 @@ public function call( array $args = array() ) { return $this->get_headlines( $content, $options ); } - - } diff --git a/src/services/suggestions-api/endpoints/class-endpoint-suggest-linked-reference.php b/src/services/suggestions-api/endpoints/class-endpoint-suggest-linked-reference.php index 0db88dea4c..a11e6423b0 100644 --- a/src/services/suggestions-api/endpoints/class-endpoint-suggest-linked-reference.php +++ b/src/services/suggestions-api/endpoints/class-endpoint-suggest-linked-reference.php @@ -1,4 +1,11 @@ * } */ -class Endpoint_Suggest_Linked_Reference extends Base_Suggestions_API_Endpoint { - +class Endpoint_Suggest_Linked_Reference extends Suggestions_API_Base_Endpoint { /** * Returns the endpoint for the API request. * @@ -44,16 +50,16 @@ public function get_endpoint(): string { * @since 3.14.0 * @since 3.17.0 Updated to use the new API service. * - * @param string $content The content to generate links for. + * @param string $content The content to generate links for. * @param Endpoint_Suggest_Linked_Reference_Options $options The options to pass to the API request. - * @param string[] $url_exclusion_list A list of URLs to exclude from the suggestions. + * @param string[] $url_exclusion_list A list of URLs to exclude from the suggestions. * * @return Smart_Link[]|WP_Error The response from the remote API, or a WP_Error * object if the response is an error. */ public function get_links( string $content, - array $options = array(), + $options = array(), array $url_exclusion_list = array() ) { $request_body = array( @@ -108,6 +114,4 @@ public function call( array $args = array() ) { return $this->get_links( $content, $options, $url_exclusion_list ); } - - } diff --git a/src/services/suggestions-api/endpoints/class-suggestions-api-base-endpoint.php b/src/services/suggestions-api/endpoints/class-suggestions-api-base-endpoint.php index 691d6d83ae..8aab72c46f 100644 --- a/src/services/suggestions-api/endpoints/class-suggestions-api-base-endpoint.php +++ b/src/services/suggestions-api/endpoints/class-suggestions-api-base-endpoint.php @@ -1,4 +1,11 @@ $method, @@ -93,5 +108,4 @@ protected function process_response( $response ) { return $decoded['result']; } - } From 1c280da15a2720fed63fe377ac29945690e5bd8d Mon Sep 17 00:00:00 2001 From: Henrique Mouta Date: Fri, 18 Oct 2024 09:28:39 +0100 Subject: [PATCH 35/49] Fix post-merge issues and namespaces --- src/class-parsely.php | 4 ++-- .../post-list-stats/class-post-list-stats.php | 4 ++-- .../content-helper/class-endpoint-excerpt-generator.php | 8 ++++---- .../content-helper/class-endpoint-smart-linking.php | 2 +- .../content-helper/class-endpoint-title-suggestions.php | 4 ++-- src/rest-api/stats/class-endpoint-post.php | 2 +- src/rest-api/stats/class-endpoint-posts.php | 2 +- src/rest-api/stats/class-endpoint-related.php | 2 +- src/rest-api/stats/trait-related-posts.php | 2 +- src/services/content-api/class-content-api-service.php | 2 +- .../endpoints/class-content-api-base-endpoint.php | 2 +- .../endpoints/class-endpoint-analytics-post-details.php | 2 +- .../endpoints/class-endpoint-analytics-posts.php | 2 +- .../endpoints/class-endpoint-referrers-post-detail.php | 2 +- .../content-api/endpoints/class-endpoint-related.php | 2 +- .../content-api/endpoints/class-endpoint-validate.php | 2 +- .../suggestions-api/class-suggestions-api-service.php | 2 +- .../endpoints/class-endpoint-suggest-brief.php | 2 +- .../endpoints/class-endpoint-suggest-headline.php | 2 +- .../endpoints/class-endpoint-suggest-linked-reference.php | 2 +- .../endpoints/class-suggestions-api-base-endpoint.php | 2 +- 21 files changed, 27 insertions(+), 27 deletions(-) diff --git a/src/class-parsely.php b/src/class-parsely.php index b62f5f6326..53af69f305 100644 --- a/src/class-parsely.php +++ b/src/class-parsely.php @@ -11,8 +11,8 @@ namespace Parsely; use Parsely\REST_API\REST_API_Controller; -use Parsely\Services\ContentAPI\Content_API_Service; -use Parsely\Services\SuggestionsAPI\Suggestions_API_Service; +use Parsely\Services\Content_API\Content_API_Service; +use Parsely\Services\Suggestions_API\Suggestions_API_Service; use Parsely\UI\Metadata_Renderer; use Parsely\UI\Settings_Page; use Parsely\Utils\Utils; diff --git a/src/content-helper/post-list-stats/class-post-list-stats.php b/src/content-helper/post-list-stats/class-post-list-stats.php index 92d4cee52f..ee0ae4c007 100644 --- a/src/content-helper/post-list-stats/class-post-list-stats.php +++ b/src/content-helper/post-list-stats/class-post-list-stats.php @@ -12,8 +12,8 @@ use DateTime; use Parsely\Parsely; -use Parsely\Services\ContentAPI\Content_API_Service; -use Parsely\Services\ContentAPI\Endpoints\Endpoint_Analytics_Posts; +use Parsely\Services\Content_API\Content_API_Service; +use Parsely\Services\Content_API\Endpoints\Endpoint_Analytics_Posts; use Parsely\Utils\Utils; use WP_Screen; diff --git a/src/rest-api/content-helper/class-endpoint-excerpt-generator.php b/src/rest-api/content-helper/class-endpoint-excerpt-generator.php index 7cb69ec29e..b0f95c18e9 100644 --- a/src/rest-api/content-helper/class-endpoint-excerpt-generator.php +++ b/src/rest-api/content-helper/class-endpoint-excerpt-generator.php @@ -12,7 +12,7 @@ namespace Parsely\REST_API\Content_Helper; use Parsely\REST_API\Base_Endpoint; -use Parsely\Services\SuggestionsAPI\Suggestions_API_Service; +use Parsely\Services\Suggestions_API\Suggestions_API_Service; use WP_Error; use WP_REST_Request; use WP_REST_Response; @@ -55,7 +55,7 @@ public function __construct( Content_Helper_Controller $controller ) { * * @return string The endpoint name. */ - public function get_endpoint_name(): string { + public static function get_endpoint_name(): string { return 'excerpt-generator'; } @@ -95,13 +95,13 @@ public function register_routes(): void { 'type' => 'string', 'required' => true, ), - 'persona' => array( + 'persona' => array( 'description' => __( 'The persona to use for the suggestion.', 'wp-parsely' ), 'type' => 'string', 'required' => false, 'default' => 'journalist', ), - 'style' => array( + 'style' => array( 'description' => __( 'The style to use for the suggestion.', 'wp-parsely' ), 'type' => 'string', 'required' => false, diff --git a/src/rest-api/content-helper/class-endpoint-smart-linking.php b/src/rest-api/content-helper/class-endpoint-smart-linking.php index 657354bd5e..b2d45735fa 100644 --- a/src/rest-api/content-helper/class-endpoint-smart-linking.php +++ b/src/rest-api/content-helper/class-endpoint-smart-linking.php @@ -14,7 +14,7 @@ use Parsely\Models\Smart_Link; use Parsely\REST_API\Base_Endpoint; use Parsely\REST_API\Use_Post_ID_Parameter_Trait; -use Parsely\Services\SuggestionsAPI\Suggestions_API_Service; +use Parsely\Services\Suggestions_API\Suggestions_API_Service; use WP_Error; use WP_Post; use WP_REST_Request; diff --git a/src/rest-api/content-helper/class-endpoint-title-suggestions.php b/src/rest-api/content-helper/class-endpoint-title-suggestions.php index f5a3a89517..b737b7566c 100644 --- a/src/rest-api/content-helper/class-endpoint-title-suggestions.php +++ b/src/rest-api/content-helper/class-endpoint-title-suggestions.php @@ -12,7 +12,7 @@ namespace Parsely\REST_API\Content_Helper; use Parsely\REST_API\Base_Endpoint; -use Parsely\Services\SuggestionsAPI\Suggestions_API_Service; +use Parsely\Services\Suggestions_API\Suggestions_API_Service; use WP_Error; use WP_REST_Request; use WP_REST_Response; @@ -161,7 +161,7 @@ public function generate_titles( WP_REST_Request $request ) { 'persona' => $persona, 'style' => $style, 'max_items' => $limit, - ) + ) ); if ( is_wp_error( $response ) ) { diff --git a/src/rest-api/stats/class-endpoint-post.php b/src/rest-api/stats/class-endpoint-post.php index 041daa4ed5..c861f8d571 100644 --- a/src/rest-api/stats/class-endpoint-post.php +++ b/src/rest-api/stats/class-endpoint-post.php @@ -12,7 +12,7 @@ use Parsely\REST_API\Base_Endpoint; use Parsely\REST_API\Use_Post_ID_Parameter_Trait; -use Parsely\Services\ContentAPI\Content_API_Service; +use Parsely\Services\Content_API\Content_API_Service; use Parsely\Utils\Utils; use stdClass; use WP_Error; diff --git a/src/rest-api/stats/class-endpoint-posts.php b/src/rest-api/stats/class-endpoint-posts.php index cf34ddf049..075f893991 100644 --- a/src/rest-api/stats/class-endpoint-posts.php +++ b/src/rest-api/stats/class-endpoint-posts.php @@ -11,7 +11,7 @@ namespace Parsely\REST_API\Stats; use Parsely\REST_API\Base_Endpoint; -use Parsely\Services\ContentAPI\Content_API_Service; +use Parsely\Services\Content_API\Content_API_Service; use WP_Error; use WP_REST_Request; use WP_REST_Response; diff --git a/src/rest-api/stats/class-endpoint-related.php b/src/rest-api/stats/class-endpoint-related.php index 5bba4608b2..1f4526c8f2 100644 --- a/src/rest-api/stats/class-endpoint-related.php +++ b/src/rest-api/stats/class-endpoint-related.php @@ -11,7 +11,7 @@ namespace Parsely\REST_API\Stats; use Parsely\REST_API\Base_Endpoint; -use Parsely\Services\ContentAPI\Content_API_Service; +use Parsely\Services\Content_API\Content_API_Service; use WP_Error; use WP_REST_Request; use WP_REST_Response; diff --git a/src/rest-api/stats/trait-related-posts.php b/src/rest-api/stats/trait-related-posts.php index 949f8e93b2..b0528b4698 100644 --- a/src/rest-api/stats/trait-related-posts.php +++ b/src/rest-api/stats/trait-related-posts.php @@ -11,7 +11,7 @@ namespace Parsely\REST_API\Stats; use Parsely\Parsely; -use Parsely\Services\ContentAPI\Content_API_Service; +use Parsely\Services\Content_API\Content_API_Service; use stdClass; use WP_Error; use WP_REST_Request; diff --git a/src/services/content-api/class-content-api-service.php b/src/services/content-api/class-content-api-service.php index bddcb724e2..c44e71b24a 100644 --- a/src/services/content-api/class-content-api-service.php +++ b/src/services/content-api/class-content-api-service.php @@ -8,7 +8,7 @@ declare(strict_types=1); -namespace Parsely\Services\ContentAPI; +namespace Parsely\Services\Content_API; use Parsely\Services\Base_API_Service; use Parsely\Services\Base_Service_Endpoint; diff --git a/src/services/content-api/endpoints/class-content-api-base-endpoint.php b/src/services/content-api/endpoints/class-content-api-base-endpoint.php index 7e347a184c..05eec2b902 100644 --- a/src/services/content-api/endpoints/class-content-api-base-endpoint.php +++ b/src/services/content-api/endpoints/class-content-api-base-endpoint.php @@ -8,7 +8,7 @@ declare(strict_types=1); -namespace Parsely\Services\ContentAPI\Endpoints; +namespace Parsely\Services\Content_API\Endpoints; use Parsely\Services\Base_Service_Endpoint; use WP_Error; diff --git a/src/services/content-api/endpoints/class-endpoint-analytics-post-details.php b/src/services/content-api/endpoints/class-endpoint-analytics-post-details.php index 14c411babe..145abb03cf 100644 --- a/src/services/content-api/endpoints/class-endpoint-analytics-post-details.php +++ b/src/services/content-api/endpoints/class-endpoint-analytics-post-details.php @@ -8,7 +8,7 @@ declare(strict_types=1); -namespace Parsely\Services\ContentAPI\Endpoints; +namespace Parsely\Services\Content_API\Endpoints; use WP_Error; diff --git a/src/services/content-api/endpoints/class-endpoint-analytics-posts.php b/src/services/content-api/endpoints/class-endpoint-analytics-posts.php index 09d5b6fe38..5329124654 100644 --- a/src/services/content-api/endpoints/class-endpoint-analytics-posts.php +++ b/src/services/content-api/endpoints/class-endpoint-analytics-posts.php @@ -8,7 +8,7 @@ declare(strict_types=1); -namespace Parsely\Services\ContentAPI\Endpoints; +namespace Parsely\Services\Content_API\Endpoints; use WP_Error; diff --git a/src/services/content-api/endpoints/class-endpoint-referrers-post-detail.php b/src/services/content-api/endpoints/class-endpoint-referrers-post-detail.php index 1d2164ac91..134ae8873d 100644 --- a/src/services/content-api/endpoints/class-endpoint-referrers-post-detail.php +++ b/src/services/content-api/endpoints/class-endpoint-referrers-post-detail.php @@ -8,7 +8,7 @@ declare(strict_types=1); -namespace Parsely\Services\ContentAPI\Endpoints; +namespace Parsely\Services\Content_API\Endpoints; use WP_Error; diff --git a/src/services/content-api/endpoints/class-endpoint-related.php b/src/services/content-api/endpoints/class-endpoint-related.php index db1b62f637..22f7616aff 100644 --- a/src/services/content-api/endpoints/class-endpoint-related.php +++ b/src/services/content-api/endpoints/class-endpoint-related.php @@ -8,7 +8,7 @@ declare(strict_types=1); -namespace Parsely\Services\ContentAPI\Endpoints; +namespace Parsely\Services\Content_API\Endpoints; use WP_Error; diff --git a/src/services/content-api/endpoints/class-endpoint-validate.php b/src/services/content-api/endpoints/class-endpoint-validate.php index 4eb38bd150..c7693dfe4b 100644 --- a/src/services/content-api/endpoints/class-endpoint-validate.php +++ b/src/services/content-api/endpoints/class-endpoint-validate.php @@ -8,7 +8,7 @@ declare(strict_types=1); -namespace Parsely\Services\ContentAPI\Endpoints; +namespace Parsely\Services\Content_API\Endpoints; use Parsely\Services\Base_Service_Endpoint; use WP_Error; diff --git a/src/services/suggestions-api/class-suggestions-api-service.php b/src/services/suggestions-api/class-suggestions-api-service.php index 5bb6de940b..4ee6c48686 100644 --- a/src/services/suggestions-api/class-suggestions-api-service.php +++ b/src/services/suggestions-api/class-suggestions-api-service.php @@ -8,7 +8,7 @@ declare(strict_types=1); -namespace Parsely\Services\SuggestionsAPI; +namespace Parsely\Services\Suggestions_API; use Parsely\Models\Smart_Link; use Parsely\Services\Base_API_Service; diff --git a/src/services/suggestions-api/endpoints/class-endpoint-suggest-brief.php b/src/services/suggestions-api/endpoints/class-endpoint-suggest-brief.php index f10b3c6e31..7091ce87b4 100644 --- a/src/services/suggestions-api/endpoints/class-endpoint-suggest-brief.php +++ b/src/services/suggestions-api/endpoints/class-endpoint-suggest-brief.php @@ -8,7 +8,7 @@ declare(strict_types=1); -namespace Parsely\Services\SuggestionsAPI\Endpoints; +namespace Parsely\Services\Suggestions_API\Endpoints; use WP_Error; diff --git a/src/services/suggestions-api/endpoints/class-endpoint-suggest-headline.php b/src/services/suggestions-api/endpoints/class-endpoint-suggest-headline.php index 8a378530c4..a796e05a31 100644 --- a/src/services/suggestions-api/endpoints/class-endpoint-suggest-headline.php +++ b/src/services/suggestions-api/endpoints/class-endpoint-suggest-headline.php @@ -8,7 +8,7 @@ declare(strict_types=1); -namespace Parsely\Services\SuggestionsAPI\Endpoints; +namespace Parsely\Services\Suggestions_API\Endpoints; use WP_Error; diff --git a/src/services/suggestions-api/endpoints/class-endpoint-suggest-linked-reference.php b/src/services/suggestions-api/endpoints/class-endpoint-suggest-linked-reference.php index a11e6423b0..27294f06ce 100644 --- a/src/services/suggestions-api/endpoints/class-endpoint-suggest-linked-reference.php +++ b/src/services/suggestions-api/endpoints/class-endpoint-suggest-linked-reference.php @@ -8,7 +8,7 @@ declare(strict_types=1); -namespace Parsely\Services\SuggestionsAPI\Endpoints; +namespace Parsely\Services\Suggestions_API\Endpoints; use Parsely\Models\Smart_Link; use WP_Error; diff --git a/src/services/suggestions-api/endpoints/class-suggestions-api-base-endpoint.php b/src/services/suggestions-api/endpoints/class-suggestions-api-base-endpoint.php index 8aab72c46f..776bc96f76 100644 --- a/src/services/suggestions-api/endpoints/class-suggestions-api-base-endpoint.php +++ b/src/services/suggestions-api/endpoints/class-suggestions-api-base-endpoint.php @@ -8,7 +8,7 @@ declare(strict_types=1); -namespace Parsely\Services\SuggestionsAPI\Endpoints; +namespace Parsely\Services\Suggestions_API\Endpoints; use Parsely\Services\Base_Service_Endpoint; use WP_Error; From e103011c9a62160fa24c766699161ab8bbee5a7d Mon Sep 17 00:00:00 2001 From: Henrique Mouta Date: Fri, 18 Oct 2024 09:38:11 +0100 Subject: [PATCH 36/49] Add condition when validate api credentials returns false. --- src/UI/class-settings-page.php | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/UI/class-settings-page.php b/src/UI/class-settings-page.php index 993f84ff66..b4011eed63 100644 --- a/src/UI/class-settings-page.php +++ b/src/UI/class-settings-page.php @@ -1217,7 +1217,10 @@ private function validate_basic_section( $input ): array { $valid_credentials = true; } - if ( is_wp_error( $valid_credentials ) && Validator::INVALID_API_CREDENTIALS === $valid_credentials->get_error_code() ) { + if ( + ( is_wp_error( $valid_credentials ) && Validator::INVALID_API_CREDENTIALS === $valid_credentials->get_error_code() ) || + ( is_bool( $valid_credentials ) && ! $valid_credentials ) + ) { add_settings_error( Parsely::OPTIONS_KEY, 'api_secret', From ccaa9404a4d62e6b92684f37f70665f380dd9b2a Mon Sep 17 00:00:00 2001 From: Henrique Mouta Date: Fri, 18 Oct 2024 09:59:47 +0100 Subject: [PATCH 37/49] Address code review feedback --- .../post-list-stats/class-post-list-stats.php | 4 +-- .../class-endpoint-excerpt-generator.php | 3 +-- src/rest-api/stats/class-endpoint-posts.php | 4 ++- .../class-endpoint-analytics-posts.php | 27 ++++++++++++++++--- 4 files changed, 29 insertions(+), 9 deletions(-) diff --git a/src/content-helper/post-list-stats/class-post-list-stats.php b/src/content-helper/post-list-stats/class-post-list-stats.php index ee0ae4c007..9ea12d9a47 100644 --- a/src/content-helper/post-list-stats/class-post-list-stats.php +++ b/src/content-helper/post-list-stats/class-post-list-stats.php @@ -301,10 +301,10 @@ public function get_parsely_stats_response(): ?array { $response = $this->content_api->get_posts( array( - 'period_start' => 'max_days', + 'period_start' => Endpoint_Analytics_Posts::MAX_PERIOD, 'pub_date_start' => $date_params['pub_date_start'] ?? '', 'pub_date_end' => $date_params['pub_date_end'] ?? '', - 'limit' => 'max', + 'limit' => Endpoint_Analytics_Posts::MAX_LIMIT, 'sort' => 'avg_engaged', // Note: API sends different stats on different sort options. ) ); diff --git a/src/rest-api/content-helper/class-endpoint-excerpt-generator.php b/src/rest-api/content-helper/class-endpoint-excerpt-generator.php index b0f95c18e9..921a7fd435 100644 --- a/src/rest-api/content-helper/class-endpoint-excerpt-generator.php +++ b/src/rest-api/content-helper/class-endpoint-excerpt-generator.php @@ -191,8 +191,7 @@ public function generate_excerpt( WP_REST_Request $request ) { return $response; } - // TODO: For now, only return the first suggestion. When the UI is ready to handle multiple suggestions, we can - // TODO: return the entire array. + // TODO: For now, only return the first suggestion. When the UI is ready to handle multiple suggestions, we can return the entire array. $response = $response[0] ?? ''; return new WP_REST_Response( array( 'data' => $response ), 200 ); } diff --git a/src/rest-api/stats/class-endpoint-posts.php b/src/rest-api/stats/class-endpoint-posts.php index 075f893991..d399a23779 100644 --- a/src/rest-api/stats/class-endpoint-posts.php +++ b/src/rest-api/stats/class-endpoint-posts.php @@ -16,6 +16,7 @@ use WP_REST_Request; use WP_REST_Response; use stdClass; +use const _PHPStan_4f7beffdf\__; /** * The Stats API Posts endpoint. @@ -210,7 +211,7 @@ public function validate_max_length_is_5( $string_or_array ) { } if ( count( $string_or_array ) > 5 ) { - return new WP_Error( 'invalid_param', 'The parameter must have at most 5 items.' ); + return new WP_Error( 'invalid_param', __( 'The parameter must have at most 5 items.', 'wp-parsely' ) ); } return true; @@ -260,6 +261,7 @@ public function get_posts( WP_REST_Request $request ) { // Process the data. $posts = array(); + /** * The analytics data object. * diff --git a/src/services/content-api/endpoints/class-endpoint-analytics-posts.php b/src/services/content-api/endpoints/class-endpoint-analytics-posts.php index 5329124654..910d5b76b8 100644 --- a/src/services/content-api/endpoints/class-endpoint-analytics-posts.php +++ b/src/services/content-api/endpoints/class-endpoint-analytics-posts.php @@ -53,8 +53,27 @@ * } */ class Endpoint_Analytics_Posts extends Content_API_Base_Endpoint { - public const MAX_RECORDS_LIMIT = 2000; - public const ANALYTICS_API_DAYS_LIMIT = 7; + private const MAX_RECORDS_LIMIT = 2000; + private const ANALYTICS_API_DAYS_LIMIT = 7; + + /** + * Maximum limit for the number of records to return, to be + * used in the `limit` parameter. + * + * @since 3.17.0 + * + * @var string + */ + public const MAX_LIMIT = 'max'; + + /** + * Maximum period for the API request, to be used in the `period_start` parameter. + * + * @since 3.17.0 + * + * @var string + */ + public const MAX_PERIOD = 'max_days'; /** * Returns the endpoint for the API request. @@ -117,13 +136,13 @@ public function call( array $args = array() ) { $query_args = array_filter( $args ); // If the period_start is set to 'max_days', set it to the maximum days limit. - if ( 'max_days' === $query_args['period_start'] ) { + if ( self::MAX_PERIOD === $query_args['period_start'] ) { $query_args['period_start'] = self::ANALYTICS_API_DAYS_LIMIT . 'd'; } // If the limit is set to 'max' or greater than the maximum records limit, // set it to the maximum records limit. - if ( 'max' === $query_args['limit'] || $query_args['limit'] > self::MAX_RECORDS_LIMIT ) { + if ( self::MAX_LIMIT === $query_args['limit'] || $query_args['limit'] > self::MAX_RECORDS_LIMIT ) { $query_args['limit'] = self::MAX_RECORDS_LIMIT; } From be67fa906690d594f1d8605453be622a021d1913 Mon Sep 17 00:00:00 2001 From: Alex Cicovic <23142906+acicovic@users.noreply.github.com> Date: Fri, 18 Oct 2024 16:22:08 +0300 Subject: [PATCH 38/49] Tests: Add PHP 8.4 and move PCOV to PHP 8.2 --- .github/workflows/integration-tests.yml | 31 +++++++++---------------- .github/workflows/unit-tests.yml | 15 ++++++------ 2 files changed, 18 insertions(+), 28 deletions(-) diff --git a/.github/workflows/integration-tests.yml b/.github/workflows/integration-tests.yml index e40393ded3..1dd9181d97 100644 --- a/.github/workflows/integration-tests.yml +++ b/.github/workflows/integration-tests.yml @@ -23,34 +23,25 @@ jobs: WP_VERSION: ${{ matrix.wp }} strategy: - # PHP 7.2 uses PHPUnit 8.5.21 - # PHP 7.3 uses PHPUnit 9.5.10 - # PHP 7.4 uses PHPUnit 9.5.10 - # PHP 8.0 uses PHPUnit 9.5.10 - # PHP 8.1 uses PHPUnit 9.5.10 - # PHP 8.2 uses PHPUnit 9.5.10 + # Note: Different PHP versions use different PHPUnit versions. # Keys: # - coverage: Whether to run the tests with code coverage. # - experimental: Whether the build is "allowed to fail". matrix: - php: [ '7.2', '7.3', '7.4', '8.0'] + php: [ '7.2', '7.3', '7.4', '8.0', '8.1', '8.3'] wp: ['latest'] coverage: [none] experimental: [false] include: - - php: '8.1' - coverage: pcov + - php: '8.2' + coverage: pcov # Warning: PCOV might not work for PHP 8.4 or newer according to https://thephp.cc/articles/pcov-or-xdebug. extensions: pcov ini-values: pcov.directory=., "pcov.exclude=\"~(vendor|tests)~\"" experimental: false - - php: '8.2' - wp: 'trunk' - coverage: none - experimental: false - - php: '8.3' + - php: '8.4' wp: 'trunk' coverage: none - experimental: false + experimental: true fail-fast: false continue-on-error: ${{ matrix.experimental }} steps: @@ -92,17 +83,17 @@ jobs: run: composer prepare-ci --no-interaction - name: Run integration tests (single site) - if: ${{ matrix.php != 8.0 && ! matrix.experimental }} + if: ${{ matrix.php != 8.2 && ! matrix.experimental }} run: composer testwp --no-interaction - - name: Run integration tests experimental + - name: Run integration tests (single site) experimental if: ${{ matrix.experimental }} run: composer testwp-experimental --no-interaction - - name: Run integration tests (multi site) - if: ${{ matrix.php != 8.0 && ! matrix.experimental }} + - name: Run integration tests (multisite) + if: ${{ matrix.php != 8.2 && ! matrix.experimental }} run: composer testwp-ms --no-interaction - name: Run integration tests (multisite site with code coverage) - if: ${{ matrix.php == 8.1 }} + if: ${{ matrix.php == 8.2 }} # To prevent unneeded test runs, use this version in the above if clauses. run: composer coveragewp-ci --no-interaction diff --git a/.github/workflows/unit-tests.yml b/.github/workflows/unit-tests.yml index e5b8b79f3c..840881a882 100644 --- a/.github/workflows/unit-tests.yml +++ b/.github/workflows/unit-tests.yml @@ -25,25 +25,24 @@ jobs: WP_VERSION: latest strategy: - # PHP 7.2 uses PHPUnit 8.5.21 - # PHP 7.3 uses PHPUnit 9.5.10 - # PHP 7.4 uses PHPUnit 9.5.10 - # PHP 8.0 uses PHPUnit 9.5.10 - # PHP 8.1 uses PHPUnit 9.5.10 + # Note: Different PHP versions use different PHPUnit versions. # Keys: # - coverage: Whether to run the tests with code coverage. # - experimental: Whether the build is "allowed to fail". matrix: - php: ['7.2', '7.3', '7.4', '8.0', '8.2', '8.3'] + php: ['7.2', '7.3', '7.4', '8.0', '8.1', '8.3'] coverage: [none] experimental: [false] include: # Run code coverage for only one PHP version - - php: '8.1' - coverage: pcov + - php: '8.2' + coverage: pcov # Warning: PCOV might not work for PHP 8.4 or newer according to https://thephp.cc/articles/pcov-or-xdebug. extensions: pcov ini-values: pcov.directory=., "pcov.exclude=\"~(vendor|tests)~\"" experimental: false + - php: '8.4' + coverage: none + experimental: true fail-fast: false continue-on-error: ${{ matrix.experimental }} steps: From 096bf59eff80654b20396bfdcdf2c9f77d43cadd Mon Sep 17 00:00:00 2001 From: Alex Cicovic <23142906+acicovic@users.noreply.github.com> Date: Fri, 18 Oct 2024 19:08:45 +0300 Subject: [PATCH 39/49] Attempt to fix PHP 8.4 deprecation errors --- src/class-parsely.php | 2 +- .../content-helper/trait-content-helper-feature.php | 2 +- src/services/content-api/class-content-api-service.php | 8 ++++---- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/class-parsely.php b/src/class-parsely.php index 53af69f305..b0b458ab66 100644 --- a/src/class-parsely.php +++ b/src/class-parsely.php @@ -640,7 +640,7 @@ public function set_default_content_helper_settings_values(): void { * for context (Default null for current). * @return string */ - public static function get_settings_url( int $_blog_id = null ): string { + public static function get_settings_url( ?int $_blog_id = null ): string { return get_admin_url( $_blog_id, 'options-general.php?page=' . self::MENU_SLUG ); } diff --git a/src/rest-api/content-helper/trait-content-helper-feature.php b/src/rest-api/content-helper/trait-content-helper-feature.php index 383e7c2ce4..ab028ce885 100644 --- a/src/rest-api/content-helper/trait-content-helper-feature.php +++ b/src/rest-api/content-helper/trait-content-helper-feature.php @@ -56,7 +56,7 @@ protected function is_pch_feature_enabled_for_user(): bool { * @param WP_REST_Request|null $request The request object. * @return bool|WP_Error True if the endpoint is available. */ - public function is_available_to_current_user( WP_REST_Request $request = null ) { + public function is_available_to_current_user( ?WP_REST_Request $request = null ) { $can_use_feature = $this->is_pch_feature_enabled_for_user(); if ( ! $can_use_feature ) { diff --git a/src/services/content-api/class-content-api-service.php b/src/services/content-api/class-content-api-service.php index c44e71b24a..0f29187cb3 100644 --- a/src/services/content-api/class-content-api-service.php +++ b/src/services/content-api/class-content-api-service.php @@ -88,8 +88,8 @@ protected function register_endpoints(): void { */ public function get_post_details( string $url, - string $period_start = null, - string $period_end = null + ?string $period_start = null, + ?string $period_end = null ) { /** @var Endpoints\Endpoint_Analytics_Post_Details $endpoint */ $endpoint = $this->get_endpoint( '/analytics/post/detail' ); @@ -117,8 +117,8 @@ public function get_post_details( */ public function get_post_referrers( string $url, - string $period_start = null, - string $period_end = null + ?string $period_start = null, + ?string $period_end = null ) { /** @var Endpoints\Endpoint_Referrers_Post_Detail $endpoint */ $endpoint = $this->get_endpoint( '/referrers/post/detail' ); From a39172073193b208d1d63e4f723ea348c0dd37e2 Mon Sep 17 00:00:00 2001 From: Alex Cicovic <23142906+acicovic@users.noreply.github.com> Date: Mon, 21 Oct 2024 11:10:47 +0300 Subject: [PATCH 40/49] Adjust some DocBlocks and whitespace --- src/rest-api/stats/class-endpoint-posts.php | 1 - src/services/class-base-service-endpoint.php | 3 +++ .../content-api/class-content-api-service.php | 1 - .../class-content-api-base-endpoint.php | 3 ++- .../class-endpoint-analytics-post-details.php | 6 +++++- .../class-endpoint-analytics-posts.php | 17 +++++++++++------ .../class-endpoint-referrers-post-detail.php | 6 +++++- .../endpoints/class-endpoint-related.php | 6 +++++- .../endpoints/class-endpoint-validate.php | 10 +++++++--- .../class-suggestions-api-service.php | 8 ++++---- .../endpoints/class-endpoint-suggest-brief.php | 2 +- .../class-endpoint-suggest-headline.php | 6 +++--- .../class-endpoint-suggest-linked-reference.php | 10 +++++----- .../class-suggestions-api-base-endpoint.php | 5 +++-- 14 files changed, 54 insertions(+), 30 deletions(-) diff --git a/src/rest-api/stats/class-endpoint-posts.php b/src/rest-api/stats/class-endpoint-posts.php index d399a23779..0671b9e5e8 100644 --- a/src/rest-api/stats/class-endpoint-posts.php +++ b/src/rest-api/stats/class-endpoint-posts.php @@ -16,7 +16,6 @@ use WP_REST_Request; use WP_REST_Response; use stdClass; -use const _PHPStan_4f7beffdf\__; /** * The Stats API Posts endpoint. diff --git a/src/services/class-base-service-endpoint.php b/src/services/class-base-service-endpoint.php index 8818384295..5f1808f7ce 100644 --- a/src/services/class-base-service-endpoint.php +++ b/src/services/class-base-service-endpoint.php @@ -35,12 +35,15 @@ abstract class Base_Service_Endpoint { /** * The API service that this endpoint belongs to. * + * @since 3.17.0 + * * @var Base_API_Service */ protected $api_service; /** * Flag to truncate the content of the request body. + * * If set to true, the content of the request body will be truncated to a maximum length. * * @since 3.14.1 diff --git a/src/services/content-api/class-content-api-service.php b/src/services/content-api/class-content-api-service.php index c44e71b24a..6970fae24b 100644 --- a/src/services/content-api/class-content-api-service.php +++ b/src/services/content-api/class-content-api-service.php @@ -22,7 +22,6 @@ * @since 3.17.0 */ class Content_API_Service extends Base_API_Service { - /** * Returns the base URL for the Parse.ly Content API, aka Public API. * diff --git a/src/services/content-api/endpoints/class-content-api-base-endpoint.php b/src/services/content-api/endpoints/class-content-api-base-endpoint.php index 05eec2b902..4a4692fac0 100644 --- a/src/services/content-api/endpoints/class-content-api-base-endpoint.php +++ b/src/services/content-api/endpoints/class-content-api-base-endpoint.php @@ -57,8 +57,9 @@ protected function get_query_args( array $args = array() ): array { /** * Processes the response from the remote API. * - * @param WP_HTTP_Response|WP_Error $response The response from the remote API. + * @since 3.17.0 * + * @param WP_HTTP_Response|WP_Error $response The response from the remote API. * @return array|WP_Error The processed response. */ protected function process_response( $response ) { diff --git a/src/services/content-api/endpoints/class-endpoint-analytics-post-details.php b/src/services/content-api/endpoints/class-endpoint-analytics-post-details.php index 145abb03cf..54d26d3ae9 100644 --- a/src/services/content-api/endpoints/class-endpoint-analytics-post-details.php +++ b/src/services/content-api/endpoints/class-endpoint-analytics-post-details.php @@ -13,7 +13,7 @@ use WP_Error; /** - * The endpoint for the analytics post detail API request. + * The endpoint for the /analytics/post/detail API request. * * @since 3.17.0 * @@ -23,6 +23,8 @@ class Endpoint_Analytics_Post_Details extends Content_API_Base_Endpoint { /** * Returns the endpoint for the API request. * + * @since 3.17.0 + * * @return string */ public function get_endpoint(): string { @@ -32,6 +34,8 @@ public function get_endpoint(): string { /** * Executes the API request. * + * @since 3.17.0 + * * @param array $args The arguments to pass to the API request. * @return WP_Error|array The response from the API request. */ diff --git a/src/services/content-api/endpoints/class-endpoint-analytics-posts.php b/src/services/content-api/endpoints/class-endpoint-analytics-posts.php index 910d5b76b8..e08f482f68 100644 --- a/src/services/content-api/endpoints/class-endpoint-analytics-posts.php +++ b/src/services/content-api/endpoints/class-endpoint-analytics-posts.php @@ -13,7 +13,7 @@ use WP_Error; /** - * The endpoint for the analytics/posts API request. + * The endpoint for the /analytics/posts API request. * * @since 3.17.0 * @@ -58,7 +58,7 @@ class Endpoint_Analytics_Posts extends Content_API_Base_Endpoint { /** * Maximum limit for the number of records to return, to be - * used in the `limit` parameter. + * used in the limit parameter. * * @since 3.17.0 * @@ -67,7 +67,7 @@ class Endpoint_Analytics_Posts extends Content_API_Base_Endpoint { public const MAX_LIMIT = 'max'; /** - * Maximum period for the API request, to be used in the `period_start` parameter. + * Maximum period for the API request, to be used in the period_start parameter. * * @since 3.17.0 * @@ -89,9 +89,9 @@ public function get_endpoint(): string { /** * Returns the endpoint URL for the API request. * - * This method appends the author, tag, and section parameters to the endpoint URL, - * if they are set. Since Parsely API needs the multiple values for these parameters to share - * the same key, we need to append them manually. + * This method appends the author, tag, and section parameters to the + * endpoint URL, if they are set. Since the Parse.ly API needs a key for + * every value (e.g. tag=tag1&tag=tag2), we need to append them manually. * * @since 3.17.0 * @@ -128,6 +128,8 @@ protected function get_endpoint_url( array $args = array() ): string { /** * Executes the API request. * + * @since 3.17.0 + * * @param array $args The arguments to pass to the API request. * @return WP_Error|array The response from the API request. */ @@ -156,6 +158,8 @@ public function call( array $args = array() ) { * This is required because the Parsely API requires the multiple values for the author, tag, * and section parameters to share the same key. * + * @since 3.17.0 + * * @param string $url The URL to append the parameters to. * @param array $params The parameters to append. * @param string $param_name The name of the parameter. @@ -170,6 +174,7 @@ protected function append_multiple_params_to_url( string $url, array $params, st $url .= '&' . $param_name . '=' . $param; } } + return $url; } } diff --git a/src/services/content-api/endpoints/class-endpoint-referrers-post-detail.php b/src/services/content-api/endpoints/class-endpoint-referrers-post-detail.php index 134ae8873d..1c233feddd 100644 --- a/src/services/content-api/endpoints/class-endpoint-referrers-post-detail.php +++ b/src/services/content-api/endpoints/class-endpoint-referrers-post-detail.php @@ -13,7 +13,7 @@ use WP_Error; /** - * The endpoint for the referrers post detail API request. + * The endpoint for the /referrers/post/detail API request. * * @since 3.17.0 * @@ -23,6 +23,8 @@ class Endpoint_Referrers_Post_Detail extends Content_API_Base_Endpoint { /** * Returns the endpoint for the API request. * + * @since 3.17.0 + * * @return string */ public function get_endpoint(): string { @@ -32,6 +34,8 @@ public function get_endpoint(): string { /** * Executes the API request. * + * @since 3.17.0 + * * @param array $args The arguments to pass to the API request. * @return WP_Error|array The response from the API request. */ diff --git a/src/services/content-api/endpoints/class-endpoint-related.php b/src/services/content-api/endpoints/class-endpoint-related.php index 22f7616aff..cd547ae843 100644 --- a/src/services/content-api/endpoints/class-endpoint-related.php +++ b/src/services/content-api/endpoints/class-endpoint-related.php @@ -13,7 +13,7 @@ use WP_Error; /** - * The endpoint for the referrers post detail API request. + * The endpoint for the /related API request. * * @since 3.17.0 * @@ -23,6 +23,8 @@ class Endpoint_Related extends Content_API_Base_Endpoint { /** * Returns the endpoint for the API request. * + * @since 3.17.0 + * * @return string */ public function get_endpoint(): string { @@ -32,6 +34,8 @@ public function get_endpoint(): string { /** * Executes the API request. * + * @since 3.17.0 + * * @param array $args The arguments to pass to the API request. * @return WP_Error|array The response from the API request. */ diff --git a/src/services/content-api/endpoints/class-endpoint-validate.php b/src/services/content-api/endpoints/class-endpoint-validate.php index c7693dfe4b..8167c837df 100644 --- a/src/services/content-api/endpoints/class-endpoint-validate.php +++ b/src/services/content-api/endpoints/class-endpoint-validate.php @@ -35,7 +35,8 @@ public function get_endpoint(): string { /** * Returns the query arguments for the API request. * - * We want to validate the API key and secret, so we don't need to send any query arguments. + * We want to validate the API key and secret, so we don't need to send any + * query arguments. * * @since 3.17.0 * @@ -48,7 +49,9 @@ public function get_query_args( array $args = array() ): array { /** * Queries the Parse.ly API credentials validation endpoint. - * The API will return a 200 response if the credentials are valid and a 403 response if they are not. + * + * The API will return a 200 response if the credentials are valid and a 403 + * response if they are not. * * @since 3.17.0 * @@ -68,7 +71,6 @@ private function api_validate_credentials( string $api_key, string $secret_key ) return $response; } - if ( false === $response['success'] ) { return new WP_Error( $response['code'] ?? 403, @@ -94,6 +96,8 @@ protected function process_response( $response ) { /** * Executes the API request. * + * @since 3.17.0 + * * @param array $args The arguments to pass to the API request. * @return WP_Error|array The response from the API request. */ diff --git a/src/services/suggestions-api/class-suggestions-api-service.php b/src/services/suggestions-api/class-suggestions-api-service.php index 4ee6c48686..d6417e6bf6 100644 --- a/src/services/suggestions-api/class-suggestions-api-service.php +++ b/src/services/suggestions-api/class-suggestions-api-service.php @@ -75,8 +75,8 @@ public function get_brief_suggestions( string $title, string $content, $options } /** - * Generates titles (headlines) for a given content using the - * Parse.ly Content Suggestion API. + * Gets titles (headlines) for a given content using the Parse.ly Content + * Suggestion API. * * @since 3.13.0 * @since 3.17.0 Updated to use the new API service. @@ -101,9 +101,9 @@ public function get_title_suggestions( string $content, $options = array() ) { * * @param string $content The content to generate links for. * @param Endpoint_Suggest_Linked_Reference_Options $options The options to pass to the API request. - * @param string[] $url_exclusion_list A list of URLs to exclude from the suggestions. + * @param array $url_exclusion_list A list of URLs to exclude from the suggestions. * - * @return Smart_Link[]|WP_Error The response from the remote API, or a WP_Error + * @return array|WP_Error The response from the remote API, or a WP_Error * object if the response is an error. */ public function get_smart_links( string $content, $options = array(), array $url_exclusion_list = array() ) { diff --git a/src/services/suggestions-api/endpoints/class-endpoint-suggest-brief.php b/src/services/suggestions-api/endpoints/class-endpoint-suggest-brief.php index 7091ce87b4..8e0d3a20cc 100644 --- a/src/services/suggestions-api/endpoints/class-endpoint-suggest-brief.php +++ b/src/services/suggestions-api/endpoints/class-endpoint-suggest-brief.php @@ -13,7 +13,7 @@ use WP_Error; /** - * The endpoint for the suggest-brief API request. + * The endpoint for the /suggest-brief API request. * * @since 3.17.0 * diff --git a/src/services/suggestions-api/endpoints/class-endpoint-suggest-headline.php b/src/services/suggestions-api/endpoints/class-endpoint-suggest-headline.php index a796e05a31..09b3e7c27c 100644 --- a/src/services/suggestions-api/endpoints/class-endpoint-suggest-headline.php +++ b/src/services/suggestions-api/endpoints/class-endpoint-suggest-headline.php @@ -13,7 +13,7 @@ use WP_Error; /** - * The endpoint for the suggest headline API request. + * The endpoint for the /suggest-headline API request. * * @since 3.17.0 * @@ -45,8 +45,8 @@ public function get_endpoint(): string { } /** - * Generates titles (headlines) for a given content using the - * Parse.ly Content Suggestion API. + * Gets titles (headlines) for a given content using the Parse.ly Content + * Suggestion API. * * @since 3.13.0 * @since 3.17.0 Updated to use the new API service. diff --git a/src/services/suggestions-api/endpoints/class-endpoint-suggest-linked-reference.php b/src/services/suggestions-api/endpoints/class-endpoint-suggest-linked-reference.php index 27294f06ce..cb27e628b4 100644 --- a/src/services/suggestions-api/endpoints/class-endpoint-suggest-linked-reference.php +++ b/src/services/suggestions-api/endpoints/class-endpoint-suggest-linked-reference.php @@ -18,7 +18,7 @@ * * @since 3.17.0 * - * @link https://content-suggestions-api.parsely.net/prod/docs#/default/suggest_headline_suggest_headline_post + * @link https://content-suggestions-api.parsely.net/prod/docs#/default/suggest_linked_reference_suggest_linked_reference_post * * @phpstan-type Traffic_Source = array{ * source: string, @@ -45,16 +45,16 @@ public function get_endpoint(): string { } /** - * Gets suggested smart links for the given content. + * Gets suggested smart links for the given content using the Parse.ly + * Content Suggestion API. * * @since 3.14.0 * @since 3.17.0 Updated to use the new API service. * * @param string $content The content to generate links for. * @param Endpoint_Suggest_Linked_Reference_Options $options The options to pass to the API request. - * @param string[] $url_exclusion_list A list of URLs to exclude from the suggestions. - * - * @return Smart_Link[]|WP_Error The response from the remote API, or a WP_Error + * @param array $url_exclusion_list A list of URLs to exclude from the suggestions. + * @return array|WP_Error The response from the remote API, or a WP_Error * object if the response is an error. */ public function get_links( diff --git a/src/services/suggestions-api/endpoints/class-suggestions-api-base-endpoint.php b/src/services/suggestions-api/endpoints/class-suggestions-api-base-endpoint.php index 776bc96f76..daac090961 100644 --- a/src/services/suggestions-api/endpoints/class-suggestions-api-base-endpoint.php +++ b/src/services/suggestions-api/endpoints/class-suggestions-api-base-endpoint.php @@ -52,7 +52,7 @@ protected function get_request_options( string $method ): array { /** * Returns the common query arguments to send to the remote API. * - * This method append the API key and secret to the query arguments. + * This method appends the API key and secret to the query arguments. * * @since 3.17.0 * @@ -71,8 +71,9 @@ protected function get_query_args( array $args = array() ): array { /** * Processes the response from the remote API. * - * @param WP_HTTP_Response|WP_Error $response The response from the remote API. + * @since 3.17.0 * + * @param WP_HTTP_Response|WP_Error $response The response from the remote API. * @return array|WP_Error The processed response. */ protected function process_response( $response ) { From 97f6d25a459cf328c850566fbde0114903264344 Mon Sep 17 00:00:00 2001 From: Henrique Mouta Date: Mon, 21 Oct 2024 10:44:19 +0100 Subject: [PATCH 41/49] Make the services base URL static, and remove the old consts --- src/UI/class-recommended-widget.php | 4 +- src/class-parsely.php | 19 ++++----- src/services/class-base-api-service.php | 6 +-- src/services/class-base-service-endpoint.php | 42 +++++++++---------- .../content-api/class-content-api-service.php | 2 +- .../class-endpoint-analytics-posts.php | 20 ++++----- .../class-suggestions-api-service.php | 2 +- 7 files changed, 47 insertions(+), 48 deletions(-) diff --git a/src/UI/class-recommended-widget.php b/src/UI/class-recommended-widget.php index b72bd2e5e3..25f8fca2e1 100644 --- a/src/UI/class-recommended-widget.php +++ b/src/UI/class-recommended-widget.php @@ -92,7 +92,7 @@ public function __construct( Parsely $parsely ) { * @return string API URL. */ private function get_api_url( string $site_id, ?int $published_within, ?string $sort, int $return_limit ): string { - $related_api_endpoint = Parsely::PUBLIC_API_BASE_URL . '/related'; + $related_api_endpoint = $this->parsely->get_content_api()->get_endpoint( '/related' ); $query_args = array( 'apikey' => $site_id, @@ -104,7 +104,7 @@ private function get_api_url( string $site_id, ?int $published_within, ?string $ $query_args['pub_date_start'] = $published_within . 'd'; } - return add_query_arg( $query_args, $related_api_endpoint ); + return $related_api_endpoint->get_endpoint_url( $query_args ); } /** diff --git a/src/class-parsely.php b/src/class-parsely.php index 53af69f305..a7a9bee5ec 100644 --- a/src/class-parsely.php +++ b/src/class-parsely.php @@ -75,13 +75,11 @@ class Parsely { /** * Declare our constants */ - public const VERSION = PARSELY_VERSION; - public const MENU_SLUG = 'parsely'; // The page param passed to options-general.php. - public const OPTIONS_KEY = 'parsely'; // The key used to store options in the WP database. - public const CAPABILITY = 'manage_options'; // The capability required to administer settings. - public const DASHBOARD_BASE_URL = 'https://dash.parsely.com'; - public const PUBLIC_API_BASE_URL = 'https://api.parsely.com/v2'; - public const PUBLIC_SUGGESTIONS_API_BASE_URL = 'https://content-suggestions-api.parsely.net/prod'; + public const VERSION = PARSELY_VERSION; + public const MENU_SLUG = 'parsely'; // The page param passed to options-general.php. + public const OPTIONS_KEY = 'parsely'; // The key used to store options in the WP database. + public const CAPABILITY = 'manage_options'; // The capability required to administer settings. + public const DASHBOARD_BASE_URL = 'https://dash.parsely.com'; /** * The Content API service. @@ -459,7 +457,8 @@ public function update_metadata_endpoint( int $post_id ): void { 'tags' => $metadata['keywords'] ?? '', ); - $parsely_api_endpoint = self::PUBLIC_API_BASE_URL . '/metadata/posts'; + $parsely_api_base_url = Content_API_Service::get_base_url(); + $parsely_api_endpoint = $parsely_api_base_url . '/metadata/posts'; $parsely_metadata_secret = $parsely_options['metadata_secret']; $headers = array( 'Content-Type' => 'application/json' ); @@ -1013,8 +1012,8 @@ private function sanitize_managed_option( string $option_id, $value ) { private function allow_parsely_remote_requests(): void { $allowed_urls = array( self::DASHBOARD_BASE_URL, - self::PUBLIC_API_BASE_URL, - self::PUBLIC_SUGGESTIONS_API_BASE_URL, + Content_API_Service::get_base_url(), + Suggestions_API_Service::get_base_url(), ); add_filter( diff --git a/src/services/class-base-api-service.php b/src/services/class-base-api-service.php index 3560a5eee6..4a1697e68f 100644 --- a/src/services/class-base-api-service.php +++ b/src/services/class-base-api-service.php @@ -83,7 +83,7 @@ protected function register_cached_endpoint( Base_Service_Endpoint $endpoint, in * @param string $endpoint The name of the endpoint. * @return Base_Service_Endpoint The endpoint. */ - protected function get_endpoint( string $endpoint ): Base_Service_Endpoint { + public function get_endpoint( string $endpoint ): Base_Service_Endpoint { return $this->endpoints[ $endpoint ]; } @@ -97,7 +97,7 @@ protected function get_endpoint( string $endpoint ): Base_Service_Endpoint { * * @return string */ - abstract public function get_base_url(): string; + abstract public static function get_base_url(): string; /** * Registers the endpoints for the service. @@ -117,7 +117,7 @@ abstract protected function register_endpoints(): void; * @return string */ public function get_api_url(): string { - return $this->get_base_url(); + return static::get_base_url(); } /** diff --git a/src/services/class-base-service-endpoint.php b/src/services/class-base-service-endpoint.php index 5f1808f7ce..ef5245d9c0 100644 --- a/src/services/class-base-service-endpoint.php +++ b/src/services/class-base-service-endpoint.php @@ -141,6 +141,27 @@ abstract public function call( array $args = array() ); */ abstract public function get_endpoint(): string; + /** + * Returns the full URL for the API request, including the endpoint and query arguments. + * + * @since 3.17.0 + * + * @param array $query_args The query arguments to send to the remote API. + * @return string The full URL for the API request. + */ + public function get_endpoint_url( array $query_args = array() ): string { + // Get the base URL from the API service. + $base_url = $this->api_service->get_api_url(); + + // Append the endpoint to the base URL. + $base_url .= $this->get_endpoint(); + + // Append any necessary query arguments. + $endpoint = add_query_arg( $this->get_query_args( $query_args ), $base_url ); + + return $endpoint; + } + /** * Sends a request to the remote API. * @@ -173,27 +194,6 @@ protected function request( string $method, array $query_args = array(), array $ return $this->process_response( $response ); } - /** - * Returns the full URL for the API request, including the endpoint and query arguments. - * - * @since 3.17.0 - * - * @param array $query_args The query arguments to send to the remote API. - * @return string The full URL for the API request. - */ - protected function get_endpoint_url( array $query_args = array() ): string { - // Get the base URL from the API service. - $base_url = $this->api_service->get_api_url(); - - // Append the endpoint to the base URL. - $base_url .= $this->get_endpoint(); - - // Append any necessary query arguments. - $endpoint = add_query_arg( $this->get_query_args( $query_args ), $base_url ); - - return $endpoint; - } - /** * Processes the response from the remote API. * diff --git a/src/services/content-api/class-content-api-service.php b/src/services/content-api/class-content-api-service.php index 6970fae24b..b2e165a085 100644 --- a/src/services/content-api/class-content-api-service.php +++ b/src/services/content-api/class-content-api-service.php @@ -29,7 +29,7 @@ class Content_API_Service extends Base_API_Service { * * @return string */ - public function get_base_url(): string { + public static function get_base_url(): string { return 'https://api.parsely.com/v2'; } diff --git a/src/services/content-api/endpoints/class-endpoint-analytics-posts.php b/src/services/content-api/endpoints/class-endpoint-analytics-posts.php index e08f482f68..90ef1aeb5f 100644 --- a/src/services/content-api/endpoints/class-endpoint-analytics-posts.php +++ b/src/services/content-api/endpoints/class-endpoint-analytics-posts.php @@ -93,29 +93,29 @@ public function get_endpoint(): string { * endpoint URL, if they are set. Since the Parse.ly API needs a key for * every value (e.g. tag=tag1&tag=tag2), we need to append them manually. * - * @since 3.17.0 + * @param array $query_args The arguments to pass to the API request. * - * @param array $args The arguments to pass to the API request. * @return string The endpoint URL for the API request. + * @since 3.17.0 */ - protected function get_endpoint_url( array $args = array() ): string { + public function get_endpoint_url( array $query_args = array() ): string { // Store the author, tag, and section parameters. /** @var array $authors */ - $authors = $args['author'] ?? array(); + $authors = $query_args['author'] ?? array(); /** @var array $tags */ - $tags = $args['tag'] ?? array(); + $tags = $query_args['tag'] ?? array(); /** @var array $sections */ - $sections = $args['section'] ?? array(); + $sections = $query_args['section'] ?? array(); // Remove the author, tag, and section parameters from the query args. - unset( $args['author'] ); - unset( $args['tag'] ); - unset( $args['section'] ); + unset( $query_args['author'] ); + unset( $query_args['tag'] ); + unset( $query_args['section'] ); // Generate the endpoint URL. - $endpoint_url = parent::get_endpoint_url( $args ); + $endpoint_url = parent::get_endpoint_url( $query_args ); // Append the author, tag, and section parameters to the endpoint URL. $endpoint_url = $this->append_multiple_params_to_url( $endpoint_url, $authors, 'author' ); diff --git a/src/services/suggestions-api/class-suggestions-api-service.php b/src/services/suggestions-api/class-suggestions-api-service.php index d6417e6bf6..8f72454671 100644 --- a/src/services/suggestions-api/class-suggestions-api-service.php +++ b/src/services/suggestions-api/class-suggestions-api-service.php @@ -33,7 +33,7 @@ class Suggestions_API_Service extends Base_API_Service { * * @return string */ - public function get_base_url(): string { + public static function get_base_url(): string { return 'https://content-suggestions-api.parsely.net/prod'; } From eb61fda03a53472bc2b1e164c7d9ae71d73cd4b3 Mon Sep 17 00:00:00 2001 From: Henrique Mouta Date: Mon, 21 Oct 2024 11:00:22 +0100 Subject: [PATCH 42/49] Fix PHPStan (not including tests) --- src/rest-api/class-base-endpoint.php | 2 +- src/rest-api/stats/trait-related-posts.php | 2 +- src/services/class-cached-service-endpoint.php | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/rest-api/class-base-endpoint.php b/src/rest-api/class-base-endpoint.php index 1b28558291..ecea575a6d 100644 --- a/src/rest-api/class-base-endpoint.php +++ b/src/rest-api/class-base-endpoint.php @@ -196,7 +196,7 @@ public function get_full_endpoint( string $route = '' ): string { * @return string */ public function get_endpoint_slug(): string { - return $this->api_controller->prefix_route( '' ) . $this->get_endpoint_name(); + return $this->api_controller->prefix_route( '' ) . static::get_endpoint_name(); } /** diff --git a/src/rest-api/stats/trait-related-posts.php b/src/rest-api/stats/trait-related-posts.php index b0528b4698..c165388de6 100644 --- a/src/rest-api/stats/trait-related-posts.php +++ b/src/rest-api/stats/trait-related-posts.php @@ -115,7 +115,7 @@ public function get_related_posts_of_url( WP_REST_Request $request, string $url /** * The raw related posts data, received by the API. * - * @var array|WP_Error $related_posts_request + * @var array>|WP_Error $related_posts_request */ $related_posts_request = $this->content_api->get_related_posts_with_url( $url, diff --git a/src/services/class-cached-service-endpoint.php b/src/services/class-cached-service-endpoint.php index 75ac2b3e31..fc19d15615 100644 --- a/src/services/class-cached-service-endpoint.php +++ b/src/services/class-cached-service-endpoint.php @@ -75,7 +75,7 @@ private function get_cache_key( array $args ): string { $api_service = $this->service_endpoint->api_service; $cache_key = 'parsely_api_' . - wp_hash( $api_service->get_base_url() ) . '_' . + wp_hash( $api_service->get_api_url() ) . '_' . wp_hash( $this->get_endpoint() ) . '_' . wp_hash( (string) wp_json_encode( $args ) ); From 11a0ae2be6efa9781672e0ee25cf93d26fc98e2e Mon Sep 17 00:00:00 2001 From: Henrique Mouta Date: Mon, 21 Oct 2024 11:05:38 +0100 Subject: [PATCH 43/49] Fix misplaced `@since` tag --- .../content-api/endpoints/class-endpoint-analytics-posts.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/services/content-api/endpoints/class-endpoint-analytics-posts.php b/src/services/content-api/endpoints/class-endpoint-analytics-posts.php index 90ef1aeb5f..3c280397cc 100644 --- a/src/services/content-api/endpoints/class-endpoint-analytics-posts.php +++ b/src/services/content-api/endpoints/class-endpoint-analytics-posts.php @@ -93,10 +93,10 @@ public function get_endpoint(): string { * endpoint URL, if they are set. Since the Parse.ly API needs a key for * every value (e.g. tag=tag1&tag=tag2), we need to append them manually. * - * @param array $query_args The arguments to pass to the API request. + * @since 3.17.0 * + * @param array $query_args The arguments to pass to the API request. * @return string The endpoint URL for the API request. - * @since 3.17.0 */ public function get_endpoint_url( array $query_args = array() ): string { // Store the author, tag, and section parameters. From 0a653e88d1b85045b5bf0aa6baf99a45d74c20fa Mon Sep 17 00:00:00 2001 From: Henrique Mouta Date: Mon, 21 Oct 2024 11:22:48 +0100 Subject: [PATCH 44/49] PCH Refactor: Refactor the external services (#2873) * Implement the new Content API service, and remove old RemoteAPI code * Implement caching mechanism for service endpoints * Implement the Suggestions API service * Address PHPCS & PHPStan errors and warnings * Fix post-merge issues and namespaces * Add condition when validate api credentials returns false. * Address code review feedback * Adjust some DocBlocks and whitespace * Make the services base URL static, and remove the old consts * Fix PHPStan (not including tests) * Fix misplaced `@since` tag --------- Co-authored-by: Alex Cicovic <23142906+acicovic@users.noreply.github.com> --- .phpcs.xml.dist | 4 +- phpstan.neon | 2 +- src/Endpoints/class-base-endpoint.php | 194 ------------- .../class-analytics-post-detail-api.php | 45 --- src/RemoteAPI/class-analytics-posts-api.php | 107 ------- src/RemoteAPI/class-base-endpoint-remote.php | 153 ---------- .../class-referrers-post-detail-api.php | 45 --- src/RemoteAPI/class-related-api.php | 39 --- src/RemoteAPI/class-remote-api-cache.php | 91 ------ src/RemoteAPI/class-validate-api.php | 124 --------- src/RemoteAPI/class-wordpress-cache.php | 52 ---- .../class-content-suggestions-base-api.php | 207 -------------- .../class-suggest-brief-api.php | 73 ----- .../class-suggest-headline-api.php | 69 ----- .../class-suggest-linked-reference-api.php | 88 ------ src/RemoteAPI/interface-cache.php | 46 --- src/RemoteAPI/interface-remote-api.php | 41 --- src/UI/class-recommended-widget.php | 4 +- src/UI/class-settings-page.php | 7 +- src/class-parsely.php | 106 +++++-- src/class-validator.php | 15 +- .../class-dashboard-widget.php | 11 +- .../post-list-stats/class-post-list-stats.php | 61 ++-- src/rest-api/class-base-api-controller.php | 26 +- src/rest-api/class-base-endpoint.php | 16 ++ src/rest-api/class-rest-api-controller.php | 51 ++++ .../class-endpoint-excerpt-generator.php | 57 +++- .../class-endpoint-smart-linking.php | 17 +- .../class-endpoint-title-suggestions.php | 21 +- src/rest-api/stats/class-endpoint-post.php | 54 ++-- src/rest-api/stats/class-endpoint-posts.php | 97 ++++--- src/rest-api/stats/class-endpoint-related.php | 13 +- src/rest-api/stats/trait-post-data.php | 36 +-- src/rest-api/stats/trait-related-posts.php | 28 +- src/services/class-base-api-service.php | 133 +++++++++ src/services/class-base-service-endpoint.php | 261 ++++++++++++++++++ .../class-cached-service-endpoint.php | 124 +++++++++ .../content-api/class-content-api-service.php | 234 ++++++++++++++++ .../class-content-api-base-endpoint.php | 89 ++++++ .../class-endpoint-analytics-post-details.php | 54 ++++ .../class-endpoint-analytics-posts.php | 180 ++++++++++++ .../class-endpoint-referrers-post-detail.php | 54 ++++ .../endpoints/class-endpoint-related.php | 53 ++++ .../endpoints/class-endpoint-validate.php | 112 ++++++++ .../class-suggestions-api-service.php | 115 ++++++++ .../class-endpoint-suggest-brief.php | 93 +++++++ .../class-endpoint-suggest-headline.php | 96 +++++++ ...lass-endpoint-suggest-linked-reference.php | 117 ++++++++ .../class-suggestions-api-base-endpoint.php | 112 ++++++++ wp-parsely.php | 6 +- 50 files changed, 2256 insertions(+), 1577 deletions(-) delete mode 100644 src/Endpoints/class-base-endpoint.php delete mode 100644 src/RemoteAPI/class-analytics-post-detail-api.php delete mode 100644 src/RemoteAPI/class-analytics-posts-api.php delete mode 100644 src/RemoteAPI/class-base-endpoint-remote.php delete mode 100644 src/RemoteAPI/class-referrers-post-detail-api.php delete mode 100644 src/RemoteAPI/class-related-api.php delete mode 100644 src/RemoteAPI/class-remote-api-cache.php delete mode 100644 src/RemoteAPI/class-validate-api.php delete mode 100644 src/RemoteAPI/class-wordpress-cache.php delete mode 100644 src/RemoteAPI/content-suggestions/class-content-suggestions-base-api.php delete mode 100644 src/RemoteAPI/content-suggestions/class-suggest-brief-api.php delete mode 100644 src/RemoteAPI/content-suggestions/class-suggest-headline-api.php delete mode 100644 src/RemoteAPI/content-suggestions/class-suggest-linked-reference-api.php delete mode 100644 src/RemoteAPI/interface-cache.php delete mode 100644 src/RemoteAPI/interface-remote-api.php create mode 100644 src/services/class-base-api-service.php create mode 100644 src/services/class-base-service-endpoint.php create mode 100644 src/services/class-cached-service-endpoint.php create mode 100644 src/services/content-api/class-content-api-service.php create mode 100644 src/services/content-api/endpoints/class-content-api-base-endpoint.php create mode 100644 src/services/content-api/endpoints/class-endpoint-analytics-post-details.php create mode 100644 src/services/content-api/endpoints/class-endpoint-analytics-posts.php create mode 100644 src/services/content-api/endpoints/class-endpoint-referrers-post-detail.php create mode 100644 src/services/content-api/endpoints/class-endpoint-related.php create mode 100644 src/services/content-api/endpoints/class-endpoint-validate.php create mode 100644 src/services/suggestions-api/class-suggestions-api-service.php create mode 100644 src/services/suggestions-api/endpoints/class-endpoint-suggest-brief.php create mode 100644 src/services/suggestions-api/endpoints/class-endpoint-suggest-headline.php create mode 100644 src/services/suggestions-api/endpoints/class-endpoint-suggest-linked-reference.php create mode 100644 src/services/suggestions-api/endpoints/class-suggestions-api-base-endpoint.php diff --git a/.phpcs.xml.dist b/.phpcs.xml.dist index f863936f2a..7aa088a3bf 100644 --- a/.phpcs.xml.dist +++ b/.phpcs.xml.dist @@ -79,9 +79,9 @@ - + - /tests/ + / diff --git a/phpstan.neon b/phpstan.neon index 0b43b3f9cd..8f3366c936 100644 --- a/phpstan.neon +++ b/phpstan.neon @@ -14,7 +14,7 @@ parameters: - vendor/php-stubs/wordpress-stubs/wordpress-stubs.php - vendor/php-stubs/wordpress-tests-stubs/wordpress-tests-stubs.php type_coverage: - return_type: 91 + return_type: 90 param_type: 79.2 property_type: 0 # We can't use property types until PHP 7.4 becomes the plugin's minimum version. print_suggestions: false diff --git a/src/Endpoints/class-base-endpoint.php b/src/Endpoints/class-base-endpoint.php deleted file mode 100644 index 91af0c1075..0000000000 --- a/src/Endpoints/class-base-endpoint.php +++ /dev/null @@ -1,194 +0,0 @@ -parsely = $parsely; - } - - /** - * Returns the user capability allowing access to the endpoint, after having - * applied capability filters. - * - * `DEFAULT_ACCESS_CAPABILITY` is not passed here by default, to allow for - * a more explicit declaration in child classes. - * - * @since 3.14.0 - * - * @param string $capability The original capability allowing access. - * @return string The capability allowing access after applying the filters. - */ - protected function apply_capability_filters( string $capability ): string { - /** - * Filter to change the default user capability for all private endpoints. - * - * @var string - */ - $default_user_capability = apply_filters( - 'wp_parsely_user_capability_for_all_private_apis', - $capability - ); - - /** - * Filter to change the user capability for the specific endpoint. - * - * @var string - */ - $endpoint_specific_user_capability = apply_filters( - 'wp_parsely_user_capability_for_' . Utils::convert_endpoint_to_filter_key( static::ENDPOINT ) . '_api', - $default_user_capability - ); - - return $endpoint_specific_user_capability; - } - - /** - * Registers the endpoint's WP REST route. - * - * @since 3.11.0 Moved from Base_Endpoint_Remote into Base_Endpoint. - * - * @param string $endpoint The endpoint's route. - * @param string $callback The callback function to call when the endpoint is hit. - * @param array $methods The HTTP methods to allow for the endpoint. - */ - public function register_endpoint( - string $endpoint, - string $callback, - array $methods = array( 'GET' ) - ): void { - if ( ! apply_filters( 'wp_parsely_enable_' . Utils::convert_endpoint_to_filter_key( $endpoint ) . '_api_proxy', true ) ) { - return; - } - - $get_items_args = array( - 'query' => array( - 'default' => array(), - 'sanitize_callback' => function ( array $query ) { - $sanitized_query = array(); - foreach ( $query as $key => $value ) { - $sanitized_query[ sanitize_key( $key ) ] = sanitize_text_field( $value ); - } - - return $sanitized_query; - }, - ), - ); - - $rest_route_args = array( - array( - 'methods' => $methods, - 'callback' => array( $this, $callback ), - 'permission_callback' => array( $this, 'is_available_to_current_user' ), - 'args' => $get_items_args, - 'show_in_index' => static::is_available_to_current_user(), - ), - ); - - register_rest_route( 'wp-parsely/v1', $endpoint, $rest_route_args ); - } - - /** - * Registers the endpoint's WP REST route with arguments. - * - * @since 3.16.0 - * - * @param string $endpoint The endpoint's route. - * @param string $callback The callback function to call when the endpoint is hit. - * @param array $methods The HTTP methods to allow for the endpoint. - * @param array $args The arguments for the endpoint. - */ - public function register_endpoint_with_args( - string $endpoint, - string $callback, - array $methods = array( 'GET' ), - array $args = array() - ): void { - if ( ! apply_filters( 'wp_parsely_enable_' . Utils::convert_endpoint_to_filter_key( $endpoint ) . '_api_proxy', true ) ) { - return; - } - - $rest_route_args = array( - array( - 'methods' => $methods, - 'callback' => array( $this, $callback ), - 'permission_callback' => array( $this, 'is_available_to_current_user' ), - 'args' => $args, - 'show_in_index' => static::is_available_to_current_user(), - ), - ); - - register_rest_route( 'wp-parsely/v1', $endpoint, $rest_route_args ); - } -} diff --git a/src/RemoteAPI/class-analytics-post-detail-api.php b/src/RemoteAPI/class-analytics-post-detail-api.php deleted file mode 100644 index 8d946f5211..0000000000 --- a/src/RemoteAPI/class-analytics-post-detail-api.php +++ /dev/null @@ -1,45 +0,0 @@ -apply_capability_filters( - Base_Endpoint::DEFAULT_ACCESS_CAPABILITY - ) - ); - } -} diff --git a/src/RemoteAPI/class-analytics-posts-api.php b/src/RemoteAPI/class-analytics-posts-api.php deleted file mode 100644 index 9b70421ccb..0000000000 --- a/src/RemoteAPI/class-analytics-posts-api.php +++ /dev/null @@ -1,107 +0,0 @@ -, - * } - * - * @phpstan-type Analytics_Post array{ - * title?: string, - * url?: string, - * link?: string, - * author?: string, - * authors?: string[], - * section?: string, - * tags?: string[], - * metrics?: Analytics_Post_Metrics, - * full_content_word_count?: int, - * image_url?: string, - * metadata?: string, - * pub_date?: string, - * thumb_url_medium?: string, - * } - * - * @phpstan-type Analytics_Post_Metrics array{ - * avg_engaged?: float, - * views?: int, - * visitors?: int, - * } - */ -class Analytics_Posts_API extends Base_Endpoint_Remote { - public const MAX_RECORDS_LIMIT = 2000; - public const ANALYTICS_API_DAYS_LIMIT = 7; - - protected const API_BASE_URL = Parsely::PUBLIC_API_BASE_URL; - protected const ENDPOINT = '/analytics/posts'; - protected const QUERY_FILTER = 'wp_parsely_analytics_posts_endpoint_args'; - - /** - * Returns whether the endpoint is available for access by the current - * user. - * - * @since 3.14.0 - * @since 3.16.0 Added the `$request` parameter. - * - * @param WP_REST_Request|null $request The request object. - * @return bool - */ - public function is_available_to_current_user( $request = null ): bool { - return current_user_can( - // phpcs:ignore WordPress.WP.Capabilities.Undetermined - $this->apply_capability_filters( - Base_Endpoint::DEFAULT_ACCESS_CAPABILITY - ) - ); - } - - /** - * Calls Parse.ly Analytics API to get posts info. - * - * Main purpose of this function is to enforce typing. - * - * @param Analytics_Post_API_Params $api_params Parameters of the API. - * @return Analytics_Post[]|WP_Error|null - */ - public function get_posts_analytics( $api_params ) { - return $this->get_items( $api_params, true ); // @phpstan-ignore-line - } - - /** - * Returns the request's options for the remote API call. - * - * @since 3.9.0 - * - * @return array The array of options. - */ - public function get_request_options(): array { - return array( - 'timeout' => 30, // phpcs:ignore WordPressVIPMinimum.Performance.RemoteRequestTimeout.timeout_timeout - ); - } -} diff --git a/src/RemoteAPI/class-base-endpoint-remote.php b/src/RemoteAPI/class-base-endpoint-remote.php deleted file mode 100644 index e9ec20eb38..0000000000 --- a/src/RemoteAPI/class-base-endpoint-remote.php +++ /dev/null @@ -1,153 +0,0 @@ - $query The query arguments to send to the remote API. - * @throws UnexpectedValueException If the endpoint constant is not defined. - * @throws UnexpectedValueException If the query filter constant is not defined. - * @return string - */ - public function get_api_url( array $query ): string { - $this->validate_required_constraints(); - - $query['apikey'] = $this->parsely->get_site_id(); - if ( $this->parsely->api_secret_is_set() ) { - $query['secret'] = $this->parsely->get_api_secret(); - } - $query = array_filter( $query ); - - // Sort by key so the query args are in alphabetical order. - ksort( $query ); - - // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.DynamicHooknameFound -- Hook names are defined in child classes. - $query = apply_filters( static::QUERY_FILTER, $query ); - return add_query_arg( $query, static::API_BASE_URL . static::ENDPOINT ); - } - - /** - * Gets items from the specified endpoint. - * - * @since 3.2.0 - * @since 3.7.0 Added $associative param. - * - * @param array $query The query arguments to send to the remote API. - * @param bool $associative When TRUE, returned objects will be converted into associative arrays. - * @return array|object|WP_Error - */ - public function get_items( array $query, bool $associative = false ) { - $full_api_url = $this->get_api_url( $query ); - - /** - * GET request options. - * - * @var WP_HTTP_Request_Args $options - */ - $options = $this->get_request_options(); - $response = wp_safe_remote_get( $full_api_url, $options ); - - if ( is_wp_error( $response ) ) { - return $response; - } - - $body = wp_remote_retrieve_body( $response ); - $decoded = json_decode( $body ); - - if ( ! is_object( $decoded ) ) { - return new WP_Error( 400, __( 'Unable to decode upstream API response', 'wp-parsely' ) ); - } - - if ( ! property_exists( $decoded, 'data' ) ) { - return new WP_Error( - $decoded->code ?? 400, - $decoded->message ?? __( 'Unable to read data from upstream API', 'wp-parsely' ), - array( 'status' => $decoded->code ?? 400 ) - ); - } - - if ( ! is_array( $decoded->data ) ) { - return new WP_Error( 400, __( 'Unable to parse data from upstream API', 'wp-parsely' ) ); - } - - $data = $decoded->data; - - return $associative ? Utils::convert_to_associative_array( $data ) : $data; - } - - /** - * Returns the request's options for the remote API call. - * - * @since 3.9.0 - * - * @return array The array of options. - */ - public function get_request_options(): array { - return array(); - } - - /** - * Validates that required constants are defined. - * - * @since 3.14.0 - * - * @throws UnexpectedValueException If any required constant is not defined. - */ - protected function validate_required_constraints(): void { - if ( static::ENDPOINT === '' ) { - throw new UnexpectedValueException( 'ENDPOINT constant must be defined in child class.' ); - } - if ( static::QUERY_FILTER === '' ) { - throw new UnexpectedValueException( 'QUERY_FILTER constant must be defined in child class.' ); - } - } -} diff --git a/src/RemoteAPI/class-referrers-post-detail-api.php b/src/RemoteAPI/class-referrers-post-detail-api.php deleted file mode 100644 index 45233630e8..0000000000 --- a/src/RemoteAPI/class-referrers-post-detail-api.php +++ /dev/null @@ -1,45 +0,0 @@ -apply_capability_filters( - Base_Endpoint::DEFAULT_ACCESS_CAPABILITY - ) - ); - } -} diff --git a/src/RemoteAPI/class-related-api.php b/src/RemoteAPI/class-related-api.php deleted file mode 100644 index ea130bf025..0000000000 --- a/src/RemoteAPI/class-related-api.php +++ /dev/null @@ -1,39 +0,0 @@ -remote_api = $remote_api; - $this->cache = $cache; - } - - /** - * Implements caching for the Remote API interface. - * - * @param array $query The query arguments to send to the remote API. - * @param bool $associative Always `false`, just present to make definition compatible - * with interface. - * @return array|object|WP_Error The response from the remote API, or false if the - * response is empty. - */ - public function get_items( array $query, bool $associative = false ) { - $cache_key = 'parsely_api_' . - wp_hash( $this->remote_api->get_endpoint() ) . '_' . - wp_hash( (string) wp_json_encode( $query ) ); - - /** - * Variable. - * - * @var array|false - */ - $items = $this->cache->get( $cache_key, self::CACHE_GROUP ); - - if ( false === $items ) { - $items = $this->remote_api->get_items( $query ); - $this->cache->set( $cache_key, $items, self::CACHE_GROUP, self::OBJECT_CACHE_TTL ); - } - - return $items; - } - - /** - * Returns whether the endpoint is available for access by the current - * user. - * - * @since 3.7.0 - * @since 3.14.0 Renamed from `is_user_allowed_to_make_api_call()`. - * @since 3.16.0 Added the `$request` parameter. - * - * @param WP_REST_Request|null $request The request object. - * @return bool - */ - public function is_available_to_current_user( $request = null ): bool { - return $this->remote_api->is_available_to_current_user( $request ); - } -} diff --git a/src/RemoteAPI/class-validate-api.php b/src/RemoteAPI/class-validate-api.php deleted file mode 100644 index 2fb0674e73..0000000000 --- a/src/RemoteAPI/class-validate-api.php +++ /dev/null @@ -1,124 +0,0 @@ -apply_capability_filters( - Base_Endpoint::DEFAULT_ACCESS_CAPABILITY - ) - ); - } - - /** - * Gets the URL for the Parse.ly API credentials validation endpoint. - * - * @since 3.11.0 - * - * @param array $query The query arguments to send to the remote API. - * @return string - */ - public function get_api_url( array $query ): string { - $query = array( - 'apikey' => $query['apikey'], - 'secret' => $query['secret'], - ); - - return add_query_arg( $query, static::API_BASE_URL . static::ENDPOINT ); - } - - /** - * Queries the Parse.ly API credentials validation endpoint. - * The API will return a 200 response if the credentials are valid and a 401 response if they are not. - * - * @param array $query The query arguments to send to the remote API. - * @return object|WP_Error The response from the remote API, or a WP_Error object if the response is an error. - */ - private function api_validate_credentials( array $query ) { - /** - * GET request options. - * - * @var WP_HTTP_Request_Args $options - */ - $options = $this->get_request_options(); - $response = wp_safe_remote_get( $this->get_api_url( $query ), $options ); - - if ( is_wp_error( $response ) ) { - return $response; - } - - $body = wp_remote_retrieve_body( $response ); - $decoded = json_decode( $body ); - - if ( ! is_object( $decoded ) ) { - return new WP_Error( - 400, - __( - 'Unable to decode upstream API response', - 'wp-parsely' - ) - ); - } - - if ( ! property_exists( $decoded, 'success' ) || false === $decoded->success ) { - return new WP_Error( - $decoded->code ?? 400, - $decoded->message ?? __( 'Unable to read data from upstream API', 'wp-parsely' ) - ); - } - - return $decoded; - } - - /** - * Returns the response from the Parse.ly API credentials validation endpoint. - * - * @since 3.11.0 - * - * @param array $query The query arguments to send to the remote API. - * @param bool $associative (optional) When TRUE, returned objects will be converted into - * associative arrays. - * @return array|object|WP_Error - */ - public function get_items( array $query, bool $associative = false ) { - $api_request = $this->api_validate_credentials( $query ); - return $associative ? Utils::convert_to_associative_array( $api_request ) : $api_request; - } -} diff --git a/src/RemoteAPI/class-wordpress-cache.php b/src/RemoteAPI/class-wordpress-cache.php deleted file mode 100644 index 59c63e32b5..0000000000 --- a/src/RemoteAPI/class-wordpress-cache.php +++ /dev/null @@ -1,52 +0,0 @@ -apply_capability_filters( - self::DEFAULT_ACCESS_CAPABILITY - ) - ); - } - - /** - * Returns the request's options for the remote API call. - * - * @since 3.12.0 - * - * @return array The array of options. - */ - public function get_request_options(): array { - $options = array( - 'headers' => array( 'Content-Type' => 'application/json; charset=utf-8' ), - 'data_format' => 'body', - 'timeout' => 60, //phpcs:ignore WordPressVIPMinimum.Performance.RemoteRequestTimeout.timeout_timeout - 'body' => '{}', - ); - - // Add API key to request headers. - if ( $this->parsely->api_secret_is_set() ) { - $options['headers']['X-APIKEY-SECRET'] = $this->parsely->get_api_secret(); - } - - return $options; - } - - /** - * Gets the URL for a particular Parse.ly API Content Suggestion endpoint. - * - * @since 3.14.0 - * - * @param array $query The query arguments to send to the remote API. - * @throws UnexpectedValueException If the endpoint constant is not defined. - * @throws UnexpectedValueException If the query filter constant is not defined. - * @return string - */ - public function get_api_url( array $query = array() ): string { - $this->validate_required_constraints(); - - $query['apikey'] = $this->parsely->get_site_id(); - - // Remove empty entries and sort by key so the query args are in - // alphabetical order. - $query = array_filter( $query ); - ksort( $query ); - - // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.DynamicHooknameFound -- Hook names are defined in child classes. - $query = apply_filters( static::QUERY_FILTER, $query ); - return add_query_arg( $query, static::API_BASE_URL . static::ENDPOINT ); - } - - /** - * Sends a POST request to the Parse.ly Content Suggestion API. - * - * This method sends a POST request to the Parse.ly Content Suggestion API and returns the - * response. The response is either a WP_Error object in case of an error, or a decoded JSON - * object in case of a successful request. - * - * @since 3.13.0 - * - * @param array $query An associative array containing the query - * parameters for the API request. - * @param array> $body An associative array containing the body - * parameters for the API request. - * @return WP_Error|object Returns a WP_Error object in case of an error, or a decoded JSON - * object in case of a successful request. - */ - protected function post_request( array $query = array(), array $body = array() ) { - $full_api_url = $this->get_api_url( $query ); - - /** - * GET request options. - * - * @var WP_HTTP_Request_Args $options - */ - $options = $this->get_request_options(); - if ( count( $body ) > 0 ) { - $body = $this->truncate_array_content( $body ); - - $options['body'] = wp_json_encode( $body ); - if ( false === $options['body'] ) { - return new WP_Error( 400, __( 'Unable to encode request body', 'wp-parsely' ) ); - } - } - - $response = wp_safe_remote_post( $full_api_url, $options ); - if ( is_wp_error( $response ) ) { - return $response; - } - - // Handle any errors returned by the API. - if ( 200 !== $response['response']['code'] ) { - $error = json_decode( wp_remote_retrieve_body( $response ), true ); - - if ( ! is_array( $error ) ) { - return new WP_Error( - 400, - __( 'Unable to decode upstream API error', 'wp-parsely' ) - ); - } - - return new WP_Error( $error['error'], $error['detail'] ); - } - - $body = wp_remote_retrieve_body( $response ); - $decoded = json_decode( $body ); - - if ( ! is_object( $decoded ) ) { - return new WP_Error( 400, __( 'Unable to decode upstream API response', 'wp-parsely' ) ); - } - - return $decoded; - } - - /** - * Truncates the content of an array to a maximum length. - * - * @since 3.14.1 - * - * @param string|array|mixed $content The content to truncate. - * @return string|array|mixed The truncated content. - */ - public function truncate_array_content( $content ) { - if ( is_array( $content ) ) { - // If the content is an array, iterate over its elements. - foreach ( $content as $key => $value ) { - // Recursively process/truncate each element of the array. - $content[ $key ] = $this->truncate_array_content( $value ); - } - return $content; - } elseif ( is_string( $content ) ) { - // If the content is a string, truncate it. - if ( static::TRUNCATE_CONTENT ) { - // Check if the string length exceeds the maximum and truncate if necessary. - if ( mb_strlen( $content ) > self::TRUNCATE_CONTENT_LENGTH ) { - return mb_substr( $content, 0, self::TRUNCATE_CONTENT_LENGTH ); - } - } - return $content; - } - return $content; - } -} diff --git a/src/RemoteAPI/content-suggestions/class-suggest-brief-api.php b/src/RemoteAPI/content-suggestions/class-suggest-brief-api.php deleted file mode 100644 index 7a9ffa7fe9..0000000000 --- a/src/RemoteAPI/content-suggestions/class-suggest-brief-api.php +++ /dev/null @@ -1,73 +0,0 @@ - array( - 'persona' => $persona, - 'style' => $style, - ), - 'title' => $title, - 'text' => wp_strip_all_tags( $content ), - ); - - $decoded = $this->post_request( array(), $body ); - - if ( is_wp_error( $decoded ) ) { - return $decoded; - } - - if ( ! property_exists( $decoded, 'result' ) || - ! is_string( $decoded->result[0] ) ) { - return new WP_Error( - 400, - __( 'Unable to parse meta description from upstream API', 'wp-parsely' ) - ); - } - - return $decoded->result[0]; - } -} diff --git a/src/RemoteAPI/content-suggestions/class-suggest-headline-api.php b/src/RemoteAPI/content-suggestions/class-suggest-headline-api.php deleted file mode 100644 index 3466976636..0000000000 --- a/src/RemoteAPI/content-suggestions/class-suggest-headline-api.php +++ /dev/null @@ -1,69 +0,0 @@ -|WP_Error The response from the remote API, or a WP_Error - * object if the response is an error. - */ - public function get_titles( - string $content, - int $limit, - string $persona = 'journalist', - string $tone = 'neutral' - ) { - $body = array( - 'output_config' => array( - 'persona' => $persona, - 'style' => $tone, - 'max_items' => $limit, - ), - 'text' => wp_strip_all_tags( $content ), - ); - - $decoded = $this->post_request( array(), $body ); - - if ( is_wp_error( $decoded ) ) { - return $decoded; - } - - if ( ! property_exists( $decoded, 'result' ) || ! is_array( $decoded->result ) ) { - return new WP_Error( - 400, - __( 'Unable to parse titles from upstream API', 'wp-parsely' ) - ); - } - - return $decoded->result; - } -} diff --git a/src/RemoteAPI/content-suggestions/class-suggest-linked-reference-api.php b/src/RemoteAPI/content-suggestions/class-suggest-linked-reference-api.php deleted file mode 100644 index 073d401e24..0000000000 --- a/src/RemoteAPI/content-suggestions/class-suggest-linked-reference-api.php +++ /dev/null @@ -1,88 +0,0 @@ - array( - 'max_link_words' => $max_link_words, - 'max_items' => $max_links, - ), - 'text' => wp_strip_all_tags( $content ), - ); - - if ( count( $url_exclusion_list ) > 0 ) { - $body['url_exclusion_list'] = $url_exclusion_list; - } - - $decoded = $this->post_request( array(), $body ); - - if ( is_wp_error( $decoded ) ) { - return $decoded; - } - - if ( ! property_exists( $decoded, 'result' ) || - ! is_array( $decoded->result ) ) { - return new WP_Error( - 400, - __( 'Unable to parse suggested links from upstream API', 'wp-parsely' ) - ); - } - - // Convert the links to Smart_Link objects. - $links = array(); - foreach ( $decoded->result as $link ) { - $link = apply_filters( 'wp_parsely_suggest_linked_reference_link', $link ); - $link_obj = new Smart_Link( - esc_url( $link->canonical_url ), - esc_attr( $link->title ), - wp_kses_post( $link->text ), - $link->offset - ); - $links[] = $link_obj; - } - - return $links; - } -} diff --git a/src/RemoteAPI/interface-cache.php b/src/RemoteAPI/interface-cache.php deleted file mode 100644 index 9b20fa5fc6..0000000000 --- a/src/RemoteAPI/interface-cache.php +++ /dev/null @@ -1,46 +0,0 @@ - $query The query arguments to send to the remote API. - * @param bool $associative (optional) When TRUE, returned objects will be converted into - * associative arrays. - * @return array|object|WP_Error - */ - public function get_items( array $query, bool $associative = false ); - - /** - * Returns whether the endpoint is available for access by the current - * user. - * - * @since 3.14.0 Renamed from `is_user_allowed_to_make_api_call()`. - * @since 3.16.0 Added the `$request` parameter. - * - * @param WP_REST_Request|null $request The request object. - * @return bool - */ - public function is_available_to_current_user( $request = null ): bool; -} diff --git a/src/UI/class-recommended-widget.php b/src/UI/class-recommended-widget.php index b72bd2e5e3..25f8fca2e1 100644 --- a/src/UI/class-recommended-widget.php +++ b/src/UI/class-recommended-widget.php @@ -92,7 +92,7 @@ public function __construct( Parsely $parsely ) { * @return string API URL. */ private function get_api_url( string $site_id, ?int $published_within, ?string $sort, int $return_limit ): string { - $related_api_endpoint = Parsely::PUBLIC_API_BASE_URL . '/related'; + $related_api_endpoint = $this->parsely->get_content_api()->get_endpoint( '/related' ); $query_args = array( 'apikey' => $site_id, @@ -104,7 +104,7 @@ private function get_api_url( string $site_id, ?int $published_within, ?string $ $query_args['pub_date_start'] = $published_within . 'd'; } - return add_query_arg( $query_args, $related_api_endpoint ); + return $related_api_endpoint->get_endpoint_url( $query_args ); } /** diff --git a/src/UI/class-settings-page.php b/src/UI/class-settings-page.php index 3e5563bc03..b4011eed63 100644 --- a/src/UI/class-settings-page.php +++ b/src/UI/class-settings-page.php @@ -1198,7 +1198,7 @@ public function validate_options( $input ) { * @param ParselySettingOptions $input Options from the settings page. * @return ParselySettingOptions Validated inputs. */ - private function validate_basic_section( $input ) { + private function validate_basic_section( $input ): array { $are_credentials_managed = $this->parsely->are_credentials_managed; $options = $this->parsely->get_options(); @@ -1217,7 +1217,10 @@ private function validate_basic_section( $input ) { $valid_credentials = true; } - if ( is_wp_error( $valid_credentials ) && Validator::INVALID_API_CREDENTIALS === $valid_credentials->get_error_code() ) { + if ( + ( is_wp_error( $valid_credentials ) && Validator::INVALID_API_CREDENTIALS === $valid_credentials->get_error_code() ) || + ( is_bool( $valid_credentials ) && ! $valid_credentials ) + ) { add_settings_error( Parsely::OPTIONS_KEY, 'api_secret', diff --git a/src/class-parsely.php b/src/class-parsely.php index e7c673fedd..a7a9bee5ec 100644 --- a/src/class-parsely.php +++ b/src/class-parsely.php @@ -10,6 +10,9 @@ namespace Parsely; +use Parsely\REST_API\REST_API_Controller; +use Parsely\Services\Content_API\Content_API_Service; +use Parsely\Services\Suggestions_API\Suggestions_API_Service; use Parsely\UI\Metadata_Renderer; use Parsely\UI\Settings_Page; use Parsely\Utils\Utils; @@ -58,12 +61,12 @@ * } * * @phpstan-type WP_HTTP_Request_Args array{ - * method: string, - * timeout: float, - * blocking: bool, - * headers: array, - * body: string, - * data_format: string, + * method?: string, + * timeout?: float, + * blocking?: bool, + * headers?: array, + * body?: string, + * data_format?: string, * } * * @phpstan-import-type Metadata_Attributes from Metadata @@ -72,13 +75,32 @@ class Parsely { /** * Declare our constants */ - public const VERSION = PARSELY_VERSION; - public const MENU_SLUG = 'parsely'; // The page param passed to options-general.php. - public const OPTIONS_KEY = 'parsely'; // The key used to store options in the WP database. - public const CAPABILITY = 'manage_options'; // The capability required to administer settings. - public const DASHBOARD_BASE_URL = 'https://dash.parsely.com'; - public const PUBLIC_API_BASE_URL = 'https://api.parsely.com/v2'; - public const PUBLIC_SUGGESTIONS_API_BASE_URL = 'https://content-suggestions-api.parsely.net/prod'; + public const VERSION = PARSELY_VERSION; + public const MENU_SLUG = 'parsely'; // The page param passed to options-general.php. + public const OPTIONS_KEY = 'parsely'; // The key used to store options in the WP database. + public const CAPABILITY = 'manage_options'; // The capability required to administer settings. + public const DASHBOARD_BASE_URL = 'https://dash.parsely.com'; + + /** + * The Content API service. + * + * @var ?Content_API_Service $content_api_service + */ + private $content_api_service; + + /** + * The Suggestions API service. + * + * @var ?Suggestions_API_Service $suggestions_api_service + */ + private $suggestions_api_service; + + /** + * The Parse.ly internal REST API controller. + * + * @var REST_API_Controller|null $rest_api_controller + */ + private $rest_api_controller; /** * Declare some class properties @@ -237,6 +259,57 @@ public function run(): void { add_action( 'save_post', array( $this, 'update_metadata_endpoint' ) ); } + /** + * Returns the Content API service. + * + * This method returns the Content API service, which is used to interact with the Parse.ly Content API. + * + * @since 3.17.0 + * + * @return Content_API_Service + */ + public function get_content_api(): Content_API_Service { + if ( ! isset( $this->content_api_service ) ) { + $this->content_api_service = new Content_API_Service( $this ); + } + + return $this->content_api_service; + } + + /** + * Returns the Suggestions API service. + * + * This method returns the Suggestions API service, which is used to interact with the Parse.ly Suggestions API. + * + * @since 3.17.0 + * + * @return Suggestions_API_Service + */ + public function get_suggestions_api(): Suggestions_API_Service { + if ( ! isset( $this->suggestions_api_service ) ) { + $this->suggestions_api_service = new Suggestions_API_Service( $this ); + } + + return $this->suggestions_api_service; + } + + /** + * Gets the REST API controller. + * + * If the controller is not set, a new instance is created. + * + * @since 3.17.0 + * + * @return REST_API_Controller + */ + public function get_rest_api_controller(): REST_API_Controller { + if ( ! isset( $this->rest_api_controller ) ) { + $this->rest_api_controller = new REST_API_Controller( $this ); + } + + return $this->rest_api_controller; + } + /** * Gets the full URL of the JavaScript tracker file for the site. If an API * key is not set, return an empty string. @@ -384,7 +457,8 @@ public function update_metadata_endpoint( int $post_id ): void { 'tags' => $metadata['keywords'] ?? '', ); - $parsely_api_endpoint = self::PUBLIC_API_BASE_URL . '/metadata/posts'; + $parsely_api_base_url = Content_API_Service::get_base_url(); + $parsely_api_endpoint = $parsely_api_base_url . '/metadata/posts'; $parsely_metadata_secret = $parsely_options['metadata_secret']; $headers = array( 'Content-Type' => 'application/json' ); @@ -938,8 +1012,8 @@ private function sanitize_managed_option( string $option_id, $value ) { private function allow_parsely_remote_requests(): void { $allowed_urls = array( self::DASHBOARD_BASE_URL, - self::PUBLIC_API_BASE_URL, - self::PUBLIC_SUGGESTIONS_API_BASE_URL, + Content_API_Service::get_base_url(), + Suggestions_API_Service::get_base_url(), ); add_filter( diff --git a/src/class-validator.php b/src/class-validator.php index 26c915c857..2f0bfe0da0 100644 --- a/src/class-validator.php +++ b/src/class-validator.php @@ -44,7 +44,7 @@ public static function validate_metadata_secret( string $metadata_secret ): bool * @param Parsely $parsely The Parsely instance. * @param string $site_id The Site ID to be validated. * @param string $api_secret The API Secret to be validated. - * @return true|WP_Error True if the API Credentials are valid, WP_Error otherwise. + * @return bool|WP_Error True if the API Credentials are valid, WP_Error otherwise. */ public static function validate_api_credentials( Parsely $parsely, string $site_id, string $api_secret ) { // If the API secret is empty, the validation endpoint will always fail. @@ -54,21 +54,16 @@ public static function validate_api_credentials( Parsely $parsely, string $site_ return true; } - $query_args = array( - 'apikey' => $site_id, - 'secret' => $api_secret, - ); + $content_api = $parsely->get_content_api(); + $is_valid = $content_api->validate_credentials( $site_id, $api_secret ); - $validate_api = new RemoteAPI\Validate_API( $parsely ); - $request = $validate_api->get_items( $query_args ); - - if ( is_wp_error( $request ) ) { + if ( is_wp_error( $is_valid ) ) { return new WP_Error( self::INVALID_API_CREDENTIALS, __( 'Invalid API Credentials', 'wp-parsely' ) ); } - return true; + return $is_valid; } } diff --git a/src/content-helper/dashboard-widget/class-dashboard-widget.php b/src/content-helper/dashboard-widget/class-dashboard-widget.php index 610cabbd10..cc72d0e79f 100644 --- a/src/content-helper/dashboard-widget/class-dashboard-widget.php +++ b/src/content-helper/dashboard-widget/class-dashboard-widget.php @@ -11,10 +11,8 @@ namespace Parsely\Content_Helper; use Parsely\Parsely; -use Parsely\RemoteAPI\Analytics_Posts_API; - -use Parsely\REST_API\Settings\Endpoint_Dashboard_Widget_Settings; use Parsely\Utils\Utils; +use Parsely\REST_API\Settings\Endpoint_Dashboard_Widget_Settings; use const Parsely\PARSELY_FILE; @@ -89,12 +87,13 @@ public function run(): void { * @return bool Whether the Dashboard Widget can be enabled. */ public function can_enable_widget(): bool { - $screen = get_current_screen(); - $posts_api = new Analytics_Posts_API( $GLOBALS['parsely'] ); + $screen = get_current_screen(); return $this->can_enable_feature( null !== $screen && 'dashboard' === $screen->id, - $posts_api->is_available_to_current_user() + $this->parsely->get_rest_api_controller()->is_available_to_current_user( + '/stats/posts' + ) ); } diff --git a/src/content-helper/post-list-stats/class-post-list-stats.php b/src/content-helper/post-list-stats/class-post-list-stats.php index 42b2452e82..9ea12d9a47 100644 --- a/src/content-helper/post-list-stats/class-post-list-stats.php +++ b/src/content-helper/post-list-stats/class-post-list-stats.php @@ -11,10 +11,9 @@ namespace Parsely\Content_Helper; use DateTime; -use Parsely\Content_Helper\Content_Helper_Feature; use Parsely\Parsely; -use Parsely\RemoteAPI\Base_Endpoint_Remote; -use Parsely\RemoteAPI\Analytics_Posts_API; +use Parsely\Services\Content_API\Content_API_Service; +use Parsely\Services\Content_API\Endpoints\Endpoint_Analytics_Posts; use Parsely\Utils\Utils; use WP_Screen; @@ -26,9 +25,14 @@ * @since 3.7.0 * @since 3.9.0 Renamed FQCN from `Parsely\UI\Admin_Columns_Parsely_Stats` to `Parsely\Content_Helper\Post_List_Stats`. * - * @phpstan-import-type Analytics_Post_API_Params from Analytics_Posts_API - * @phpstan-import-type Analytics_Post from Analytics_Posts_API - * @phpstan-import-type Remote_API_Error from Base_Endpoint_Remote + * @phpstan-import-type Analytics_Posts_API_Params from Endpoint_Analytics_Posts + * @phpstan-import-type Analytics_Post from Endpoint_Analytics_Posts + * + * @phpstan-type Remote_API_Error array{ + * code: int, + * message: string, + * htmlMessage: string, + * } * * @phpstan-type Parsely_Post_Stats array{ * page_views: string, @@ -42,12 +46,13 @@ * } */ class Post_List_Stats extends Content_Helper_Feature { + /** - * Instance of Parsely Analytics Posts API. + * Instance of Content API Service. * - * @var Analytics_Posts_API + * @var Content_API_Service */ - private $analytics_api; + private $content_api; /** * Internal Variable. @@ -72,7 +77,8 @@ class Post_List_Stats extends Content_Helper_Feature { * @param Parsely $parsely Instance of Parsely class. */ public function __construct( Parsely $parsely ) { - $this->parsely = $parsely; + $this->parsely = $parsely; + $this->content_api = $parsely->get_content_api(); } /** @@ -114,12 +120,10 @@ public static function get_style_id(): string { * @since 3.7.0 */ public function run(): void { - $this->analytics_api = new Analytics_Posts_API( $this->parsely ); - if ( ! $this->can_enable_feature( $this->parsely->site_id_is_set(), $this->parsely->api_secret_is_set(), - $this->analytics_api->is_available_to_current_user() + $this->parsely->get_rest_api_controller()->is_available_to_current_user( '/stats/posts' ) ) ) { return; } @@ -225,7 +229,7 @@ public function enqueue_parsely_stats_script_with_data(): void { return; // Avoid calling the API if column is hidden. } - $parsely_stats_response = $this->get_parsely_stats_response( $this->analytics_api ); + $parsely_stats_response = $this->get_parsely_stats_response(); if ( null === $parsely_stats_response ) { return; @@ -267,10 +271,9 @@ public function is_parsely_stats_column_hidden(): bool { * * @since 3.7.0 * - * @param Analytics_Posts_API $analytics_api Instance of Analytics_Posts_API. * @return Parsely_Posts_Stats_Response|null */ - public function get_parsely_stats_response( $analytics_api ) { + public function get_parsely_stats_response(): ?array { if ( ! $this->is_tracked_as_post_type() ) { return null; } @@ -296,12 +299,12 @@ public function get_parsely_stats_response( $analytics_api ) { return null; } - $response = $analytics_api->get_posts_analytics( + $response = $this->content_api->get_posts( array( - 'period_start' => Analytics_Posts_API::ANALYTICS_API_DAYS_LIMIT . 'd', + 'period_start' => Endpoint_Analytics_Posts::MAX_PERIOD, 'pub_date_start' => $date_params['pub_date_start'] ?? '', 'pub_date_end' => $date_params['pub_date_end'] ?? '', - 'limit' => Analytics_Posts_API::MAX_RECORDS_LIMIT, + 'limit' => Endpoint_Analytics_Posts::MAX_LIMIT, 'sort' => 'avg_engaged', // Note: API sends different stats on different sort options. ) ); @@ -322,20 +325,14 @@ public function get_parsely_stats_response( $analytics_api ) { ); } - if ( null === $response ) { - return array( - 'data' => array(), - 'error' => null, - ); - } - /** - * Variable. - * - * @var array + * @var array $parsely_stats_map */ $parsely_stats_map = array(); + /** + * @var Analytics_Post $post_analytics + */ foreach ( $response as $post_analytics ) { $key = $this->get_unique_stats_key_from_analytics( $post_analytics ); @@ -349,9 +346,7 @@ public function get_parsely_stats_response( $analytics_api ) { $engaged_seconds = isset( $metrics['avg_engaged'] ) ? round( $metrics['avg_engaged'] * 60, 2 ) : 0; /** - * Variable. - * - * @var Parsely_Post_Stats + * @var Parsely_Post_Stats $stats */ $stats = array( 'page_views' => Utils::get_formatted_number( (string) $views ) . ' ' . _n( 'page view', 'page views', $views, 'wp-parsely' ), @@ -375,7 +370,7 @@ public function get_parsely_stats_response( $analytics_api ) { * * @since 3.7.0 * - * @return Analytics_Post_API_Params|null + * @return Analytics_Posts_API_Params|null */ private function get_publish_date_params_for_analytics_api() { $published_times = $this->utc_published_times; diff --git a/src/rest-api/class-base-api-controller.php b/src/rest-api/class-base-api-controller.php index 9766122449..b0553ff211 100644 --- a/src/rest-api/class-base-api-controller.php +++ b/src/rest-api/class-base-api-controller.php @@ -26,7 +26,7 @@ abstract class Base_API_Controller { * * @since 3.17.0 * - * @var Base_Endpoint[] + * @var array */ private $endpoints; @@ -147,7 +147,7 @@ public function get_endpoints(): array { * @param Base_Endpoint $endpoint The endpoint to register. */ protected function register_endpoint( Base_Endpoint $endpoint ): void { - $this->endpoints[] = $endpoint; + $this->endpoints[ $endpoint->get_endpoint_slug() ] = $endpoint; $endpoint->init(); } @@ -179,4 +179,26 @@ public function prefix_route( string $route ): string { return static::get_route_prefix() . '/' . $route; } + + /** + * Returns a specific endpoint by name. + * + * @since 3.17.0 + * + * @param string $endpoint The endpoint name/path. + * @return Base_Endpoint|null The endpoint object, or null if not found. + */ + protected function get_endpoint( string $endpoint ): ?Base_Endpoint { + return $this->endpoints[ $endpoint ] ?? null; + } + + /** + * Checks if a specific endpoint is available to the current user. + * + * @since 3.17.0 + * + * @param string $endpoint The endpoint to check. + * @return bool True if the controller is available to the current user, false otherwise. + */ + abstract public function is_available_to_current_user( string $endpoint ): bool; } diff --git a/src/rest-api/class-base-endpoint.php b/src/rest-api/class-base-endpoint.php index b732a5d805..ecea575a6d 100644 --- a/src/rest-api/class-base-endpoint.php +++ b/src/rest-api/class-base-endpoint.php @@ -183,6 +183,22 @@ public function get_full_endpoint( string $route = '' ): string { $this->api_controller->prefix_route( $route ); } + /** + * Returns the endpoint slug. + * + * The slug is the endpoint name prefixed with the route prefix, from + * the API controller. + * + * Used as an identifier for the endpoint, when registering routes. + * + * @since 3.17.0 + * + * @return string + */ + public function get_endpoint_slug(): string { + return $this->api_controller->prefix_route( '' ) . static::get_endpoint_name(); + } + /** * Returns the registered routes. * diff --git a/src/rest-api/class-rest-api-controller.php b/src/rest-api/class-rest-api-controller.php index 007c49e54e..de83801c8d 100644 --- a/src/rest-api/class-rest-api-controller.php +++ b/src/rest-api/class-rest-api-controller.php @@ -73,4 +73,55 @@ public function init(): void { $this->controllers = $controllers; } + + /** + * Determines if the specified endpoint is available to the current user. + * + * @since 3.17.0 + * + * @param string $endpoint The endpoint to check. + * @return bool True if the endpoint is available to the current user, false otherwise. + */ + public function is_available_to_current_user( string $endpoint ): bool { + // Remove any forward or trailing slashes. + $endpoint = trim( $endpoint, '/' ); + + // Get the controller for the endpoint. + $controller = $this->get_controller_for_endpoint( $endpoint ); + if ( null === $controller ) { + return false; + } + + // Get the endpoint object. + $endpoint_obj = $controller->get_endpoint( $endpoint ); + if ( null === $endpoint_obj ) { + return false; + } + + // Check if the endpoint is available to the current user. + $is_available = $endpoint_obj->is_available_to_current_user(); + if ( is_wp_error( $is_available ) ) { + return false; + } + + return $is_available; + } + + /** + * Gets the controller for the specified endpoint. + * + * @since 3.17.0 + * + * @param string $endpoint The endpoint to get the controller for. + * @return Base_API_Controller|null The controller for the specified endpoint. + */ + private function get_controller_for_endpoint( string $endpoint ): ?Base_API_Controller { + foreach ( $this->controllers as $controller ) { + if ( null !== $controller->get_endpoint( $endpoint ) ) { + return $controller; + } + } + + return null; + } } diff --git a/src/rest-api/content-helper/class-endpoint-excerpt-generator.php b/src/rest-api/content-helper/class-endpoint-excerpt-generator.php index 1c267a8195..921a7fd435 100644 --- a/src/rest-api/content-helper/class-endpoint-excerpt-generator.php +++ b/src/rest-api/content-helper/class-endpoint-excerpt-generator.php @@ -11,8 +11,8 @@ namespace Parsely\REST_API\Content_Helper; -use Parsely\RemoteAPI\ContentSuggestions\Suggest_Brief_API; use Parsely\REST_API\Base_Endpoint; +use Parsely\Services\Suggestions_API\Suggestions_API_Service; use WP_Error; use WP_REST_Request; use WP_REST_Response; @@ -28,13 +28,13 @@ class Endpoint_Excerpt_Generator extends Base_Endpoint { use Content_Helper_Feature; /** - * The Suggest Brief API instance. + * The Suggestions API service. * * @since 3.17.0 * - * @var Suggest_Brief_API $suggest_brief_api + * @var Suggestions_API_Service $suggestions_api */ - protected $suggest_brief_api; + protected $suggestions_api; /** * Initializes the class. @@ -45,7 +45,7 @@ class Endpoint_Excerpt_Generator extends Base_Endpoint { */ public function __construct( Content_Helper_Controller $controller ) { parent::__construct( $controller ); - $this->suggest_brief_api = new Suggest_Brief_API( $this->parsely ); + $this->suggestions_api = $controller->get_parsely()->get_suggestions_api(); } /** @@ -85,28 +85,40 @@ public function register_routes(): void { array( 'POST' ), array( $this, 'generate_excerpt' ), array( - 'text' => array( + 'text' => array( 'description' => __( 'The text to generate the excerpt from.', 'wp-parsely' ), 'type' => 'string', 'required' => true, ), - 'title' => array( + 'title' => array( 'description' => __( 'The title of the content.', 'wp-parsely' ), 'type' => 'string', 'required' => true, ), - 'persona' => array( + 'persona' => array( 'description' => __( 'The persona to use for the suggestion.', 'wp-parsely' ), 'type' => 'string', 'required' => false, 'default' => 'journalist', ), - 'style' => array( + 'style' => array( 'description' => __( 'The style to use for the suggestion.', 'wp-parsely' ), 'type' => 'string', 'required' => false, 'default' => 'neutral', ), + 'max_items' => array( + 'description' => __( 'The maximum number of items to generate.', 'wp-parsely' ), + 'type' => 'integer', + 'required' => false, + 'default' => 1, + ), + 'max_characters' => array( + 'description' => __( 'The maximum number of characters to generate.', 'wp-parsely' ), + 'type' => 'integer', + 'required' => false, + 'default' => 160, + ), ) ); } @@ -150,12 +162,37 @@ public function generate_excerpt( WP_REST_Request $request ) { */ $style = $request->get_param( 'style' ); - $response = $this->suggest_brief_api->get_suggestion( $post_title, $post_content, $persona, $style ); + /** + * The maximum number of items to generate. + * + * @var int $max_items + */ + $max_items = $request->get_param( 'max_items' ); + + /** + * The maximum number of characters to generate. + * + * @var int $max_characters + */ + $max_characters = $request->get_param( 'max_characters' ); + + $response = $this->suggestions_api->get_brief_suggestions( + $post_title, + $post_content, + array( + 'persona' => $persona, + 'style' => $style, + 'max_items' => $max_items, + 'max_characters' => $max_characters, + ) + ); if ( is_wp_error( $response ) ) { return $response; } + // TODO: For now, only return the first suggestion. When the UI is ready to handle multiple suggestions, we can return the entire array. + $response = $response[0] ?? ''; return new WP_REST_Response( array( 'data' => $response ), 200 ); } } diff --git a/src/rest-api/content-helper/class-endpoint-smart-linking.php b/src/rest-api/content-helper/class-endpoint-smart-linking.php index 79474b24ce..b2d45735fa 100644 --- a/src/rest-api/content-helper/class-endpoint-smart-linking.php +++ b/src/rest-api/content-helper/class-endpoint-smart-linking.php @@ -12,9 +12,9 @@ namespace Parsely\REST_API\Content_Helper; use Parsely\Models\Smart_Link; -use Parsely\RemoteAPI\ContentSuggestions\Suggest_Linked_Reference_API; use Parsely\REST_API\Base_Endpoint; use Parsely\REST_API\Use_Post_ID_Parameter_Trait; +use Parsely\Services\Suggestions_API\Suggestions_API_Service; use WP_Error; use WP_Post; use WP_REST_Request; @@ -32,13 +32,13 @@ class Endpoint_Smart_Linking extends Base_Endpoint { use Use_Post_ID_Parameter_Trait; /** - * The Suggest Linked Reference API instance. + * The Suggestions API service. * * @since 3.17.0 * - * @var Suggest_Linked_Reference_API $suggest_linked_reference_api + * @var Suggestions_API_Service $suggestions_api */ - private $suggest_linked_reference_api; + protected $suggestions_api; /** * Initializes the class. @@ -49,7 +49,7 @@ class Endpoint_Smart_Linking extends Base_Endpoint { */ public function __construct( Content_Helper_Controller $controller ) { parent::__construct( $controller ); - $this->suggest_linked_reference_api = new Suggest_Linked_Reference_API( $this->parsely ); + $this->suggestions_api = $controller->get_parsely()->get_suggestions_api(); } /** @@ -225,10 +225,11 @@ public function generate_smart_links( WP_REST_Request $request ) { */ $url_exclusion_list = $request->get_param( 'url_exclusion_list' ) ?? array(); - $response = $this->suggest_linked_reference_api->get_links( + $response = $this->suggestions_api->get_smart_links( $post_content, - 4, // TODO: will be removed after API refactoring. - $max_links, + array( + 'max_items' => $max_links, + ), $url_exclusion_list ); diff --git a/src/rest-api/content-helper/class-endpoint-title-suggestions.php b/src/rest-api/content-helper/class-endpoint-title-suggestions.php index f71b13d220..b737b7566c 100644 --- a/src/rest-api/content-helper/class-endpoint-title-suggestions.php +++ b/src/rest-api/content-helper/class-endpoint-title-suggestions.php @@ -11,8 +11,8 @@ namespace Parsely\REST_API\Content_Helper; -use Parsely\RemoteAPI\ContentSuggestions\Suggest_Headline_API; use Parsely\REST_API\Base_Endpoint; +use Parsely\Services\Suggestions_API\Suggestions_API_Service; use WP_Error; use WP_REST_Request; use WP_REST_Response; @@ -28,13 +28,13 @@ class Endpoint_Title_Suggestions extends Base_Endpoint { use Content_Helper_Feature; /** - * The Suggest Headline API. + * The Suggestions API service. * * @since 3.17.0 * - * @var Suggest_Headline_API $suggest_headline_api + * @var Suggestions_API_Service $suggestions_api */ - private $suggest_headline_api; + protected $suggestions_api; /** * Initializes the class. @@ -45,7 +45,7 @@ class Endpoint_Title_Suggestions extends Base_Endpoint { */ public function __construct( Content_Helper_Controller $controller ) { parent::__construct( $controller ); - $this->suggest_headline_api = new Suggest_Headline_API( $this->parsely ); + $this->suggestions_api = $controller->get_parsely()->get_suggestions_api(); } /** @@ -120,7 +120,7 @@ public function register_routes(): void { * @since 3.17.0 * * @param WP_REST_Request $request The request object. - * @return WP_REST_Response|\WP_Error The response object or a WP_Error object on failure. + * @return WP_REST_Response|WP_Error The response object or a WP_Error object on failure. */ public function generate_titles( WP_REST_Request $request ) { /** @@ -155,7 +155,14 @@ public function generate_titles( WP_REST_Request $request ) { $limit = 3; } - $response = $this->suggest_headline_api->get_titles( $post_content, $limit, $persona, $style ); + $response = $this->suggestions_api->get_title_suggestions( + $post_content, + array( + 'persona' => $persona, + 'style' => $style, + 'max_items' => $limit, + ) + ); if ( is_wp_error( $response ) ) { return $response; diff --git a/src/rest-api/stats/class-endpoint-post.php b/src/rest-api/stats/class-endpoint-post.php index 24fd669f6a..c861f8d571 100644 --- a/src/rest-api/stats/class-endpoint-post.php +++ b/src/rest-api/stats/class-endpoint-post.php @@ -10,11 +10,9 @@ namespace Parsely\REST_API\Stats; -use Parsely\RemoteAPI\Analytics_Post_Detail_API; -use Parsely\RemoteAPI\Referrers_Post_Detail_API; -use Parsely\RemoteAPI\Related_API; use Parsely\REST_API\Base_Endpoint; use Parsely\REST_API\Use_Post_ID_Parameter_Trait; +use Parsely\Services\Content_API\Content_API_Service; use Parsely\Utils\Utils; use stdClass; use WP_Error; @@ -36,22 +34,13 @@ class Endpoint_Post extends Base_Endpoint { use Related_Posts_Trait; /** - * The API for fetching post details. + * The Parse.ly Content API service. * * @since 3.17.0 * - * @var Analytics_Post_Detail_API $analytics_post_detail_api + * @var Content_API_Service $content_api */ - public $analytics_post_detail_api; - - /** - * The API for fetching post referrers. - * - * @since 3.17.0 - * - * @var Referrers_Post_Detail_API $referrers_post_detail_api - */ - public $referrers_post_detail_api; + public $content_api; /** * The total views of the post. @@ -71,9 +60,7 @@ class Endpoint_Post extends Base_Endpoint { */ public function __construct( Stats_Controller $controller ) { parent::__construct( $controller ); - $this->analytics_post_detail_api = new Analytics_Post_Detail_API( $this->parsely ); - $this->referrers_post_detail_api = new Referrers_Post_Detail_API( $this->parsely ); - $this->related_posts_api = new Related_API( $this->parsely ); + $this->content_api = $this->parsely->get_content_api(); } /** @@ -177,16 +164,18 @@ public function get_post_details( WP_REST_Request $request ) { $post = $request->get_param( 'post' ); $permalink = get_permalink( $post->ID ); + if ( ! is_string( $permalink ) ) { + return new WP_Error( 'invalid_post', __( 'Invalid post.', 'wp-parsely' ), array( 'status' => 404 ) ); + } + // Set the itm_source parameter. $this->set_itm_source_from_request( $request ); // Get the data from the API. - $analytics_request = $this->analytics_post_detail_api->get_items( - array( - 'url' => $permalink, - 'period_start' => $request->get_param( 'period_start' ), - 'period_end' => $request->get_param( 'period_end' ), - ) + $analytics_request = $this->content_api->get_post_details( + $permalink, + $request->get_param( 'period_start' ), + $request->get_param( 'period_end' ) ); if ( is_wp_error( $analytics_request ) ) { @@ -194,10 +183,11 @@ public function get_post_details( WP_REST_Request $request ) { } $post_data = array(); + /** * The analytics data object. * - * @var array $analytics_request + * @var array> $analytics_request */ foreach ( $analytics_request as $data ) { $post_data[] = $this->extract_post_data( $data ); @@ -230,6 +220,10 @@ public function get_post_referrers( WP_REST_Request $request ) { $post = $request->get_param( 'post' ); $permalink = get_permalink( $post->ID ); + if ( ! is_string( $permalink ) ) { + return new WP_Error( 'invalid_post', __( 'Invalid post.', 'wp-parsely' ), array( 'status' => 404 ) ); + } + // Set the itm_source parameter. $this->set_itm_source_from_request( $request ); @@ -243,12 +237,10 @@ public function get_post_referrers( WP_REST_Request $request ) { $this->total_views = $total_views; // Get the data from the API. - $analytics_request = $this->referrers_post_detail_api->get_items( - array( - 'url' => $permalink, - 'period_start' => $request->get_param( 'period_start' ), - 'period_end' => $request->get_param( 'period_end' ), - ) + $analytics_request = $this->content_api->get_post_referrers( + $permalink, + $request->get_param( 'period_start' ), + $request->get_param( 'period_end' ) ); if ( is_wp_error( $analytics_request ) ) { diff --git a/src/rest-api/stats/class-endpoint-posts.php b/src/rest-api/stats/class-endpoint-posts.php index 8b4c45c899..0671b9e5e8 100644 --- a/src/rest-api/stats/class-endpoint-posts.php +++ b/src/rest-api/stats/class-endpoint-posts.php @@ -10,8 +10,8 @@ namespace Parsely\REST_API\Stats; -use Parsely\RemoteAPI\Analytics_Posts_API; use Parsely\REST_API\Base_Endpoint; +use Parsely\Services\Content_API\Content_API_Service; use WP_Error; use WP_REST_Request; use WP_REST_Response; @@ -62,13 +62,13 @@ class Endpoint_Posts extends Base_Endpoint { ); /** - * The Analytics Posts API. + * The Parse.ly Content API service. * * @since 3.17.0 * - * @var Analytics_Posts_API + * @var Content_API_Service */ - public $analytics_posts_api; + public $content_api; /** * Constructor. @@ -79,7 +79,7 @@ class Endpoint_Posts extends Base_Endpoint { */ public function __construct( Stats_Controller $controller ) { parent::__construct( $controller ); - $this->analytics_posts_api = new Analytics_Posts_API( $this->parsely ); + $this->content_api = $this->parsely->get_content_api(); } /** @@ -149,27 +149,25 @@ public function register_routes(): void { 'default' => 1, ), 'author' => array( - 'description' => 'The author to filter by.', - 'type' => 'array', - 'items' => array( - 'type' => 'string', - ), - 'required' => false, - 'maxItems' => 5, + 'description' => 'Comma-separated list of authors to filter by.', + 'type' => 'string', + 'required' => false, + 'validate_callback' => array( $this, 'validate_max_length_is_5' ), + 'sanitize_callback' => array( $this, 'sanitize_string_to_array' ), ), 'section' => array( - 'description' => 'The section to filter by.', - 'type' => 'string', - 'required' => false, + 'description' => 'Comma-separated list of sections to filter by.', + 'type' => 'string', + 'required' => false, + 'validate_callback' => array( $this, 'validate_max_length_is_5' ), + 'sanitize_callback' => array( $this, 'sanitize_string_to_array' ), ), 'tag' => array( - 'description' => 'The tag to filter by.', - 'type' => 'array', - 'items' => array( - 'type' => 'string', - ), - 'required' => false, - 'maxItems' => 5, + 'description' => 'Comma-separated list of tags to filter by.', + 'type' => 'string', + 'required' => false, + 'validate_callback' => array( $this, 'validate_max_length_is_5' ), + 'sanitize_callback' => array( $this, 'sanitize_string_to_array' ), ), 'segment' => array( 'description' => 'The segment to filter by.', @@ -182,6 +180,42 @@ public function register_routes(): void { ); } + /** + * Sanitizes a string to an array, splitting it by commas. + * + * @since 3.17.0 + * + * @param string|array $str The string to sanitize. + * @return array The sanitized array. + */ + public function sanitize_string_to_array( $str ): array { + if ( is_array( $str ) ) { + return $str; + } + + return explode( ',', $str ); + } + + /** + * Validates that the parameter has at most 5 items. + * + * @since 3.17.0 + * + * @param string|array $string_or_array The string or array to validate. + * @return true|WP_Error + */ + public function validate_max_length_is_5( $string_or_array ) { + if ( is_string( $string_or_array ) ) { + $string_or_array = $this->sanitize_string_to_array( $string_or_array ); + } + + if ( count( $string_or_array ) > 5 ) { + return new WP_Error( 'invalid_param', __( 'The parameter must have at most 5 items.', 'wp-parsely' ) ); + } + + return true; + } + /** * API Endpoint: GET /stats/posts * @@ -198,23 +232,12 @@ public function get_posts( WP_REST_Request $request ) { // Setup the itm_source if it is provided. $this->set_itm_source_from_request( $request ); - // TODO: Needed before the Public API refactor. - // Convert array of authors to a string with the first element. - if ( isset( $params['author'] ) && is_array( $params['author'] ) ) { - $params['author'] = $params['author'][0]; - } - // Convert array of tags to a string with the first element. - if ( isset( $params['tag'] ) && is_array( $params['tag'] ) ) { - $params['tag'] = $params['tag'][0]; - } - // TODO END. - /** * The raw analytics data, received by the API. * * @var array|WP_Error $analytics_request */ - $analytics_request = $this->analytics_posts_api->get_items( + $analytics_request = $this->content_api->get_posts( array( 'period_start' => $params['period_start'] ?? null, 'period_end' => $params['period_end'] ?? null, @@ -237,6 +260,12 @@ public function get_posts( WP_REST_Request $request ) { // Process the data. $posts = array(); + + /** + * The analytics data object. + * + * @var array> $analytics_request + */ foreach ( $analytics_request as $item ) { $posts[] = $this->extract_post_data( $item ); } diff --git a/src/rest-api/stats/class-endpoint-related.php b/src/rest-api/stats/class-endpoint-related.php index f896b0546f..1f4526c8f2 100644 --- a/src/rest-api/stats/class-endpoint-related.php +++ b/src/rest-api/stats/class-endpoint-related.php @@ -10,8 +10,8 @@ namespace Parsely\REST_API\Stats; -use Parsely\RemoteAPI\Related_API; use Parsely\REST_API\Base_Endpoint; +use Parsely\Services\Content_API\Content_API_Service; use WP_Error; use WP_REST_Request; use WP_REST_Response; @@ -27,6 +27,15 @@ class Endpoint_Related extends Base_Endpoint { use Post_Data_Trait; use Related_Posts_Trait; + /** + * The Parse.ly Content API service. + * + * @since 3.17.0 + * + * @var Content_API_Service $content_api + */ + public $content_api; + /** * Constructor. * @@ -36,7 +45,7 @@ class Endpoint_Related extends Base_Endpoint { */ public function __construct( Stats_Controller $controller ) { parent::__construct( $controller ); - $this->related_posts_api = new Related_API( $this->parsely ); + $this->content_api = $this->parsely->get_content_api(); } /** diff --git a/src/rest-api/stats/trait-post-data.php b/src/rest-api/stats/trait-post-data.php index 784e64b58d..374333c632 100644 --- a/src/rest-api/stats/trait-post-data.php +++ b/src/rest-api/stats/trait-post-data.php @@ -83,45 +83,45 @@ private function get_itm_source_param_args(): array { * @since 3.10.0 * @since 3.17.0 Moved from the `Base_API_Proxy` class. * - * @param stdClass $item The object to extract the data from. + * @param array $item The object to extract the data from. * @return array The extracted data. */ - protected function extract_post_data( stdClass $item ): array { + protected function extract_post_data( array $item ): array { $data = array(); - if ( isset( $item->author ) ) { - $data['author'] = $item->author; + if ( isset( $item['author'] ) ) { + $data['author'] = $item['author']; } - if ( isset( $item->metrics->views ) ) { - $data['views'] = number_format_i18n( $item->metrics->views ); + if ( isset( $item['metrics']['views'] ) ) { + $data['views'] = number_format_i18n( $item['metrics']['views'] ); } - if ( isset( $item->metrics->visitors ) ) { - $data['visitors'] = number_format_i18n( $item->metrics->visitors ); + if ( isset( $item['metrics']['visitors'] ) ) { + $data['visitors'] = number_format_i18n( $item['metrics']['visitors'] ); } // The avg_engaged metric can be in different locations depending on the // endpoint and passed sort/url parameters. - $avg_engaged = $item->metrics->avg_engaged ?? $item->avg_engaged ?? null; + $avg_engaged = $item['metrics']['avg_engaged'] ?? $item['avg_engaged'] ?? null; if ( null !== $avg_engaged ) { $data['avgEngaged'] = Utils::get_formatted_duration( (float) $avg_engaged ); } - if ( isset( $item->pub_date ) ) { - $data['date'] = wp_date( Utils::get_date_format(), strtotime( $item->pub_date ) ); + if ( isset( $item['pub_date'] ) ) { + $data['date'] = wp_date( Utils::get_date_format(), strtotime( $item['pub_date'] ) ); } - if ( isset( $item->title ) ) { - $data['title'] = $item->title; + if ( isset( $item['title'] ) ) { + $data['title'] = $item['title']; } - if ( isset( $item->url ) ) { + if ( isset( $item['url'] ) ) { $site_id = $this->parsely->get_site_id(); // phpcs:ignore WordPressVIPMinimum.Functions.RestrictedFunctions.url_to_postid_url_to_postid - $post_id = url_to_postid( $item->url ); // 0 if the post cannot be found. + $post_id = url_to_postid( $item['url'] ); // 0 if the post cannot be found. - $post_url = Parsely::get_url_with_itm_source( $item->url, null ); + $post_url = Parsely::get_url_with_itm_source( $item['url'], null ); if ( Utils::parsely_is_https_supported() ) { $post_url = str_replace( 'http://', 'https://', $post_url ); } @@ -136,8 +136,8 @@ protected function extract_post_data( stdClass $item ): array { $thumbnail_url = get_the_post_thumbnail_url( $post_id, 'thumbnail' ); if ( false !== $thumbnail_url ) { $data['thumbnailUrl'] = $thumbnail_url; - } elseif ( isset( $item->thumb_url_medium ) ) { - $data['thumbnailUrl'] = $item->thumb_url_medium; + } elseif ( isset( $item['thumb_url_medium'] ) ) { + $data['thumbnailUrl'] = $item['thumb_url_medium']; } } diff --git a/src/rest-api/stats/trait-related-posts.php b/src/rest-api/stats/trait-related-posts.php index f7c486f5ca..c165388de6 100644 --- a/src/rest-api/stats/trait-related-posts.php +++ b/src/rest-api/stats/trait-related-posts.php @@ -11,7 +11,7 @@ namespace Parsely\REST_API\Stats; use Parsely\Parsely; -use Parsely\RemoteAPI\Related_API; +use Parsely\Services\Content_API\Content_API_Service; use stdClass; use WP_Error; use WP_REST_Request; @@ -22,15 +22,6 @@ * @since 3.17.0 */ trait Related_Posts_Trait { - /** - * The API for fetching related posts. - * - * @since 3.17.0 - * - * @var Related_API $related_posts_api - */ - public $related_posts_api; - /** * The itm_source of for the post URL. * @@ -124,9 +115,10 @@ public function get_related_posts_of_url( WP_REST_Request $request, string $url /** * The raw related posts data, received by the API. * - * @var array|WP_Error $related_posts_request + * @var array>|WP_Error $related_posts_request */ - $related_posts_request = $this->related_posts_api->get_items( + $related_posts_request = $this->content_api->get_related_posts_with_url( + $url, array( 'url' => $url, 'sort' => $request->get_param( 'sort' ), @@ -147,12 +139,12 @@ public function get_related_posts_of_url( WP_REST_Request $request, string $url $itm_source = $this->itm_source; $related_posts = array_map( - static function ( stdClass $item ) use ( $itm_source ) { - return (object) array( - 'image_url' => $item->image_url, - 'thumb_url_medium' => $item->thumb_url_medium, - 'title' => $item->title, - 'url' => Parsely::get_url_with_itm_source( $item->url, $itm_source ), + static function ( array $item ) use ( $itm_source ) { + return array( + 'image_url' => $item['image_url'], + 'thumb_url_medium' => $item['thumb_url_medium'], + 'title' => $item['title'], + 'url' => Parsely::get_url_with_itm_source( $item['url'], $itm_source ), ); }, $related_posts_request diff --git a/src/services/class-base-api-service.php b/src/services/class-base-api-service.php new file mode 100644 index 0000000000..4a1697e68f --- /dev/null +++ b/src/services/class-base-api-service.php @@ -0,0 +1,133 @@ + + */ + protected $endpoints; + + /** + * The Parsely instance. + * + * @since 3.17.0 + * + * @var Parsely + */ + private $parsely; + + + /** + * Initializes the class. + * + * @since 3.17.0 + * + * @param Parsely $parsely The Parsely instance. + */ + public function __construct( Parsely $parsely ) { + $this->parsely = $parsely; + $this->register_endpoints(); + } + + /** + * Registers an endpoint with the service. + * + * @since 3.17.0 + * + * @param Base_Service_Endpoint $endpoint The endpoint to register. + */ + protected function register_endpoint( Base_Service_Endpoint $endpoint ): void { + $this->endpoints[ $endpoint->get_endpoint() ] = $endpoint; + } + + /** + * Registers a cached endpoint with the service. + * + * @since 3.17.0 + * + * @param Base_Service_Endpoint $endpoint The endpoint to register. + * @param int $ttl The time-to-live for the cache, in seconds. + */ + protected function register_cached_endpoint( Base_Service_Endpoint $endpoint, int $ttl ): void { + $this->endpoints[ $endpoint->get_endpoint() ] = new Cached_Service_Endpoint( $endpoint, $ttl ); + } + + /** + * Gets an endpoint by name. + * + * @since 3.17.0 + * + * @param string $endpoint The name of the endpoint. + * @return Base_Service_Endpoint The endpoint. + */ + public function get_endpoint( string $endpoint ): Base_Service_Endpoint { + return $this->endpoints[ $endpoint ]; + } + + /** + * Returns the base URL for the API service. + * + * This method should be overridden in the child class to return the base URL + * for the API service. + * + * @since 3.17.0 + * + * @return string + */ + abstract public static function get_base_url(): string; + + /** + * Registers the endpoints for the service. + * + * This method should be overridden in the child class to register the + * endpoints for the service. + * + * @since 3.17.0 + */ + abstract protected function register_endpoints(): void; + + /** + * Returns the API URL for the service. + * + * @since 3.17.0 + * + * @return string + */ + public function get_api_url(): string { + return static::get_base_url(); + } + + /** + * Returns the Parsely instance. + * + * @since 3.17.0 + * + * @return Parsely + */ + public function get_parsely(): Parsely { + return $this->parsely; + } +} diff --git a/src/services/class-base-service-endpoint.php b/src/services/class-base-service-endpoint.php new file mode 100644 index 0000000000..ef5245d9c0 --- /dev/null +++ b/src/services/class-base-service-endpoint.php @@ -0,0 +1,261 @@ +, + * body: string, + * response: array{ + * code: int|false, + * message: string|false, + * }, + * cookies: array, + * http_response: \WP_HTTP_Requests_Response|null, + * } + * + * @phpstan-import-type WP_HTTP_Request_Args from Parsely + */ +abstract class Base_Service_Endpoint { + /** + * The API service that this endpoint belongs to. + * + * @since 3.17.0 + * + * @var Base_API_Service + */ + protected $api_service; + + /** + * Flag to truncate the content of the request body. + * + * If set to true, the content of the request body will be truncated to a maximum length. + * + * @since 3.14.1 + * @since 3.17.0 Moved to the Base_Service_Endpoint class. + * + * @var bool + */ + protected const TRUNCATE_CONTENT = false; + + /** + * The maximum length of the content of the request body. + * + * @since 3.14.1 + * @since 3.17.0 Moved to the Base_Service_Endpoint class. + * + * @var int + */ + protected const TRUNCATE_CONTENT_LENGTH = 25000; + + /** + * Initializes the class. + * + * @since 3.17.0 + * + * @param Base_API_Service $api_service The API service that this endpoint belongs to. + */ + public function __construct( Base_API_Service $api_service ) { + $this->api_service = $api_service; + } + + /** + * Returns the headers to send with the request. + * + * @since 3.17.0 + * + * @return array The headers to send with the request. + */ + protected function get_headers(): array { + return array( + 'Content-Type' => 'application/json', + ); + } + + /** + * Returns the request options for the remote API request. + * + * @since 3.17.0 + * + * @param string $method The HTTP method to use for the request. + * @return WP_HTTP_Request_Args The request options for the remote API request. + */ + protected function get_request_options( string $method ): array { + $options = array( + 'headers' => $this->get_headers(), + 'method' => $method, + ); + + return $options; + } + + /** + * Returns the common query arguments to send to the remote API. + * + * This can be used for setting common query arguments that are shared + * across multiple endpoints, such as the API key. + * + * @since 3.17.0 + * + * @param array $args Additional query arguments to send to the remote API. + * @return array The query arguments to send to the remote API. + */ + protected function get_query_args( array $args = array() ): array { + return $args; + } + + /** + * Executes the API request. + * + * @since 3.17.0 + * + * @param array $args The arguments to pass to the API request. + * @return WP_Error|array The response from the API. + */ + abstract public function call( array $args = array() ); + + /** + * Returns the endpoint for the API request. + * + * This should be the path to the endpoint, not the full URL. + * Override this method in the child class to return the endpoint. + * + * @since 3.17.0 + * + * @return string The endpoint for the API request. + */ + abstract public function get_endpoint(): string; + + /** + * Returns the full URL for the API request, including the endpoint and query arguments. + * + * @since 3.17.0 + * + * @param array $query_args The query arguments to send to the remote API. + * @return string The full URL for the API request. + */ + public function get_endpoint_url( array $query_args = array() ): string { + // Get the base URL from the API service. + $base_url = $this->api_service->get_api_url(); + + // Append the endpoint to the base URL. + $base_url .= $this->get_endpoint(); + + // Append any necessary query arguments. + $endpoint = add_query_arg( $this->get_query_args( $query_args ), $base_url ); + + return $endpoint; + } + + /** + * Sends a request to the remote API. + * + * @since 3.17.0 + * + * @param string $method The HTTP method to use for the request. + * @param array $query_args The query arguments to send to the remote API. + * @param array $data The data to send in the request body. + * @return WP_Error|array The response from the remote API. + */ + protected function request( string $method, array $query_args = array(), array $data = array() ) { + // Get the URL to send the request to. + $request_url = $this->get_endpoint_url( $query_args ); + + // Build the request options. + $request_options = $this->get_request_options( $method ); + + if ( count( $data ) > 0 ) { + $data = $this->truncate_array_content( $data ); + + $request_options['body'] = wp_json_encode( $data ); + if ( false === $request_options['body'] ) { + return new WP_Error( 400, __( 'Unable to encode request body', 'wp-parsely' ) ); + } + } + + /** @var WP_HTTP_Response|WP_Error $response */ + $response = wp_safe_remote_request( $request_url, $request_options ); + + return $this->process_response( $response ); + } + + /** + * Processes the response from the remote API. + * + * @since 3.17.0 + * + * @param WP_HTTP_Response|WP_Error $response The response from the remote API. + * @return array|WP_Error The processed response. + */ + protected function process_response( $response ) { + if ( is_wp_error( $response ) ) { + /** @var WP_Error $response */ + return $response; + } + + $body = wp_remote_retrieve_body( $response ); + $decoded = json_decode( $body, true ); + + if ( ! is_array( $decoded ) ) { + return new WP_Error( 400, __( 'Unable to decode upstream API response', 'wp-parsely' ) ); + } + + return $decoded; + } + + /** + * Returns the Parsely instance. + * + * @since 3.17.0 + * + * @return Parsely The Parsely instance. + */ + public function get_parsely(): Parsely { + return $this->api_service->get_parsely(); + } + + /** + * Truncates the content of an array to a maximum length. + * + * @since 3.14.1 + * @since 3.17.0 Moved to the Base_Service_Endpoint class. + * + * @param string|array|mixed $content The content to truncate. + * @return string|array|mixed The truncated content. + */ + private function truncate_array_content( $content ) { + if ( is_array( $content ) ) { + // If the content is an array, iterate over its elements. + foreach ( $content as $key => $value ) { + // Recursively process/truncate each element of the array. + $content[ $key ] = $this->truncate_array_content( $value ); + } + return $content; + } elseif ( is_string( $content ) ) { + // If the content is a string, truncate it. + if ( static::TRUNCATE_CONTENT ) { + // Check if the string length exceeds the maximum and truncate if necessary. + if ( mb_strlen( $content ) > self::TRUNCATE_CONTENT_LENGTH ) { + return mb_substr( $content, 0, self::TRUNCATE_CONTENT_LENGTH ); + } + } + return $content; + } + return $content; + } +} diff --git a/src/services/class-cached-service-endpoint.php b/src/services/class-cached-service-endpoint.php new file mode 100644 index 0000000000..fc19d15615 --- /dev/null +++ b/src/services/class-cached-service-endpoint.php @@ -0,0 +1,124 @@ +service_endpoint = $service_endpoint; + $this->cache_ttl = $cache_ttl; + + parent::__construct( $service_endpoint->api_service ); + } + + /** + * Returns the cache key for the API request. + * + * @since 3.17.0 + * + * @param array $args The arguments to pass to the API request. + * @return string The cache key for the API request. + */ + private function get_cache_key( array $args ): string { + $api_service = $this->service_endpoint->api_service; + + $cache_key = 'parsely_api_' . + wp_hash( $api_service->get_api_url() ) . '_' . + wp_hash( $this->get_endpoint() ) . '_' . + wp_hash( (string) wp_json_encode( $args ) ); + + return $cache_key; + } + + /** + * Executes the API request, caching the response. + * + * If the response is already cached, it will be returned from the cache, + * otherwise the API request will be made and the response will be cached. + * + * @since 3.17.0 + * + * @param array $args The arguments to pass to the API request. + * @return WP_Error|array The response from the API. + */ + public function call( array $args = array() ) { + $cache_key = $this->get_cache_key( $args ); + $cache = wp_cache_get( $cache_key, self::CACHE_GROUP ); + + if ( false !== $cache ) { + // @phpstan-ignore-next-line + return $cache; + } + + $response = $this->service_endpoint->call( $args ); + + if ( ! is_wp_error( $response ) ) { + wp_cache_set( $cache_key, $response, self::CACHE_GROUP, $this->cache_ttl ); // phpcs:ignore + } + + return $response; + } + + /** + * Returns the endpoint for the API request. + * + * @since 3.17.0 + * + * @return string The endpoint for the API request. + */ + public function get_endpoint(): string { + return $this->service_endpoint->get_endpoint(); + } +} diff --git a/src/services/content-api/class-content-api-service.php b/src/services/content-api/class-content-api-service.php new file mode 100644 index 0000000000..b2e165a085 --- /dev/null +++ b/src/services/content-api/class-content-api-service.php @@ -0,0 +1,234 @@ +register_endpoint( $endpoint ); + } + + /** + * The cached endpoints. + * + * The second element in the array is the time-to-live for the cache, in seconds. + * + * @var array $cached_endpoints + */ + $cached_endpoints = array( + array( new Endpoints\Endpoint_Analytics_Posts( $this ), 300 ), // 5 minutes. + array( new Endpoints\Endpoint_Related( $this ), 600 ), // 10 minutes. + array( new Endpoints\Endpoint_Referrers_Post_Detail( $this ), 300 ), // 5 minutes. + array( new Endpoints\Endpoint_Analytics_Post_Details( $this ), 300 ), // 5 minutes. + ); + + foreach ( $cached_endpoints as $cached_endpoint ) { + $this->register_cached_endpoint( $cached_endpoint[0], $cached_endpoint[1] ); + } + } + + /** + * Returns the post’s metadata, as well as total views and visitors in the metrics field. + * + * By default, this returns the total pageviews on the link for the last 90 days. + * + * @since 3.17.0 + * + * @link https://docs.parse.ly/api-analytics-endpoint/#2-get-analytics-post-detail + * + * @param string $url The URL of the post. + * @param string|null $period_start The start date of the period to get the data for. + * @param string|null $period_end The end date of the period to get the data for. + * @return array|WP_Error Returns the post details or a WP_Error object in case of an error. + */ + public function get_post_details( + string $url, + string $period_start = null, + string $period_end = null + ) { + /** @var Endpoints\Endpoint_Analytics_Post_Details $endpoint */ + $endpoint = $this->get_endpoint( '/analytics/post/detail' ); + + $args = array( + 'url' => $url, + 'period_start' => $period_start, + 'period_end' => $period_end, + ); + + return $endpoint->call( $args ); + } + + /** + * Returns the referrers for a given post URL. + * + * @since 3.17.0 + * + * @link https://docs.parse.ly/api-referrers-endpoint/#3-get-referrers-post-detail + * + * @param string $url The URL of the post. + * @param string|null $period_start The start date of the period to get the data for. + * @param string|null $period_end The end date of the period to get the data for. + * @return array|WP_Error Returns the referrers or a WP_Error object in case of an error. + */ + public function get_post_referrers( + string $url, + string $period_start = null, + string $period_end = null + ) { + /** @var Endpoints\Endpoint_Referrers_Post_Detail $endpoint */ + $endpoint = $this->get_endpoint( '/referrers/post/detail' ); + + $args = array( + 'url' => $url, + 'period_start' => $period_start, + 'period_end' => $period_end, + ); + + return $endpoint->call( $args ); + } + + /** + * Returns the related posts for a given URL. + * + * @since 3.17.0 + * + * @link https://docs.parse.ly/content-recommendations/#h-get-related + * + * @param string $url The URL of the post. + * @param array $params The parameters to pass to the API request. + * @return array|WP_Error Returns the related posts or a WP_Error object in case of an error. + */ + public function get_related_posts_with_url( string $url, array $params = array() ) { + /** @var Endpoints\Endpoint_Related $endpoint */ + $endpoint = $this->get_endpoint( '/related' ); + + $args = array( + 'url' => $url, + ); + + // Merge the optional params. + $args = array_merge( $params, $args ); + + return $endpoint->call( $args ); + } + + /** + * Returns the related posts for a given UUID. + * + * @since 3.17.0 + * + * @link https://docs.parse.ly/content-recommendations/#h-get-related + * + * @param string $uuid The UUID of the user. + * @param array $params The parameters to pass to the API request. + * @return array|WP_Error Returns the related posts or a WP_Error object in case of an error. + */ + public function get_related_posts_with_uuid( string $uuid, array $params = array() ) { + /** @var Endpoints\Endpoint_Related $endpoint */ + $endpoint = $this->get_endpoint( '/related' ); + + $args = array( + 'uuid' => $uuid, + ); + + // Merge the optional params. + $args = array_merge( $params, $args ); + + return $endpoint->call( $args ); + } + + /** + * Returns the posts analytics. + * + * @since 3.17.0 + * + * @link https://docs.parse.ly/api-analytics-endpoint/#1-get-analytics-posts + * + * @param array $params The parameters to pass to the API request. + * @return array|WP_Error Returns the posts analytics or a WP_Error object in case of an error. + */ + public function get_posts( array $params = array() ) { + /** @var Endpoints\Endpoint_Analytics_Posts $endpoint */ + $endpoint = $this->get_endpoint( '/analytics/posts' ); + + return $endpoint->call( $params ); + } + + /** + * Validates the Parse.ly API credentials. + * + * The API will return a 200 response if the credentials are valid and a 401 response if they are not. + * + * @since 3.17.0 + * + * @param string $api_key The API key to validate. + * @param string $secret_key The secret key to validate. + * @return bool|WP_Error Returns true if the credentials are valid, false otherwise. + */ + public function validate_credentials( string $api_key, string $secret_key ) { + /** @var Endpoints\Endpoint_Validate $endpoint */ + $endpoint = $this->get_endpoint( '/validate/secret' ); + + $args = array( + 'apikey' => $api_key, + 'secret' => $secret_key, + ); + + $response = $endpoint->call( $args ); + + if ( is_wp_error( $response ) ) { + /** @var WP_Error $response */ + return $response; + } + + if ( true === $response['success'] ) { + return true; + } + + return false; + } +} diff --git a/src/services/content-api/endpoints/class-content-api-base-endpoint.php b/src/services/content-api/endpoints/class-content-api-base-endpoint.php new file mode 100644 index 0000000000..4a4692fac0 --- /dev/null +++ b/src/services/content-api/endpoints/class-content-api-base-endpoint.php @@ -0,0 +1,89 @@ +, + * } + * + * @phpstan-type Content_API_Error_Response array{ + * code?: int, + * message?: string, + * } + * + * @phpstan-type Content_API_Response = Content_API_Valid_Response|Content_API_Error_Response + * + * @phpstan-import-type WP_HTTP_Response from Base_Service_Endpoint + */ +abstract class Content_API_Base_Endpoint extends Base_Service_Endpoint { + /** + * Returns the common query arguments to send to the remote API. + * + * This method append the API key and secret to the query arguments. + * + * @since 3.17.0 + * + * @param array $args Additional query arguments to send to the remote API. + * @return array The query arguments to send to the remote API. + */ + protected function get_query_args( array $args = array() ): array { + $query_args = parent::get_query_args( $args ); + + // Set up the API key and secret. + $query_args['apikey'] = $this->get_parsely()->get_site_id(); + if ( $this->get_parsely()->api_secret_is_set() ) { + $query_args['secret'] = $this->get_parsely()->get_api_secret(); + } + + return $query_args; + } + + /** + * Processes the response from the remote API. + * + * @since 3.17.0 + * + * @param WP_HTTP_Response|WP_Error $response The response from the remote API. + * @return array|WP_Error The processed response. + */ + protected function process_response( $response ) { + $response = parent::process_response( $response ); + + if ( is_wp_error( $response ) ) { + /** @var WP_Error $response */ + return $response; + } + + if ( ! isset( $response['data'] ) ) { + /** @var Content_API_Error_Response $response */ + return new WP_Error( + $response['code'] ?? 400, + $response['message'] ?? __( 'Unable to read data from upstream API', 'wp-parsely' ), + array( 'status' => $response['code'] ?? 400 ) + ); + } + + if ( ! is_array( $response['data'] ) ) { + return new WP_Error( 400, __( 'Unable to parse data from upstream API', 'wp-parsely' ) ); + } + + /** @var Content_API_Valid_Response $response */ + return $response['data']; + } +} diff --git a/src/services/content-api/endpoints/class-endpoint-analytics-post-details.php b/src/services/content-api/endpoints/class-endpoint-analytics-post-details.php new file mode 100644 index 0000000000..54d26d3ae9 --- /dev/null +++ b/src/services/content-api/endpoints/class-endpoint-analytics-post-details.php @@ -0,0 +1,54 @@ + $args The arguments to pass to the API request. + * @return WP_Error|array The response from the API request. + */ + public function call( array $args = array() ) { + $query_args = array( + 'url' => $args['url'], + 'period_start' => $args['period_start'], + 'period_end' => $args['period_end'], + ); + + // Filter out the empty values. + $query_args = array_filter( $query_args ); + + return $this->request( 'GET', $query_args ); + } +} diff --git a/src/services/content-api/endpoints/class-endpoint-analytics-posts.php b/src/services/content-api/endpoints/class-endpoint-analytics-posts.php new file mode 100644 index 0000000000..3c280397cc --- /dev/null +++ b/src/services/content-api/endpoints/class-endpoint-analytics-posts.php @@ -0,0 +1,180 @@ +, + * } + * + * @phpstan-type Analytics_Post array{ + * title?: string, + * url?: string, + * link?: string, + * author?: string, + * authors?: string[], + * section?: string, + * tags?: string[], + * metrics?: Analytics_Post_Metrics, + * full_content_word_count?: int, + * image_url?: string, + * metadata?: string, + * pub_date?: string, + * thumb_url_medium?: string, + * } + * + * @phpstan-type Analytics_Post_Metrics array{ + * avg_engaged?: float, + * views?: int, + * visitors?: int, + * } + */ +class Endpoint_Analytics_Posts extends Content_API_Base_Endpoint { + private const MAX_RECORDS_LIMIT = 2000; + private const ANALYTICS_API_DAYS_LIMIT = 7; + + /** + * Maximum limit for the number of records to return, to be + * used in the limit parameter. + * + * @since 3.17.0 + * + * @var string + */ + public const MAX_LIMIT = 'max'; + + /** + * Maximum period for the API request, to be used in the period_start parameter. + * + * @since 3.17.0 + * + * @var string + */ + public const MAX_PERIOD = 'max_days'; + + /** + * Returns the endpoint for the API request. + * + * @since 3.17.0 + * + * @return string + */ + public function get_endpoint(): string { + return '/analytics/posts'; + } + + /** + * Returns the endpoint URL for the API request. + * + * This method appends the author, tag, and section parameters to the + * endpoint URL, if they are set. Since the Parse.ly API needs a key for + * every value (e.g. tag=tag1&tag=tag2), we need to append them manually. + * + * @since 3.17.0 + * + * @param array $query_args The arguments to pass to the API request. + * @return string The endpoint URL for the API request. + */ + public function get_endpoint_url( array $query_args = array() ): string { + // Store the author, tag, and section parameters. + /** @var array $authors */ + $authors = $query_args['author'] ?? array(); + + /** @var array $tags */ + $tags = $query_args['tag'] ?? array(); + + /** @var array $sections */ + $sections = $query_args['section'] ?? array(); + + // Remove the author, tag, and section parameters from the query args. + unset( $query_args['author'] ); + unset( $query_args['tag'] ); + unset( $query_args['section'] ); + + // Generate the endpoint URL. + $endpoint_url = parent::get_endpoint_url( $query_args ); + + // Append the author, tag, and section parameters to the endpoint URL. + $endpoint_url = $this->append_multiple_params_to_url( $endpoint_url, $authors, 'author' ); + $endpoint_url = $this->append_multiple_params_to_url( $endpoint_url, $tags, 'tag' ); + $endpoint_url = $this->append_multiple_params_to_url( $endpoint_url, $sections, 'section' ); + + return $endpoint_url; + } + + /** + * Executes the API request. + * + * @since 3.17.0 + * + * @param array $args The arguments to pass to the API request. + * @return WP_Error|array The response from the API request. + */ + public function call( array $args = array() ) { + // Filter out the empty values. + $query_args = array_filter( $args ); + + // If the period_start is set to 'max_days', set it to the maximum days limit. + if ( self::MAX_PERIOD === $query_args['period_start'] ) { + $query_args['period_start'] = self::ANALYTICS_API_DAYS_LIMIT . 'd'; + } + + // If the limit is set to 'max' or greater than the maximum records limit, + // set it to the maximum records limit. + if ( self::MAX_LIMIT === $query_args['limit'] || $query_args['limit'] > self::MAX_RECORDS_LIMIT ) { + $query_args['limit'] = self::MAX_RECORDS_LIMIT; + } + + return $this->request( 'GET', $query_args ); + } + + + /** + * Appends multiple parameters to the URL. + * + * This is required because the Parsely API requires the multiple values for the author, tag, + * and section parameters to share the same key. + * + * @since 3.17.0 + * + * @param string $url The URL to append the parameters to. + * @param array $params The parameters to append. + * @param string $param_name The name of the parameter. + * @return string The URL with the appended parameters. + */ + protected function append_multiple_params_to_url( string $url, array $params, string $param_name ): string { + foreach ( $params as $param ) { + $param = rawurlencode( $param ); + if ( strpos( $url, $param_name . '=' ) === false ) { + $url = add_query_arg( $param_name, $param, $url ); + } else { + $url .= '&' . $param_name . '=' . $param; + } + } + + return $url; + } +} diff --git a/src/services/content-api/endpoints/class-endpoint-referrers-post-detail.php b/src/services/content-api/endpoints/class-endpoint-referrers-post-detail.php new file mode 100644 index 0000000000..1c233feddd --- /dev/null +++ b/src/services/content-api/endpoints/class-endpoint-referrers-post-detail.php @@ -0,0 +1,54 @@ + $args The arguments to pass to the API request. + * @return WP_Error|array The response from the API request. + */ + public function call( array $args = array() ) { + $query_args = array( + 'url' => $args['url'], + 'period_start' => $args['period_start'], + 'period_end' => $args['period_end'], + ); + + // Filter out the empty values. + $query_args = array_filter( $query_args ); + + return $this->request( 'GET', $query_args ); + } +} diff --git a/src/services/content-api/endpoints/class-endpoint-related.php b/src/services/content-api/endpoints/class-endpoint-related.php new file mode 100644 index 0000000000..cd547ae843 --- /dev/null +++ b/src/services/content-api/endpoints/class-endpoint-related.php @@ -0,0 +1,53 @@ + $args The arguments to pass to the API request. + * @return WP_Error|array The response from the API request. + */ + public function call( array $args = array() ) { + // Filter out the empty values. + $args = array_filter( $args ); + + // When the URL is provided, the UUID cannot be provided. + if ( isset( $args['uuid'] ) && isset( $args['url'] ) ) { + unset( $args['uuid'] ); + } + + return $this->request( 'GET', $args ); + } +} diff --git a/src/services/content-api/endpoints/class-endpoint-validate.php b/src/services/content-api/endpoints/class-endpoint-validate.php new file mode 100644 index 0000000000..8167c837df --- /dev/null +++ b/src/services/content-api/endpoints/class-endpoint-validate.php @@ -0,0 +1,112 @@ + $args The query arguments to send to the remote API. + * @return array The query arguments for the API request. + */ + public function get_query_args( array $args = array() ): array { + return $args; + } + + /** + * Queries the Parse.ly API credentials validation endpoint. + * + * The API will return a 200 response if the credentials are valid and a 403 + * response if they are not. + * + * @since 3.17.0 + * + * @param string $api_key The API key to validate. + * @param string $secret_key The secret key to validate. + * @return array|WP_Error The response from the remote API, or a WP_Error object if the response is an error. + */ + private function api_validate_credentials( string $api_key, string $secret_key ) { + $query = array( + 'apikey' => $api_key, + 'secret' => $secret_key, + ); + + $response = $this->request( 'GET', $query ); + + if ( is_wp_error( $response ) ) { + return $response; + } + + if ( false === $response['success'] ) { + return new WP_Error( + $response['code'] ?? 403, + $response['message'] ?? __( 'Unable to validate the API credentials', 'wp-parsely' ) + ); + } + + return $response; + } + + /** + * Processes the response from the remote API. + * + * @since 3.17.0 + * + * @param WP_HTTP_Response|WP_Error $response The response from the remote API. + * @return array|WP_Error The processed response. + */ + protected function process_response( $response ) { + return Base_Service_Endpoint::process_response( $response ); + } + + /** + * Executes the API request. + * + * @since 3.17.0 + * + * @param array $args The arguments to pass to the API request. + * @return WP_Error|array The response from the API request. + */ + public function call( array $args = array() ) { + /** @var string $api_key */ + $api_key = $args['apikey']; + /** @var string $secret */ + $secret = $args['secret']; + + return $this->api_validate_credentials( $api_key, $secret ); + } +} diff --git a/src/services/suggestions-api/class-suggestions-api-service.php b/src/services/suggestions-api/class-suggestions-api-service.php new file mode 100644 index 0000000000..8f72454671 --- /dev/null +++ b/src/services/suggestions-api/class-suggestions-api-service.php @@ -0,0 +1,115 @@ +register_endpoint( $endpoint ); + } + } + + /** + * Gets the first brief (meta description) for a given content using the + * Parse.ly Content Suggestion API. + * + * @since 3.13.0 + * @since 3.17.0 Updated to use the new API service. + * + * @param string $title The title of the content. + * @param string $content The query arguments to send to the remote API. + * @param Endpoint_Suggest_Brief_Options $options The options to pass to the API request. + * @return array|WP_Error The response from the remote API, or a WP_Error + * object if the response is an error. + */ + public function get_brief_suggestions( string $title, string $content, $options = array() ) { + /** @var Endpoints\Endpoint_Suggest_Brief $endpoint */ + $endpoint = $this->get_endpoint( '/suggest-brief' ); + + return $endpoint->get_suggestion( $title, $content, $options ); + } + + /** + * Gets titles (headlines) for a given content using the Parse.ly Content + * Suggestion API. + * + * @since 3.13.0 + * @since 3.17.0 Updated to use the new API service. + * + * @param string $content The query arguments to send to the remote API. + * @param Endpoint_Suggest_Headline_Options $options The options to pass to the API request. + * @return array|WP_Error The response from the remote API, or a WP_Error + * object if the response is an error. + */ + public function get_title_suggestions( string $content, $options = array() ) { + /** @var Endpoints\Endpoint_Suggest_Headline $endpoint */ + $endpoint = $this->get_endpoint( '/suggest-headline' ); + + return $endpoint->get_headlines( $content, $options ); + } + + /** + * Gets suggested smart links for the given content. + * + * @since 3.14.0 + * @since 3.17.0 Updated to use the new API service. + * + * @param string $content The content to generate links for. + * @param Endpoint_Suggest_Linked_Reference_Options $options The options to pass to the API request. + * @param array $url_exclusion_list A list of URLs to exclude from the suggestions. + * + * @return array|WP_Error The response from the remote API, or a WP_Error + * object if the response is an error. + */ + public function get_smart_links( string $content, $options = array(), array $url_exclusion_list = array() ) { + /** @var Endpoints\Endpoint_Suggest_Linked_Reference $endpoint */ + $endpoint = $this->get_endpoint( '/suggest-linked-reference' ); + + return $endpoint->get_links( $content, $options, $url_exclusion_list ); + } +} diff --git a/src/services/suggestions-api/endpoints/class-endpoint-suggest-brief.php b/src/services/suggestions-api/endpoints/class-endpoint-suggest-brief.php new file mode 100644 index 0000000000..8e0d3a20cc --- /dev/null +++ b/src/services/suggestions-api/endpoints/class-endpoint-suggest-brief.php @@ -0,0 +1,93 @@ +|WP_Error The response from the remote API, or a WP_Error + * object if the response is an error. + */ + public function get_suggestion( + string $title, + string $content, + $options = array() + ) { + $request_body = array( + 'output_config' => array( + 'persona' => $options['persona'] ?? 'journalist', + 'style' => $options['style'] ?? 'neutral', + 'max_characters' => $options['max_characters'] ?? 160, + 'max_items' => $options['max_items'] ?? 1, + ), + 'title' => $title, + 'text' => wp_strip_all_tags( $content ), + ); + + /** @var array|WP_Error $response */ + $response = $this->request( 'POST', array(), $request_body ); + + return $response; + } + + /** + * Executes the API request. + * + * @since 3.17.0 + * + * @param array $args The arguments to pass to the API request. + * @return WP_Error|array The response from the API. + */ + public function call( array $args = array() ) { + /** @var string $title */ + $title = $args['title'] ?? ''; + /** @var string $content */ + $content = $args['content'] ?? ''; + /** @var Endpoint_Suggest_Brief_Options $options */ + $options = $args['options'] ?? array(); + + return $this->get_suggestion( $title, $content, $options ); + } +} diff --git a/src/services/suggestions-api/endpoints/class-endpoint-suggest-headline.php b/src/services/suggestions-api/endpoints/class-endpoint-suggest-headline.php new file mode 100644 index 0000000000..09b3e7c27c --- /dev/null +++ b/src/services/suggestions-api/endpoints/class-endpoint-suggest-headline.php @@ -0,0 +1,96 @@ + + * } + */ +class Endpoint_Suggest_Headline extends Suggestions_API_Base_Endpoint { + /** + * Returns the endpoint for the API request. + * + * @since 3.17.0 + * + * @return string The endpoint for the API request. + */ + public function get_endpoint(): string { + return '/suggest-headline'; + } + + /** + * Gets titles (headlines) for a given content using the Parse.ly Content + * Suggestion API. + * + * @since 3.13.0 + * @since 3.17.0 Updated to use the new API service. + * + * @param string $content The query arguments to send to the remote API. + * @param Endpoint_Suggest_Headline_Options $options The options to pass to the API request. + * @return array|WP_Error The response from the remote API, or a WP_Error + * object if the response is an error. + */ + public function get_headlines( + string $content, + $options = array() + ) { + $request_body = array( + 'output_config' => array( + 'persona' => $options['persona'] ?? 'journalist', + 'style' => $options['style'] ?? 'neutral', + 'max_items' => $options['max_items'] ?? 1, + ), + 'text' => wp_strip_all_tags( $content ), + ); + + /** @var array|WP_Error $response */ + $response = $this->request( 'POST', array(), $request_body ); + + return $response; + } + + /** + * Executes the API request. + * + * @since 3.17.0 + * + * @param array $args The arguments to pass to the API request. + * @return WP_Error|array The response from the API. + */ + public function call( array $args = array() ) { + /** @var string $title */ + $title = $args['title'] ?? ''; + /** @var string $content */ + $content = $args['content'] ?? ''; + /** @var Endpoint_Suggest_Headline_Options $options */ + $options = $args['options'] ?? array(); + + return $this->get_headlines( $content, $options ); + } +} diff --git a/src/services/suggestions-api/endpoints/class-endpoint-suggest-linked-reference.php b/src/services/suggestions-api/endpoints/class-endpoint-suggest-linked-reference.php new file mode 100644 index 0000000000..cb27e628b4 --- /dev/null +++ b/src/services/suggestions-api/endpoints/class-endpoint-suggest-linked-reference.php @@ -0,0 +1,117 @@ + + * } + */ +class Endpoint_Suggest_Linked_Reference extends Suggestions_API_Base_Endpoint { + /** + * Returns the endpoint for the API request. + * + * @since 3.17.0 + * + * @return string The endpoint for the API request. + */ + public function get_endpoint(): string { + return '/suggest-linked-reference'; + } + + /** + * Gets suggested smart links for the given content using the Parse.ly + * Content Suggestion API. + * + * @since 3.14.0 + * @since 3.17.0 Updated to use the new API service. + * + * @param string $content The content to generate links for. + * @param Endpoint_Suggest_Linked_Reference_Options $options The options to pass to the API request. + * @param array $url_exclusion_list A list of URLs to exclude from the suggestions. + * @return array|WP_Error The response from the remote API, or a WP_Error + * object if the response is an error. + */ + public function get_links( + string $content, + $options = array(), + array $url_exclusion_list = array() + ) { + $request_body = array( + 'output_config' => array( + 'max_link_words' => $options['max_link_words'] ?? 4, + 'max_items' => $options['max_items'] ?? 10, + ), + 'text' => wp_strip_all_tags( $content ), + ); + + if ( count( $url_exclusion_list ) > 0 ) { + $request_body['url_exclusion_list'] = $url_exclusion_list; + } + + $response = $this->request( 'POST', array(), $request_body ); + + if ( is_wp_error( $response ) ) { + return $response; + } + + // Convert the links to Smart_Link objects. + $links = array(); + foreach ( $response as $link ) { + $link = apply_filters( 'wp_parsely_suggest_linked_reference_link', $link ); + $link_obj = new Smart_Link( + esc_url( $link['canonical_url'] ), + esc_attr( $link['title'] ), + wp_kses_post( $link['text'] ), + $link['offset'] + ); + $links[] = $link_obj; + } + + return $links; + } + + /** + * Executes the API request. + * + * @since 3.17.0 + * + * @param array $args The arguments to pass to the API request. + * @return WP_Error|array The response from the API. + */ + public function call( array $args = array() ) { + /** @var string $content */ + $content = $args['content'] ?? ''; + /** @var Endpoint_Suggest_Linked_Reference_Options $options */ + $options = $args['options'] ?? array(); + /** @var string[] $url_exclusion_list */ + $url_exclusion_list = $args['url_exclusion_list'] ?? array(); + + return $this->get_links( $content, $options, $url_exclusion_list ); + } +} diff --git a/src/services/suggestions-api/endpoints/class-suggestions-api-base-endpoint.php b/src/services/suggestions-api/endpoints/class-suggestions-api-base-endpoint.php new file mode 100644 index 0000000000..daac090961 --- /dev/null +++ b/src/services/suggestions-api/endpoints/class-suggestions-api-base-endpoint.php @@ -0,0 +1,112 @@ + $method, + 'headers' => array( 'Content-Type' => 'application/json; charset=utf-8' ), + 'data_format' => 'body', + 'timeout' => 60, //phpcs:ignore WordPressVIPMinimum.Performance.RemoteRequestTimeout.timeout_timeout + 'body' => '{}', + ); + + // Add API key to request headers. + if ( $this->get_parsely()->api_secret_is_set() ) { + $options['headers']['X-APIKEY-SECRET'] = $this->get_parsely()->get_api_secret(); + } + + return $options; + } + + /** + * Returns the common query arguments to send to the remote API. + * + * This method appends the API key and secret to the query arguments. + * + * @since 3.17.0 + * + * @param array $args Additional query arguments to send to the remote API. + * @return array The query arguments to send to the remote API. + */ + protected function get_query_args( array $args = array() ): array { + $query_args = parent::get_query_args( $args ); + + // Set up the API key and secret. + $query_args['apikey'] = $this->get_parsely()->get_site_id(); + + return $query_args; + } + + /** + * Processes the response from the remote API. + * + * @since 3.17.0 + * + * @param WP_HTTP_Response|WP_Error $response The response from the remote API. + * @return array|WP_Error The processed response. + */ + protected function process_response( $response ) { + if ( is_wp_error( $response ) ) { + /** @var WP_Error $response */ + return $response; + } + + // Handle any errors returned by the API. + if ( 200 !== $response['response']['code'] ) { + $error = json_decode( wp_remote_retrieve_body( $response ), true ); + + if ( ! is_array( $error ) ) { + return new WP_Error( + 400, + __( 'Unable to decode upstream API error', 'wp-parsely' ) + ); + } + + return new WP_Error( $error['error'], $error['detail'] ); + } + + $body = wp_remote_retrieve_body( $response ); + $decoded = json_decode( $body, true ); + + if ( ! is_array( $decoded ) ) { + return new WP_Error( 400, __( 'Unable to decode upstream API response', 'wp-parsely' ) ); + } + + if ( ! is_array( $decoded['result'] ) ) { + return new WP_Error( 400, __( 'Unable to parse data from upstream API', 'wp-parsely' ) ); + } + + return $decoded['result']; + } +} diff --git a/wp-parsely.php b/wp-parsely.php index 29997ea080..f1db9e67cc 100644 --- a/wp-parsely.php +++ b/wp-parsely.php @@ -57,6 +57,10 @@ require_once __DIR__ . '/vendor/autoload.php'; } +// Load Telemetry classes. +require_once __DIR__ . '/src/Telemetry/telemetry-init.php'; + + add_action( 'plugins_loaded', __NAMESPACE__ . '\\parsely_initialize_plugin' ); /** * Registers the basic classes to initialize the plugin. @@ -108,7 +112,7 @@ function parsely_wp_admin_early_register(): void { $network_admin_sites_list->run(); // Initialize the REST API Controller. - $rest_api_controller = new REST_API_Controller( $GLOBALS['parsely'] ); + $rest_api_controller = $GLOBALS['parsely']->get_rest_api_controller(); $rest_api_controller->init(); } From 95a2394ada007b119f66d1122e010c7331403c53 Mon Sep 17 00:00:00 2001 From: Henrique Mouta Date: Fri, 25 Oct 2024 09:08:48 +0100 Subject: [PATCH 45/49] PCH Refactor: Update integration tests (#2885) * Implement and update integration tests * Adjust some DocBlocks and whitespace * Adjust some DocBlocks and whitespace * Fix tests * Fix leaking tests * Apply code review suggestions * Remove backticks from function summary * Improve code formatting consistency between 2 tests --------- Co-authored-by: Alex Cicovic <23142906+acicovic@users.noreply.github.com> --- .../editor-sidebar/class-editor-sidebar.php | 5 - src/rest-api/stats/class-endpoint-post.php | 123 +++++++------ src/services/class-base-service-endpoint.php | 13 +- .../class-cached-service-endpoint.php | 41 +++++ .../content-api/class-content-api-service.php | 2 +- .../class-endpoint-analytics-posts.php | 6 +- .../class-suggestions-api-base-endpoint.php | 11 ++ .../ContentHelperDashboardWidgetTest.php | 43 ++++- .../ContentHelperFeatureTest.php | 42 ++--- .../ContentHelperPostListStatsTest.php | 50 ++++-- .../RemoteAPI/AnalyticsPostsRemoteAPITest.php | 128 -------------- .../RemoteAPI/BaseRemoteAPITest.php | 163 ------------------ .../BaseContentSuggestionsAPITest.php | 129 -------------- .../SuggestBriefAPITest.php | 137 --------------- .../RemoteAPI/RelatedRemoteAPITest.php | 94 ---------- .../RestAPI/BaseAPIControllerTest.php | 99 +++++++++-- .../Integration/RestAPI/BaseEndpointTest.php | 30 +--- .../ContentHelperControllerTest.php | 8 +- .../ContentHelperFeatureTestTrait.php | 13 +- .../EndpointExcerptGeneratorTest.php | 39 +++-- .../EndpointSmartLinkingTest.php | 29 ++-- .../EndpointTitleSuggestionsTest.php | 77 ++++----- .../RestAPI/Stats/EndpointPostTest.php | 50 ++---- .../RestAPI/Stats/EndpointPostsTest.php | 8 - .../RestAPI/Stats/EndpointRelatedTest.php | 15 +- .../Services/BaseAPIServiceTestCase.php | 57 ++++++ .../Services/BaseServiceEndpointTestCase.php | 136 +++++++++++++++ .../ContentAPI/ContentApiServiceTestCase.php | 133 ++++++++++++++ .../ContentAPIBaseEndpointTestCase.php | 129 ++++++++++++++ .../Endpoints/EndpointAnalyticsPostsTest.php | 48 ++++++ .../Endpoints/EndpointRelatedTest.php | 54 ++++++ .../Endpoints/EndpointSuggestBriefTest.php | 132 ++++++++++++++ .../EndpointSuggestHeadlineTest.php} | 64 ++++--- .../EndpointSuggestLinkedReferenceTest.php} | 81 +++++---- .../SuggestionsAPIBaseEndpointTestCase.php | 131 ++++++++++++++ .../SuggestionsApiServiceTestCase.php | 115 ++++++++++++ tests/Traits/TestsReflection.php | 67 +++++-- 37 files changed, 1459 insertions(+), 1043 deletions(-) delete mode 100644 tests/Integration/RemoteAPI/AnalyticsPostsRemoteAPITest.php delete mode 100644 tests/Integration/RemoteAPI/BaseRemoteAPITest.php delete mode 100644 tests/Integration/RemoteAPI/ContentSuggestions/BaseContentSuggestionsAPITest.php delete mode 100644 tests/Integration/RemoteAPI/ContentSuggestions/SuggestBriefAPITest.php delete mode 100644 tests/Integration/RemoteAPI/RelatedRemoteAPITest.php create mode 100644 tests/Integration/Services/BaseAPIServiceTestCase.php create mode 100644 tests/Integration/Services/BaseServiceEndpointTestCase.php create mode 100644 tests/Integration/Services/ContentAPI/ContentApiServiceTestCase.php create mode 100644 tests/Integration/Services/ContentAPI/Endpoints/ContentAPIBaseEndpointTestCase.php create mode 100644 tests/Integration/Services/ContentAPI/Endpoints/EndpointAnalyticsPostsTest.php create mode 100644 tests/Integration/Services/ContentAPI/Endpoints/EndpointRelatedTest.php create mode 100644 tests/Integration/Services/SuggestionsAPI/Endpoints/EndpointSuggestBriefTest.php rename tests/Integration/{RemoteAPI/ContentSuggestions/SuggestHeadlineAPITest.php => Services/SuggestionsAPI/Endpoints/EndpointSuggestHeadlineTest.php} (57%) rename tests/Integration/{RemoteAPI/ContentSuggestions/SuggestLinkedReferenceAPITest.php => Services/SuggestionsAPI/Endpoints/EndpointSuggestLinkedReferenceTest.php} (57%) create mode 100644 tests/Integration/Services/SuggestionsAPI/Endpoints/SuggestionsAPIBaseEndpointTestCase.php create mode 100644 tests/Integration/Services/SuggestionsAPI/SuggestionsApiServiceTestCase.php diff --git a/src/content-helper/editor-sidebar/class-editor-sidebar.php b/src/content-helper/editor-sidebar/class-editor-sidebar.php index 8f732bde0e..7b2256aa3d 100644 --- a/src/content-helper/editor-sidebar/class-editor-sidebar.php +++ b/src/content-helper/editor-sidebar/class-editor-sidebar.php @@ -20,11 +20,6 @@ use const Parsely\PARSELY_FILE; -/** - * Features requires for the PCH Editor Sidebar. - */ -require_once __DIR__ . '/smart-linking/class-smart-linking.php'; - /** * Class that generates and manages the PCH Editor Sidebar. * diff --git a/src/rest-api/stats/class-endpoint-post.php b/src/rest-api/stats/class-endpoint-post.php index c861f8d571..2789d259c7 100644 --- a/src/rest-api/stats/class-endpoint-post.php +++ b/src/rest-api/stats/class-endpoint-post.php @@ -27,6 +27,25 @@ * posts for a given post. * * @since 3.17.0 + * + * @phpstan-type Referrer_Data array{ + * metrics: array{ + * referrers_views?: int + * }, + * type?: string, + * name?: string + * } + * + * @phpstan-type Referrer_Type_Data array{ + * views: string, + * viewsPercentage: string + * } + * + * @phpstan-type Referrers_Data_Item array{ + * views: string, + * viewsPercentage: string, + * datasetViewsPercentage: string + * } */ class Endpoint_Post extends Base_Endpoint { use Use_Post_ID_Parameter_Trait; @@ -250,11 +269,11 @@ public function get_post_referrers( WP_REST_Request $request ) { /** * The analytics data object. * - * @var array $analytics_request + * @var array $analytics_request */ $referrers_types = $this->generate_referrer_types_data( $analytics_request ); $direct_views = Utils::convert_to_positive_integer( - $referrers_types->direct->views ?? '0' + $referrers_types['direct']['views'] ?? '0' ); $referrers_top = $this->generate_referrers_data( 5, $analytics_request, $direct_views ); @@ -314,73 +333,68 @@ public function get_related_posts( WP_REST_Request $request ) { * - `internal`: Views coming from linking pages of the same website. * * Returned object properties: - * - `views`: The number of views. - * - `viewPercentage`: The number of views as a percentage, compared to the - * total views of all referrer types. + * - `views`: The number of views. + * - `viewsPercentage`: The number of views as a percentage, compared to the + * total views of all referrer types. * * @since 3.6.0 * @since 3.17.0 Moved from the `Referrers_Post_Detail_API_Proxy` class. * - * @param array $response The response received by the proxy. - * @return stdClass The generated data. + * @param array $response The response received by the proxy. + * @return array The generated data. */ - private function generate_referrer_types_data( array $response ): stdClass { - $result = new stdClass(); + private function generate_referrer_types_data( array $response ): array { + $result = array(); $total_referrer_views = 0; // Views from all referrer types combined. // Set referrer type order as it is displayed in the Parse.ly dashboard. $referrer_type_keys = array( 'social', 'search', 'other', 'internal', 'direct' ); foreach ( $referrer_type_keys as $key ) { - $result->$key = (object) array( 'views' => 0 ); + $result[ $key ] = array( 'views' => 0 ); } // Set views and views totals. foreach ( $response as $referrer_data ) { /** - * Variable. - * - * @var int + * @var int $current_views */ - $current_views = $referrer_data->metrics->referrers_views ?? 0; + $current_views = $referrer_data['metrics']['referrers_views'] ?? 0; $total_referrer_views += $current_views; /** - * Variable. - * - * @var string + * @var string $current_key */ - $current_key = $referrer_data->type ?? ''; + $current_key = $referrer_data['type'] ?? ''; if ( '' !== $current_key ) { - if ( ! isset( $result->$current_key->views ) ) { - $result->$current_key = (object) array( 'views' => 0 ); + if ( ! isset( $result[ $current_key ]['views'] ) ) { + $result[ $current_key ] = array( 'views' => 0 ); } - $result->$current_key->views += $current_views; + $result[ $current_key ]['views'] += $current_views; } } // Add direct and total views to the object. - $result->direct->views = $this->total_views - $total_referrer_views; - $result->totals = (object) array( 'views' => $this->total_views ); + $result['direct']['views'] = $this->total_views - $total_referrer_views; + $result['totals'] = array( 'views' => $this->total_views ); // Remove referrer types without views. foreach ( $referrer_type_keys as $key ) { - if ( 0 === $result->$key->views ) { - unset( $result->$key ); + if ( 0 === $result[ $key ]['views'] ) { + unset( $result[ $key ] ); } } // Set percentage values and format numbers. - // @phpstan-ignore-next-line. foreach ( $result as $key => $value ) { // Set and format percentage values. - $result->{ $key }->viewsPercentage = $this->get_i18n_percentage( - absint( $value->views ), + $result[ $key ]['viewsPercentage'] = $this->get_i18n_percentage( + absint( $value['views'] ), $this->total_views ); // Format views values. - $result->{ $key }->views = number_format_i18n( $result->{ $key }->views ); + $result[ $key ]['views'] = number_format_i18n( $result[ $key ]['views'] ); } return $result; @@ -390,25 +404,25 @@ private function generate_referrer_types_data( array $response ): stdClass { * Generates the top referrers data. * * Returned object properties: - * - `views`: The number of views. - * - `viewPercentage`: The number of views as a percentage, compared to the - * total views of all referrer types. - * - `datasetViewsPercentage: The number of views as a percentage, compared - * to the total views of the current dataset. + * - `views`: The number of views. + * - `viewsPercentage`: The number of views as a percentage, compared to the + * total views of all referrer types. + * - `datasetViewsPercentage`: The number of views as a percentage, compared + * to the total views of the current dataset. * * @since 3.6.0 * @since 3.17.0 Moved from the `Referrers_Post_Detail_API_Proxy` class. * - * @param int $limit The limit of returned referrers. - * @param array $response The response received by the proxy. - * @param int $direct_views The count of direct views. - * @return stdClass The generated data. + * @param int $limit The limit of returned referrers. + * @param array $response The response received by the proxy. + * @param int $direct_views The count of direct views. + * @return array The generated data. */ private function generate_referrers_data( int $limit, array $response, int $direct_views - ): stdClass { + ): array { $temp_views = array(); $totals = 0; $referrer_count = count( $response ); @@ -419,14 +433,12 @@ private function generate_referrers_data( $data = $response[ $i ]; /** - * Variable. - * - * @var int + * @var int $referrer_views */ - $referrer_views = $data->metrics->referrers_views ?? 0; + $referrer_views = $data['metrics']['referrers_views'] ?? 0; $totals += $referrer_views; - if ( isset( $data->name ) ) { - $temp_views[ $data->name ] = $referrer_views; + if ( isset( $data['name'] ) ) { + $temp_views[ $data['name'] ] = $referrer_views; } } @@ -441,27 +453,26 @@ private function generate_referrers_data( } // Convert temporary array to result object and add totals. - $result = new stdClass(); + $result = array(); foreach ( $temp_views as $key => $value ) { - $result->$key = (object) array( 'views' => $value ); + $result[ $key ] = array( 'views' => $value ); } - $result->totals = (object) array( 'views' => $totals ); + $result['totals'] = array( 'views' => $totals ); - // Set percentages values and format numbers. - // @phpstan-ignore-next-line. + // Set percentage values and format numbers. foreach ( $result as $key => $value ) { // Percentage against all referrer views, even those not included // in the dataset due to the $limit argument. - $result->{ $key }->viewsPercentage = $this - ->get_i18n_percentage( absint( $value->views ), $this->total_views ); + $result[ $key ]['viewsPercentage'] = $this + ->get_i18n_percentage( absint( $value['views'] ), $this->total_views ); // Percentage against the current dataset that is limited due to the // $limit argument. - $result->{ $key }->datasetViewsPercentage = $this - ->get_i18n_percentage( absint( $value->views ), $totals ); + $result[ $key ]['datasetViewsPercentage'] = $this + ->get_i18n_percentage( absint( $value['views'] ), $totals ); // Format views values. - $result->{ $key }->views = number_format_i18n( $result->{ $key }->views ); + $result[ $key ]['views'] = number_format_i18n( $result[ $key ]['views'] ); } return $result; @@ -475,7 +486,7 @@ private function generate_referrers_data( * @since 3.17.0 Moved from the `Referrers_Post_Detail_API_Proxy` class. * * @param int $number The number to be calculated as a percentage. - * @param int $total The total number to compare against. + * @param int $total The total number to compare against. * @return string|false The internationalized percentage or false on error. */ private function get_i18n_percentage( int $number, int $total ) { diff --git a/src/services/class-base-service-endpoint.php b/src/services/class-base-service-endpoint.php index ef5245d9c0..2077c57786 100644 --- a/src/services/class-base-service-endpoint.php +++ b/src/services/class-base-service-endpoint.php @@ -180,7 +180,9 @@ protected function request( string $method, array $query_args = array(), array $ $request_options = $this->get_request_options( $method ); if ( count( $data ) > 0 ) { - $data = $this->truncate_array_content( $data ); + if ( true === static::TRUNCATE_CONTENT ) { + $data = $this->truncate_array_content( $data ); + } $request_options['body'] = wp_json_encode( $data ); if ( false === $request_options['body'] ) { @@ -247,12 +249,9 @@ private function truncate_array_content( $content ) { } return $content; } elseif ( is_string( $content ) ) { - // If the content is a string, truncate it. - if ( static::TRUNCATE_CONTENT ) { - // Check if the string length exceeds the maximum and truncate if necessary. - if ( mb_strlen( $content ) > self::TRUNCATE_CONTENT_LENGTH ) { - return mb_substr( $content, 0, self::TRUNCATE_CONTENT_LENGTH ); - } + // Check if the string length exceeds the maximum and truncate if necessary. + if ( mb_strlen( $content ) > self::TRUNCATE_CONTENT_LENGTH ) { + return mb_substr( $content, 0, self::TRUNCATE_CONTENT_LENGTH ); } return $content; } diff --git a/src/services/class-cached-service-endpoint.php b/src/services/class-cached-service-endpoint.php index fc19d15615..debdb63d9b 100644 --- a/src/services/class-cached-service-endpoint.php +++ b/src/services/class-cached-service-endpoint.php @@ -19,6 +19,8 @@ * from the API request. * * @since 3.17.0 + * + * @phpstan-import-type WP_HTTP_Request_Args from Base_Service_Endpoint */ class Cached_Service_Endpoint extends Base_Service_Endpoint { /** @@ -121,4 +123,43 @@ public function call( array $args = array() ) { public function get_endpoint(): string { return $this->service_endpoint->get_endpoint(); } + + /** + * Returns the uncached endpoint for the API request. + * + * @since 3.17.0 + * + * @return Base_Service_Endpoint The uncached endpoint for the API request. + */ + public function get_uncached_endpoint(): Base_Service_Endpoint { + return $this->service_endpoint; + } + + /** + * Returns the request options for the remote API request. + * + * Gets the request options from the uncached service endpoint. + * + * @since 3.17.0 + * + * @param string $method The HTTP method to use for the request. + * @return WP_HTTP_Request_Args The request options for the remote API request. + */ + protected function get_request_options( string $method ): array { + return $this->service_endpoint->get_request_options( $method ); + } + + /** + * Returns the common query arguments to send to the remote API. + * + * Gets the query arguments from the uncached service endpoint. + * + * @since 3.17.0 + * + * @param array $args Additional query arguments to send to the remote API. + * @return array The query arguments to send to the remote API. + */ + protected function get_query_args( array $args = array() ): array { + return $this->service_endpoint->get_query_args( $args ); + } } diff --git a/src/services/content-api/class-content-api-service.php b/src/services/content-api/class-content-api-service.php index b2e165a085..83ceeb825c 100644 --- a/src/services/content-api/class-content-api-service.php +++ b/src/services/content-api/class-content-api-service.php @@ -42,7 +42,7 @@ protected function register_endpoints(): void { /** * The endpoints for the Parse.ly Content API. * - * @var Base_Service_Endpoint[] $endpoints + * @var array $endpoints */ $endpoints = array( new Endpoints\Endpoint_Validate( $this ), diff --git a/src/services/content-api/endpoints/class-endpoint-analytics-posts.php b/src/services/content-api/endpoints/class-endpoint-analytics-posts.php index 3c280397cc..b8ebc61688 100644 --- a/src/services/content-api/endpoints/class-endpoint-analytics-posts.php +++ b/src/services/content-api/endpoints/class-endpoint-analytics-posts.php @@ -138,13 +138,15 @@ public function call( array $args = array() ) { $query_args = array_filter( $args ); // If the period_start is set to 'max_days', set it to the maximum days limit. - if ( self::MAX_PERIOD === $query_args['period_start'] ) { + if ( isset( $query_args['period_start'] ) && self::MAX_PERIOD === $query_args['period_start'] ) { $query_args['period_start'] = self::ANALYTICS_API_DAYS_LIMIT . 'd'; } // If the limit is set to 'max' or greater than the maximum records limit, // set it to the maximum records limit. - if ( self::MAX_LIMIT === $query_args['limit'] || $query_args['limit'] > self::MAX_RECORDS_LIMIT ) { + if ( isset( $query_args['limit'] ) && ( + self::MAX_LIMIT === $query_args['limit'] || $query_args['limit'] > self::MAX_RECORDS_LIMIT ) + ) { $query_args['limit'] = self::MAX_RECORDS_LIMIT; } diff --git a/src/services/suggestions-api/endpoints/class-suggestions-api-base-endpoint.php b/src/services/suggestions-api/endpoints/class-suggestions-api-base-endpoint.php index daac090961..79c7e2cb32 100644 --- a/src/services/suggestions-api/endpoints/class-suggestions-api-base-endpoint.php +++ b/src/services/suggestions-api/endpoints/class-suggestions-api-base-endpoint.php @@ -24,6 +24,17 @@ * @phpstan-import-type WP_HTTP_Request_Args from Base_Service_Endpoint */ abstract class Suggestions_API_Base_Endpoint extends Base_Service_Endpoint { + /** + * Flag to truncate the content of the request body. + * + * By setting it to true, the content of the request body will be truncated to a maximum length. + * + * @since 3.17.0 + * + * @var bool + */ + protected const TRUNCATE_CONTENT = true; + /** * Returns the request options for the remote API request. * diff --git a/tests/Integration/ContentHelper/ContentHelperDashboardWidgetTest.php b/tests/Integration/ContentHelper/ContentHelperDashboardWidgetTest.php index fb63354ccb..7109879c3b 100644 --- a/tests/Integration/ContentHelper/ContentHelperDashboardWidgetTest.php +++ b/tests/Integration/ContentHelper/ContentHelperDashboardWidgetTest.php @@ -11,16 +11,48 @@ use Parsely\Content_Helper\Dashboard_Widget; use Parsely\Parsely; +use Parsely\Tests\Integration\TestCase; /** * Integration Tests for the PCH Dashboard Widget. */ final class ContentHelperDashboardWidgetTest extends ContentHelperFeatureTest { + /** + * Internal variable. + * + * @since 3.17.0 + * + * @var Parsely $parsely Holds the Parsely object. + */ + private static $parsely; + /** * Setup method called before each test. + * + * @since 3.17.0 */ public function set_up(): void { - $GLOBALS['parsely'] = new Parsely(); + parent::set_up(); + + self::$parsely = new Parsely(); + self::$parsely->get_rest_api_controller()->init(); + + TestCase::set_options( + array( + 'apikey' => 'test_apikey', + 'api_secret' => 'test_secret', + ) + ); + } + + /** + * Teardown method called after each test. + * + * @since 3.17.0 + */ + public function tear_down(): void { + parent::tear_down(); + TestCase::set_options(); } /** @@ -44,7 +76,7 @@ protected function assert_enqueued_status( string $user_role, array $additional_args = array() ): void { - $feature = new Dashboard_Widget( $GLOBALS['parsely'] ); + $feature = new Dashboard_Widget( self::$parsely ); self::set_current_user_to( $user_login, $user_role ); parent::set_filters( @@ -85,11 +117,8 @@ protected function assert_enqueued_status( * @covers \Parsely\Content_Helper\Dashboard_Widget::get_script_id * @covers \Parsely\Content_Helper\Dashboard_Widget::get_style_id * @covers \Parsely\Content_Helper\Dashboard_Widget::run - * @covers \Parsely\RemoteAPI\Analytics_Posts_API::is_available_to_current_user * @uses \Parsely\Parsely::__construct - * @uses \Parsely\Endpoints\Base_Endpoint::__construct * @uses \Parsely\Utils\Utils::convert_endpoint_to_filter_key - * @uses \Parsely\Utils::get_asset_info * * @group content-helper */ @@ -112,11 +141,9 @@ public function test_assets_do_not_get_enqueued_when_user_has_not_enough_capabil * @covers \Parsely\Content_Helper\Dashboard_Widget::get_script_id * @covers \Parsely\Content_Helper\Dashboard_Widget::get_style_id * @covers \Parsely\Content_Helper\Dashboard_Widget::run - * @covers \Parsely\RemoteAPI\Analytics_Posts_API::is_available_to_current_user * @uses \Parsely\Parsely::__construct - * @uses \Parsely\Endpoints\Base_Endpoint::__construct * @uses \Parsely\Utils\Utils::convert_endpoint_to_filter_key - * @uses \Parsely\Utils::get_asset_info + * @uses \Parsely\Utils\Utils::get_asset_info * * @group content-helper */ diff --git a/tests/Integration/ContentHelper/ContentHelperFeatureTest.php b/tests/Integration/ContentHelper/ContentHelperFeatureTest.php index da6dd0e315..45f4700c5e 100644 --- a/tests/Integration/ContentHelper/ContentHelperFeatureTest.php +++ b/tests/Integration/ContentHelper/ContentHelperFeatureTest.php @@ -172,7 +172,6 @@ protected static function deregister_feature_assets_and_run( * @covers \Parsely\Content_Helper\Post_List_Stats::set_current_screen * @uses \Parsely\Content_Helper\Content_Helper_Feature::get_credentials_not_set_message * @uses \Parsely\Content_Helper\Content_Helper_Feature::inject_inline_scripts - * @uses \Parsely\Endpoints\Base_Endpoint::__construct * @uses \Parsely\Parsely::__construct * @uses \Parsely\Parsely::allow_parsely_remote_requests * @uses \Parsely\Parsely::api_secret_is_set @@ -184,7 +183,7 @@ protected static function deregister_feature_assets_and_run( * @uses \Parsely\Parsely::set_managed_options * @uses \Parsely\Parsely::site_id_is_set * @uses \Parsely\Utils\Utils::convert_endpoint_to_filter_key - * @uses \Parsely\Utils::get_asset_info + * @uses \Parsely\Utils\Utils::get_asset_info * * @group content-helper */ @@ -229,7 +228,6 @@ public function test_assets_get_enqueued_by_default(): void { * @covers \Parsely\Content_Helper\Post_List_Stats::set_current_screen * @uses \Parsely\Content_Helper\Content_Helper_Feature::get_credentials_not_set_message * @uses \Parsely\Content_Helper\Content_Helper_Feature::inject_inline_scripts - * @uses \Parsely\Endpoints\Base_Endpoint::__construct * @uses \Parsely\Parsely::__construct * @uses \Parsely\Parsely::allow_parsely_remote_requests * @uses \Parsely\Parsely::api_secret_is_set @@ -241,7 +239,7 @@ public function test_assets_get_enqueued_by_default(): void { * @uses \Parsely\Parsely::set_managed_options * @uses \Parsely\Parsely::site_id_is_set * @uses \Parsely\Utils\Utils::convert_endpoint_to_filter_key - * @uses \Parsely\Utils::get_asset_info + * @uses \Parsely\Utils\Utils::get_asset_info * * @group content-helper */ @@ -277,7 +275,6 @@ public function test_assets_get_enqueued_when_global_filter_is_true(): void { * @covers \Parsely\Content_Helper\Post_List_Stats::__construct * @covers \Parsely\Content_Helper\Post_List_Stats::get_feature_filter_name * @covers \Parsely\Content_Helper\Post_List_Stats::run - * @uses \Parsely\Endpoints\Base_Endpoint::__construct * @uses \Parsely\Parsely::__construct * @uses \Parsely\Parsely::allow_parsely_remote_requests * @uses \Parsely\Parsely::api_secret_is_set @@ -286,7 +283,7 @@ public function test_assets_get_enqueued_when_global_filter_is_true(): void { * @uses \Parsely\Parsely::set_managed_options * @uses \Parsely\Parsely::site_id_is_set * @uses \Parsely\Utils\Utils::convert_endpoint_to_filter_key - * @uses \Parsely\Utils::get_asset_info + * @uses \Parsely\Utils\Utils::get_asset_info * * @group content-helper */ @@ -322,7 +319,6 @@ public function test_assets_do_not_get_enqueued_when_global_filter_is_false(): v * @covers \Parsely\Content_Helper\Post_List_Stats::__construct * @covers \Parsely\Content_Helper\Post_List_Stats::get_feature_filter_name * @covers \Parsely\Content_Helper\Post_List_Stats::run - * @uses \Parsely\Endpoints\Base_Endpoint::__construct * @uses \Parsely\Parsely::__construct * @uses \Parsely\Parsely::allow_parsely_remote_requests * @uses \Parsely\Parsely::api_secret_is_set @@ -331,7 +327,7 @@ public function test_assets_do_not_get_enqueued_when_global_filter_is_false(): v * @uses \Parsely\Parsely::set_managed_options * @uses \Parsely\Parsely::site_id_is_set * @uses \Parsely\Utils\Utils::convert_endpoint_to_filter_key - * @uses \Parsely\Utils::get_asset_info + * @uses \Parsely\Utils\Utils::get_asset_info * * @group content-helper */ @@ -377,7 +373,6 @@ public function test_assets_do_not_get_enqueued_when_global_filter_is_invalid(): * @covers \Parsely\Content_Helper\Post_List_Stats::set_current_screen * @uses \Parsely\Content_Helper\Content_Helper_Feature::get_credentials_not_set_message * @uses \Parsely\Content_Helper\Content_Helper_Feature::inject_inline_scripts - * @uses \Parsely\Endpoints\Base_Endpoint::__construct * @uses \Parsely\Parsely::__construct * @uses \Parsely\Parsely::allow_parsely_remote_requests * @uses \Parsely\Parsely::api_secret_is_set @@ -389,7 +384,7 @@ public function test_assets_do_not_get_enqueued_when_global_filter_is_invalid(): * @uses \Parsely\Parsely::set_managed_options * @uses \Parsely\Parsely::site_id_is_set * @uses \Parsely\Utils\Utils::convert_endpoint_to_filter_key - * @uses \Parsely\Utils::get_asset_info + * @uses \Parsely\Utils\Utils::get_asset_info * * @group content-helper */ @@ -425,7 +420,6 @@ public function test_assets_get_enqueued_when_feature_filter_is_true(): void { * @covers \Parsely\Content_Helper\Post_List_Stats::__construct * @covers \Parsely\Content_Helper\Post_List_Stats::get_feature_filter_name * @covers \Parsely\Content_Helper\Post_List_Stats::run - * @uses \Parsely\Endpoints\Base_Endpoint::__construct * @uses \Parsely\Parsely::__construct * @uses \Parsely\Parsely::allow_parsely_remote_requests * @uses \Parsely\Parsely::api_secret_is_set @@ -434,7 +428,7 @@ public function test_assets_get_enqueued_when_feature_filter_is_true(): void { * @uses \Parsely\Parsely::set_managed_options * @uses \Parsely\Parsely::site_id_is_set * @uses \Parsely\Utils\Utils::convert_endpoint_to_filter_key - * @uses \Parsely\Utils::get_asset_info + * @uses \Parsely\Utils\Utils::get_asset_info * * @group content-helper */ @@ -470,7 +464,6 @@ public function test_assets_do_not_get_enqueued_when_feature_filter_is_false(): * @covers \Parsely\Content_Helper\Post_List_Stats::__construct * @covers \Parsely\Content_Helper\Post_List_Stats::get_feature_filter_name * @covers \Parsely\Content_Helper\Post_List_Stats::run - * @uses \Parsely\Endpoints\Base_Endpoint::__construct * @uses \Parsely\Parsely::__construct * @uses \Parsely\Parsely::allow_parsely_remote_requests * @uses \Parsely\Parsely::api_secret_is_set @@ -479,7 +472,7 @@ public function test_assets_do_not_get_enqueued_when_feature_filter_is_false(): * @uses \Parsely\Parsely::set_managed_options * @uses \Parsely\Parsely::site_id_is_set * @uses \Parsely\Utils\Utils::convert_endpoint_to_filter_key - * @uses \Parsely\Utils::get_asset_info + * @uses \Parsely\Utils\Utils::get_asset_info * * @group content-helper */ @@ -524,7 +517,6 @@ public function test_assets_do_not_get_enqueued_when_feature_filter_is_invalid() * @covers \Parsely\Content_Helper\Post_List_Stats::set_current_screen * @uses \Parsely\Content_Helper\Content_Helper_Feature::get_credentials_not_set_message * @uses \Parsely\Content_Helper\Content_Helper_Feature::inject_inline_scripts - * @uses \Parsely\Endpoints\Base_Endpoint::__construct * @uses \Parsely\Parsely::__construct * @uses \Parsely\Parsely::allow_parsely_remote_requests * @uses \Parsely\Parsely::api_secret_is_set @@ -536,7 +528,7 @@ public function test_assets_do_not_get_enqueued_when_feature_filter_is_invalid() * @uses \Parsely\Parsely::set_managed_options * @uses \Parsely\Parsely::site_id_is_set * @uses \Parsely\Utils\Utils::convert_endpoint_to_filter_key - * @uses \Parsely\Utils::get_asset_info + * @uses \Parsely\Utils\Utils::get_asset_info * * @group content-helper */ @@ -579,7 +571,6 @@ public function test_assets_get_enqueued_when_both_filters_are_true(): void { * @covers \Parsely\Content_Helper\Post_List_Stats::is_tracked_as_post_type * @covers \Parsely\Content_Helper\Post_List_Stats::run * @covers \Parsely\Content_Helper\Post_List_Stats::set_current_screen - * @uses \Parsely\Endpoints\Base_Endpoint::__construct * @uses \Parsely\Parsely::__construct * @uses \Parsely\Parsely::allow_parsely_remote_requests * @uses \Parsely\Parsely::api_secret_is_set @@ -588,7 +579,7 @@ public function test_assets_get_enqueued_when_both_filters_are_true(): void { * @uses \Parsely\Parsely::set_managed_options * @uses \Parsely\Parsely::site_id_is_set * @uses \Parsely\Utils\Utils::convert_endpoint_to_filter_key - * @uses \Parsely\Utils::get_asset_info + * @uses \Parsely\Utils\Utils::get_asset_info * * @group content-helper */ @@ -624,7 +615,6 @@ public function test_assets_do_not_get_enqueued_when_both_filters_are_false(): v * @covers \Parsely\Content_Helper\Post_List_Stats::__construct * @covers \Parsely\Content_Helper\Post_List_Stats::get_feature_filter_name * @covers \Parsely\Content_Helper\Post_List_Stats::run - * @uses \Parsely\Endpoints\Base_Endpoint::__construct * @uses \Parsely\Parsely::__construct * @uses \Parsely\Parsely::allow_parsely_remote_requests * @uses \Parsely\Parsely::api_secret_is_set @@ -633,7 +623,7 @@ public function test_assets_do_not_get_enqueued_when_both_filters_are_false(): v * @uses \Parsely\Parsely::set_managed_options * @uses \Parsely\Parsely::site_id_is_set * @uses \Parsely\Utils\Utils::convert_endpoint_to_filter_key - * @uses \Parsely\Utils::get_asset_info + * @uses \Parsely\Utils\Utils::get_asset_info * * @group content-helper */ @@ -669,7 +659,6 @@ public function test_assets_do_not_get_enqueued_when_both_filters_are_invalid(): * @covers \Parsely\Content_Helper\Post_List_Stats::__construct * @covers \Parsely\Content_Helper\Post_List_Stats::get_feature_filter_name * @covers \Parsely\Content_Helper\Post_List_Stats::run - * @uses \Parsely\Endpoints\Base_Endpoint::__construct * @uses \Parsely\Parsely::__construct * @uses \Parsely\Parsely::allow_parsely_remote_requests * @uses \Parsely\Parsely::api_secret_is_set @@ -678,7 +667,7 @@ public function test_assets_do_not_get_enqueued_when_both_filters_are_invalid(): * @uses \Parsely\Parsely::set_managed_options * @uses \Parsely\Parsely::site_id_is_set * @uses \Parsely\Utils\Utils::convert_endpoint_to_filter_key - * @uses \Parsely\Utils::get_asset_info + * @uses \Parsely\Utils\Utils::get_asset_info * * @group content-helper */ @@ -723,7 +712,6 @@ public function test_assets_do_not_get_enqueued_when_global_filter_is_true_and_f * @covers \Parsely\Content_Helper\Post_List_Stats::set_current_screen * @uses \Parsely\Content_Helper\Content_Helper_Feature::get_credentials_not_set_message * @uses \Parsely\Content_Helper\Content_Helper_Feature::inject_inline_scripts - * @uses \Parsely\Endpoints\Base_Endpoint::__construct * @uses \Parsely\Parsely::__construct * @uses \Parsely\Parsely::allow_parsely_remote_requests * @uses \Parsely\Parsely::api_secret_is_set @@ -735,7 +723,7 @@ public function test_assets_do_not_get_enqueued_when_global_filter_is_true_and_f * @uses \Parsely\Parsely::set_managed_options * @uses \Parsely\Parsely::site_id_is_set * @uses \Parsely\Utils\Utils::convert_endpoint_to_filter_key - * @uses \Parsely\Utils::get_asset_info + * @uses \Parsely\Utils\Utils::get_asset_info * * @group content-helper */ @@ -772,7 +760,6 @@ public function test_assets_get_enqueued_when_global_filter_is_false_and_feature * @covers \Parsely\Content_Helper\Post_List_Stats::__construct * @covers \Parsely\Content_Helper\Post_List_Stats::get_feature_filter_name * @covers \Parsely\Content_Helper\Post_List_Stats::run - * @uses \Parsely\Endpoints\Base_Endpoint::__construct * @uses \Parsely\Parsely::__construct * @uses \Parsely\Parsely::allow_parsely_remote_requests * @uses \Parsely\Parsely::api_secret_is_set @@ -781,7 +768,7 @@ public function test_assets_get_enqueued_when_global_filter_is_false_and_feature * @uses \Parsely\Parsely::set_managed_options * @uses \Parsely\Parsely::site_id_is_set * @uses \Parsely\Utils\Utils::convert_endpoint_to_filter_key - * @uses \Parsely\Utils::get_asset_info + * @uses \Parsely\Utils\Utils::get_asset_info * * @group content-helper */ @@ -826,7 +813,6 @@ public function test_assets_do_not_get_enqueued_when_global_filter_is_true_and_f * @covers \Parsely\Content_Helper\Post_List_Stats::set_current_screen * @uses \Parsely\Content_Helper\Content_Helper_Feature::get_credentials_not_set_message * @uses \Parsely\Content_Helper\Content_Helper_Feature::inject_inline_scripts - * @uses \Parsely\Endpoints\Base_Endpoint::__construct * @uses \Parsely\Parsely::__construct * @uses \Parsely\Parsely::allow_parsely_remote_requests * @uses \Parsely\Parsely::api_secret_is_set @@ -838,7 +824,7 @@ public function test_assets_do_not_get_enqueued_when_global_filter_is_true_and_f * @uses \Parsely\Parsely::set_managed_options * @uses \Parsely\Parsely::site_id_is_set * @uses \Parsely\Utils\Utils::convert_endpoint_to_filter_key - * @uses \Parsely\Utils::get_asset_info + * @uses \Parsely\Utils\Utils::get_asset_info * * @group content-helper */ diff --git a/tests/Integration/ContentHelper/ContentHelperPostListStatsTest.php b/tests/Integration/ContentHelper/ContentHelperPostListStatsTest.php index 9b817413e1..276d27c371 100644 --- a/tests/Integration/ContentHelper/ContentHelperPostListStatsTest.php +++ b/tests/Integration/ContentHelper/ContentHelperPostListStatsTest.php @@ -12,8 +12,10 @@ use Mockery; use Parsely\Content_Helper\Post_List_Stats; use Parsely\Parsely; -use Parsely\RemoteAPI\Analytics_Posts_API; +use Parsely\Services\Content_API\Content_API_Service; +use Parsely\Services\Content_API\Endpoints\Endpoint_Analytics_Posts; use Parsely\Tests\Integration\TestCase; +use ReflectionProperty; use WP_Error; use WP_Post; use WP_Scripts; @@ -23,11 +25,17 @@ * * @since 3.7.0 * - * @phpstan-import-type Analytics_Post_API_Params from Analytics_Posts_API - * @phpstan-import-type Analytics_Post from Analytics_Posts_API + * @phpstan-import-type Analytics_Post from Endpoint_Analytics_Posts * @phpstan-import-type Parsely_Posts_Stats_Response from Post_List_Stats */ final class ContentHelperPostListStatsTest extends ContentHelperFeatureTest { + /** + * The Parsely instance. + * + * @var Parsely + */ + private static $parsely; + /** * Internal variable. * @@ -51,6 +59,9 @@ final class ContentHelperPostListStatsTest extends ContentHelperFeatureTest { public function set_up(): void { parent::set_up(); + self::$parsely = new Parsely(); + self::$parsely->get_rest_api_controller()->init(); + $this->set_permalink_structure( '/%year%/%monthnum%/%day%/%postname%' ); $this->set_current_user_to_admin(); } @@ -121,13 +132,11 @@ protected function assert_enqueued_status( * @covers \Parsely\Content_Helper\Post_List_Stats::__construct * @covers \Parsely\Content_Helper\Post_List_Stats::get_feature_filter_name * @covers \Parsely\Content_Helper\Post_List_Stats::run - * @covers \Parsely\RemoteAPI\Analytics_Posts_API::is_available_to_current_user * @covers \Parsely\Utils\Utils::convert_endpoint_to_filter_key * @uses \Parsely\Parsely::__construct * @uses \Parsely\Parsely::api_secret_is_set * @uses \Parsely\Parsely::get_options * @uses \Parsely\Parsely::site_id_is_set - * @uses \Parsely\Endpoints\Base_Endpoint::__construct * * @group content-helper */ @@ -150,13 +159,11 @@ public function test_assets_do_not_get_enqueued_when_user_has_not_enough_capabil * @covers \Parsely\Content_Helper\Post_List_Stats::is_tracked_as_post_type * @covers \Parsely\Content_Helper\Post_List_Stats::run * @covers \Parsely\Content_Helper\Post_List_Stats::set_current_screen - * @covers \Parsely\RemoteAPI\Analytics_Posts_API::is_available_to_current_user * @covers \Parsely\Utils\Utils::convert_endpoint_to_filter_key * @uses \Parsely\Parsely::__construct * @uses \Parsely\Parsely::api_secret_is_set * @uses \Parsely\Parsely::get_options * @uses \Parsely\Parsely::site_id_is_set - * @uses \Parsely\Endpoints\Base_Endpoint::__construct * * @group content-helper */ @@ -754,7 +761,7 @@ public function test_script_of_parsely_stats_admin_column_on_valid_posts_and_val * @return Post_List_Stats */ private function mock_parsely_stats_response( ?array $return_value ): Post_List_Stats { - $obj = Mockery::mock( Post_List_Stats::class, array( new Parsely() ) )->makePartial(); + $obj = Mockery::mock( Post_List_Stats::class, array( self::$parsely ) )->makePartial(); $obj->shouldReceive( 'get_parsely_stats_response' )->once()->andReturn( $return_value ); $obj->run(); @@ -785,7 +792,7 @@ public function test_should_not_call_parsely_api_on_empty_api_secret_and_hidden_ * @return Post_List_Stats */ private function mock_is_parsely_stats_column_hidden( bool $return_value = false ): Post_List_Stats { - $obj = Mockery::mock( Post_List_Stats::class, array( new Parsely() ) )->makePartial(); + $obj = Mockery::mock( Post_List_Stats::class, array( self::$parsely ) )->makePartial(); $obj->shouldReceive( 'is_parsely_stats_column_hidden' )->once()->andReturn( $return_value ); $obj->run(); @@ -1251,7 +1258,7 @@ public function test_parsely_stats_response_on_valid_hierarchal_post_type_and_ha * @param array $posts Available Posts. * @param string $post_type Type of the post. * @param array|WP_Error|null $api_response Mocked response that we return on calling API. - * @param Analytics_Post_API_Params|null $api_params API Parameters. + * @param array|null $api_params API Parameters. * @return Parsely_Posts_Stats_Response|null */ private function get_parsely_stats_response( @@ -1266,9 +1273,13 @@ private function get_parsely_stats_response( $this->show_content_on_parsely_stats_column( $posts, $post_type ); ob_get_clean(); // Discard output to keep console clean while running tests. - $api = Mockery::mock( Analytics_Posts_API::class, array( new Parsely() ) )->makePartial(); + if ( null === $api_response ) { + $api_response = self::$parsely_api_empty_response; + } + + $api = Mockery::mock( Content_API_Service::class, array( self::$parsely ) )->makePartial(); if ( ! is_null( $api_params ) ) { - $api->shouldReceive( 'get_posts_analytics' ) + $api->shouldReceive( 'get_posts' ) ->once() ->withArgs( array( @@ -1276,8 +1287,8 @@ private function get_parsely_stats_response( $api_params, // Params which will not change. array( - 'period_start' => Analytics_Posts_API::ANALYTICS_API_DAYS_LIMIT . 'd', - 'limit' => 2000, + 'period_start' => Endpoint_Analytics_Posts::MAX_PERIOD, + 'limit' => Endpoint_Analytics_Posts::MAX_LIMIT, 'sort' => 'avg_engaged', ) ), @@ -1285,10 +1296,15 @@ private function get_parsely_stats_response( ) ->andReturn( $api_response ); } else { - $api->shouldReceive( 'get_posts_analytics' )->once()->andReturn( $api_response ); + $api->shouldReceive( 'get_posts' )->once()->andReturn( $api_response ); } - return $obj->get_parsely_stats_response( $api ); + // Replace the original API with the mock, using reflection. + $api_reflection = new ReflectionProperty( $obj, 'content_api' ); + $api_reflection->setAccessible( true ); + $api_reflection->setValue( $obj, $api ); + + return $obj->get_parsely_stats_response(); } /** @@ -1309,7 +1325,7 @@ private function assert_hooks_for_parsely_stats_response( $assert_type = true ): * @return Post_List_Stats */ private function init_post_list_stats(): Post_List_Stats { - $obj = new Post_List_Stats( new Parsely() ); + $obj = new Post_List_Stats( self::$parsely ); $obj->run(); return $obj; diff --git a/tests/Integration/RemoteAPI/AnalyticsPostsRemoteAPITest.php b/tests/Integration/RemoteAPI/AnalyticsPostsRemoteAPITest.php deleted file mode 100644 index 8eb7dc1058..0000000000 --- a/tests/Integration/RemoteAPI/AnalyticsPostsRemoteAPITest.php +++ /dev/null @@ -1,128 +0,0 @@ - - */ - public function data_api_url(): iterable { - yield 'Basic (Expected data)' => array( - array( - 'apikey' => 'my-key', - 'limit' => 5, - ), - Parsely::PUBLIC_API_BASE_URL . '/analytics/posts?apikey=my-key&limit=5', - ); - } - - /** - * Verifies default user capability filter. - * - * @covers \Parsely\RemoteAPI\Analytics_Posts_API::is_available_to_current_user - * @uses \Parsely\Endpoints\Base_Endpoint::__construct - * @uses \Parsely\Endpoints\Base_Endpoint::apply_capability_filters - * @uses \Parsely\Parsely::__construct - * @uses \Parsely\Parsely::allow_parsely_remote_requests - * @uses \Parsely\Parsely::are_credentials_managed - * @uses \Parsely\Parsely::set_managed_options - * @uses \Parsely\Utils\Utils::convert_endpoint_to_filter_key - */ - public function test_user_is_allowed_to_make_api_call_if_default_user_capability_is_changed(): void { - $this->set_current_user_to_contributor(); - add_filter( - 'wp_parsely_user_capability_for_all_private_apis', - function () { - return 'edit_posts'; - } - ); - - $api = new Analytics_Posts_API( new Parsely() ); - - self::assertTrue( $api->is_available_to_current_user() ); - } - - /** - * Verifies endpoint specific user capability filter. - * - * @covers \Parsely\RemoteAPI\Analytics_Posts_API::is_available_to_current_user - * @uses \Parsely\Endpoints\Base_Endpoint::__construct - * @uses \Parsely\Endpoints\Base_Endpoint::apply_capability_filters - * @uses \Parsely\Parsely::__construct - * @uses \Parsely\Parsely::allow_parsely_remote_requests - * @uses \Parsely\Parsely::are_credentials_managed - * @uses \Parsely\Parsely::set_managed_options - * @uses \Parsely\Utils\Utils::convert_endpoint_to_filter_key - */ - public function test_user_is_allowed_to_make_api_call_if_endpoint_specific_user_capability_is_changed(): void { - $this->set_current_user_to_contributor(); - add_filter( - 'wp_parsely_user_capability_for_analytics_posts_api', - function () { - return 'edit_posts'; - } - ); - - $api = new Analytics_Posts_API( new Parsely() ); - - self::assertTrue( $api->is_available_to_current_user() ); - } - - /** - * Verifies that the endpoint specific user capability filter has more priority than the default capability filter. - * - * @covers \Parsely\RemoteAPI\Analytics_Posts_API::is_available_to_current_user - * @uses \Parsely\Endpoints\Base_Endpoint::__construct - * @uses \Parsely\Endpoints\Base_Endpoint::apply_capability_filters - * @uses \Parsely\Parsely::__construct - * @uses \Parsely\Parsely::allow_parsely_remote_requests - * @uses \Parsely\Parsely::are_credentials_managed - * @uses \Parsely\Parsely::set_managed_options - * @uses \Parsely\Utils\Utils::convert_endpoint_to_filter_key - */ - public function test_endpoint_specific_user_capability_filter_have_more_priority_than_default(): void { - $this->set_current_user_to_contributor(); - - add_filter( - 'wp_parsely_user_capability_for_all_private_apis', - function () { - return 'publish_posts'; - } - ); - - add_filter( - 'wp_parsely_user_capability_for_analytics_posts_api', - function () { - return 'edit_posts'; - } - ); - - $api = new Analytics_Posts_API( new Parsely() ); - - self::assertTrue( $api->is_available_to_current_user() ); - } -} diff --git a/tests/Integration/RemoteAPI/BaseRemoteAPITest.php b/tests/Integration/RemoteAPI/BaseRemoteAPITest.php deleted file mode 100644 index 6f978d11c8..0000000000 --- a/tests/Integration/RemoteAPI/BaseRemoteAPITest.php +++ /dev/null @@ -1,163 +0,0 @@ - - */ - abstract public function data_api_url(): iterable; - - /** - * Runs once before all tests. - */ - public static function set_up_before_class(): void { - static::initialize(); - } - - /** - * Verifies the basic generation of the API URL. - * - * @dataProvider data_api_url - * @covers \Parsely\RemoteAPI\Related_API::get_api_url - * @covers \Parsely\RemoteAPI\Analytics_Posts_API::get_api_url - * @uses \Parsely\Endpoints\Base_Endpoint::__construct - * @uses \Parsely\Parsely::api_secret_is_set - * @uses \Parsely\Parsely::get_managed_credentials - * @uses \Parsely\Parsely::get_options - * @uses \Parsely\Parsely::get_site_id - * @uses \Parsely\Parsely::site_id_is_set - * @uses \Parsely\RemoteAPI\Base_Endpoint_Remote::validate_required_constraints - * @uses \Parsely\RemoteAPI\ContentSuggestions\Content_Suggestions_Base_API::get_api_url - * - * @param array $query Test query arguments. - * @param string $url Expected generated URL. - */ - public function test_api_url( array $query, string $url ): void { - self::set_options( array( 'apikey' => 'my-key' ) ); - self::assertSame( $url, self::$remote_api->get_api_url( $query ) ); - } - - /** - * Verifies that the cache is used instead of the api when there's a cache - * hit. - * - * @covers \Parsely\RemoteAPI\Remote_API_Cache::get_items - * @covers \Parsely\RemoteAPI\Remote_API_Cache::__construct - * @uses \Parsely\RemoteAPI\Base_Endpoint_Remote::get_endpoint - */ - public function test_remote_api_cache_returns_cached_value(): void { - $api_mock = $this->getMockBuilder( get_class( self::$remote_api ) ) - ->disableOriginalConstructor() - ->getMock(); - - // If this method is called, that means our cache did not hit as expected. - $api_mock->expects( self::never() )->method( 'get_items' ); - $api_mock->method( 'get_endpoint' )->willReturn( self::$remote_api->get_endpoint() ); // Passing call to non-mock method. - - $cache_key = 'parsely_api_' . wp_hash( self::$remote_api->get_endpoint() ) . '_' . wp_hash( $this->wp_json_encode( array() ) ); - - $object_cache = $this->createMock( Cache::class ); - $object_cache->method( 'get' ) - ->willReturn( (object) array( 'cache_hit' => true ) ); - - $object_cache->expects( self::once() ) - ->method( 'get' ) - ->with( - self::equalTo( $cache_key ), - self::equalTo( 'wp-parsely' ), - self::equalTo( false ), - self::isNull() - ); - - /** - * Variable. - * - * @var Remote_API_Cache - */ - $remote_api_cache = $this->getMockBuilder( Remote_API_Cache::class ) - ->setConstructorArgs( array( $api_mock, $object_cache ) ) - ->setMethodsExcept( array( 'get_items' ) ) - ->getMock(); - - self::assertEquals( (object) array( 'cache_hit' => true ), $remote_api_cache->get_items( array() ) ); - } - - /** - * Verifies that when the cache misses, the api is used instead and the - * resultant value is cached. - * - * @covers \Parsely\RemoteAPI\Remote_API_Cache::get_items - * @covers \Parsely\RemoteAPI\Remote_API_Cache::__construct - * @uses \Parsely\RemoteAPI\Base_Endpoint_Remote::get_endpoint - */ - public function test_caching_decorator_returns_uncached_value(): void { - $api_mock = $this->getMockBuilder( get_class( self::$remote_api ) ) - ->disableOriginalConstructor() - ->getMock(); - - $api_mock->method( 'get_items' ) - ->willReturn( (object) array( 'cache_hit' => false ) ); - - // If this method is _NOT_ called, that means our cache did not miss as expected. - $api_mock->expects( self::once() )->method( 'get_items' ); - $api_mock->method( 'get_endpoint' )->willReturn( self::$remote_api->get_endpoint() ); // Passing call to non-mock method. - - $cache_key = 'parsely_api_' . wp_hash( self::$remote_api->get_endpoint() ) . '_' . wp_hash( $this->wp_json_encode( array() ) ); - - $object_cache = $this->createMock( Cache::class ); - $object_cache->method( 'get' ) - ->willReturn( false ); - - $object_cache->expects( self::once() ) - ->method( 'get' ) - ->with( - self::equalTo( $cache_key ), - self::equalTo( 'wp-parsely' ), - self::equalTo( false ), - self::isNull() - ); - - /** - * Variable. - * - * @var Remote_API_Cache - */ - $remote_api_cache = $this->getMockBuilder( Remote_API_Cache::class ) - ->setConstructorArgs( array( $api_mock, $object_cache ) ) - ->setMethodsExcept( array( 'get_items' ) ) - ->getMock(); - - self::assertEquals( (object) array( 'cache_hit' => false ), $remote_api_cache->get_items( array() ) ); - } -} diff --git a/tests/Integration/RemoteAPI/ContentSuggestions/BaseContentSuggestionsAPITest.php b/tests/Integration/RemoteAPI/ContentSuggestions/BaseContentSuggestionsAPITest.php deleted file mode 100644 index 708b08a6c7..0000000000 --- a/tests/Integration/RemoteAPI/ContentSuggestions/BaseContentSuggestionsAPITest.php +++ /dev/null @@ -1,129 +0,0 @@ - 'my-key', - 'api_secret' => 'my-secret', - ) - ); - - $request_options = self::$remote_api->get_request_options(); - - // Ensure that $request_options is an array and 'headers' key exists. - self::assertIsArray( $request_options ); - self::assertArrayHasKey( 'headers', $request_options ); - - $headers = $request_options['headers']; - self::assertIsArray( $headers ); // Ensures $headers is indeed an array. - - // Verify the Content-Type header is present and its value is application/json. - self::assertArrayHasKey( 'Content-Type', $headers ); - self::assertEquals( 'application/json; charset=utf-8', $headers['Content-Type'] ); - - // Verify the API key is present in the headers and its value matches the one set in the options. - self::assertArrayHasKey( 'X-APIKEY-SECRET', $headers ); - self::assertEquals( 'my-secret', $headers['X-APIKEY-SECRET'] ); - } - - /** - * Verifies that the truncate function is properly truncated long content on the body array. - * - * @since 3.14.1 - * - * @covers \Parsely\RemoteAPI\ContentSuggestions\Content_Suggestions_Base_API::truncate_array_content - */ - public function test_truncate_body_content(): void { - /** - * @var Content_Suggestions_Base_API $remote_api - */ - $remote_api = self::$remote_api; - - $body = array( - 'output_params' => array( - 'some_param' => true, - 'other_param' => 'Hello', - 'recursive' => array( - 'key' => 'value', - ), - ), - 'text' => $this->generate_content_with_length( 30000 ), - 'something' => 'else', - ); - - $truncated_array = $remote_api->truncate_array_content( $body ); - - self::assertIsArray( $truncated_array ); - self::assertArrayHasKey( 'output_params', $truncated_array ); - self::assertArrayHasKey( 'text', $truncated_array ); - self::assertLessThanOrEqual( 25000, strlen( $truncated_array['text'] ) ); - - // Assert that the truncated text is the beginning of the original text. - self::assertStringStartsWith( $truncated_array['text'], $body['text'] ); - - // Assert that the other keys are the same in both arrays. - self::assertEquals( $body['output_params'], $truncated_array['output_params'] ); - self::assertEquals( $body['something'], $truncated_array['something'] ); - } - - /** - * Generate content with a specific length. - * - * @since 3.14.1 - * - * @param int $length Length of the generated content. - * - * @return string The generated content. - */ - private function generate_content_with_length( int $length ): string { - $words = array( 'lorem', 'ipsum', 'dolor', 'sit', 'amet', 'consectetur', 'adipiscing', 'elit' ); - $string = ''; - $current_length = 0; - while ( $current_length < $length ) { - $word = $words[ array_rand( $words ) ]; - if ( $current_length > 0 ) { - if ( $current_length + strlen( $word ) + 1 > $length ) { - break; - } - $string .= ' '; - ++$current_length; - } - $string .= $word; - $current_length += strlen( $word ); - } - return $string; - } -} diff --git a/tests/Integration/RemoteAPI/ContentSuggestions/SuggestBriefAPITest.php b/tests/Integration/RemoteAPI/ContentSuggestions/SuggestBriefAPITest.php deleted file mode 100644 index 6c3ab62e12..0000000000 --- a/tests/Integration/RemoteAPI/ContentSuggestions/SuggestBriefAPITest.php +++ /dev/null @@ -1,137 +0,0 @@ - - */ - public function data_api_url(): iterable { - yield 'Basic (Expected data)' => array( - array( - 'apikey' => 'my-key', - ), - Parsely::PUBLIC_SUGGESTIONS_API_BASE_URL . - '/suggest-brief?apikey=my-key', - ); - } - - - /** - * Mocks a successful HTTP response to the Content Suggestion suggest-brief - * API endpoint. - * - * @since 3.13.0 - * - * @param string $response The response to mock. - * @param array $args The arguments passed to the HTTP request. - * @param string $url The URL of the HTTP request. - * @return array|false The mocked response. - * - * @phpstan-ignore-next-line - */ - public function mock_successful_suggest_brief_response( - string $response, - array $args, - string $url - ) { - if ( ! str_contains( $url, 'suggest-brief' ) ) { - return false; - } - - $response = array( - 'result' => array( - 'Lorem ipsum dolor sit amet, consectetur adipiscing elit.', - ), - ); - - return array( - 'headers' => array(), - 'cookies' => array(), - 'filename' => null, - 'response' => array( - 'code' => 200, - 'message' => 'OK', - ), - 'status_code' => 200, - 'success' => true, - 'body' => $this->wp_json_encode( $response ), - ); - } - - /** - * Tests getting meta description from the API with some generic content. - * - * @since 3.13.0 - * - * @covers \Parsely\RemoteAPI\ContentSuggestions\Suggest_Brief_API::get_suggestion - * @uses \Parsely\Parsely::api_secret_is_set() - * @uses \Parsely\Parsely::get_managed_credentials() - * @uses \Parsely\Parsely::get_options() - * @uses \Parsely\Parsely::get_site_id() - * @uses \Parsely\Parsely::set_default_track_as_values() - * @uses \Parsely\Parsely::site_id_is_set() - * @uses \Parsely\RemoteAPI\Base_Endpoint_Remote::validate_required_constraints() - * @uses \Parsely\RemoteAPI\ContentSuggestions\Content_Suggestions_Base_API::get_api_url() - * @uses \Parsely\RemoteAPI\ContentSuggestions\Content_Suggestions_Base_API::get_request_options() - * @uses \Parsely\RemoteAPI\ContentSuggestions\Content_Suggestions_Base_API::post_request() - */ - public function test_get_suggestion(): void { - $title = 'Lorem Ipsum is a random title'; - $content = '

Lorem ipsum dolor sit amet, consectetur adipiscing elit.

'; - $persona = 'journalist'; - $style = 'neutral'; - - // Mock API result. - add_filter( 'pre_http_request', array( $this, 'mock_successful_suggest_brief_response' ), 10, 3 ); - - // Test getting meta description. - $brief = self::$suggest_brief_api->get_suggestion( $title, $content, $persona, $style ); - - self::assertIsString( $brief ); - self::assertEquals( 'Lorem ipsum dolor sit amet, consectetur adipiscing elit.', $brief ); - - // Remove mock. - remove_filter( 'pre_http_request', array( $this, 'mock_successful_suggest_brief_response' ) ); - } -} diff --git a/tests/Integration/RemoteAPI/RelatedRemoteAPITest.php b/tests/Integration/RemoteAPI/RelatedRemoteAPITest.php deleted file mode 100644 index 52c4ab80d4..0000000000 --- a/tests/Integration/RemoteAPI/RelatedRemoteAPITest.php +++ /dev/null @@ -1,94 +0,0 @@ - - */ - public function data_api_url(): iterable { - yield 'Basic (Expected data)' => array( - array( - 'apikey' => 'my-key', - 'pub_date_start' => '7d', - 'sort' => 'score', - 'limit' => 5, - ), - Parsely::PUBLIC_API_BASE_URL . '/related?apikey=my-key&limit=5&pub_date_start=7d&sort=score', - ); - - yield 'published_within value of 0' => array( - array( - 'apikey' => 'my-key', - 'sort' => 'score', - 'limit' => 5, - ), - Parsely::PUBLIC_API_BASE_URL . '/related?apikey=my-key&limit=5&sort=score', - ); - - yield 'Sort on publish date' => array( - array( - 'apikey' => 'my-key', - 'sort' => 'pub_date', - 'limit' => 5, - ), - Parsely::PUBLIC_API_BASE_URL . '/related?apikey=my-key&limit=5&sort=pub_date', - ); - - yield 'Rank by relevance only' => array( - array( - 'apikey' => 'my-key', - 'sort' => 'score', - 'limit' => 5, - ), - Parsely::PUBLIC_API_BASE_URL . '/related?apikey=my-key&limit=5&sort=score', - ); - } - - /** - * Verifies that the endpoint does not have filters that check user capability. - * - * @covers \Parsely\RemoteAPI\Related_API::is_available_to_current_user - * @uses \Parsely\Endpoints\Base_Endpoint::__construct - * @uses \Parsely\Parsely::get_managed_credentials - * @uses \Parsely\Parsely::__construct - * @uses \Parsely\Parsely::allow_parsely_remote_requests - * @uses \Parsely\Parsely::are_credentials_managed - * @uses \Parsely\Parsely::set_managed_options - */ - public function test_related_endpoint_does_not_have_user_capability_filters(): void { - $api = new Related_API( new Parsely() ); - - self::assertTrue( $api->is_available_to_current_user() ); - $this->assert_wp_hooks_availability( - array( - 'wp_parsely_user_capability_for_all_private_apis', - 'wp_parsely_user_capability_for_related_api', - ), - false - ); - } -} diff --git a/tests/Integration/RestAPI/BaseAPIControllerTest.php b/tests/Integration/RestAPI/BaseAPIControllerTest.php index 23f718ed0e..4211b10bbc 100644 --- a/tests/Integration/RestAPI/BaseAPIControllerTest.php +++ b/tests/Integration/RestAPI/BaseAPIControllerTest.php @@ -53,6 +53,17 @@ protected function get_namespace(): string { return 'test'; } + /** + * Gets the route prefix, which acts as a namespace for the endpoints. + * + * @since 3.17.0 + * + * @return string The route prefix. + */ + public static function get_route_prefix(): string { + return 'test'; + } + /** * Gets the version for the API. * @@ -74,6 +85,8 @@ protected function init(): void {} /** * Exposes the protected method for testing. * + * @since 3.17.0 + * * @param Base_Endpoint[] $endpoints The endpoints to register. */ public function testable_register_endpoints( array $endpoints ): void { @@ -83,11 +96,25 @@ public function testable_register_endpoints( array $endpoints ): void { /** * Exposes the protected method for testing. * + * @since 3.17.0 + * * @param Base_Endpoint $endpoint The endpoint to register. */ public function testable_register_endpoint( Base_Endpoint $endpoint ): void { $this->register_endpoint( $endpoint ); } + + /** + * Checks if a specific endpoint is available to the current user. + * + * @since 3.17.0 + * + * @param string $endpoint The endpoint to check. + * @return bool True if the controller is available to the current user, false otherwise. + */ + public function is_available_to_current_user( string $endpoint ): bool { + return true; + } }; } @@ -112,10 +139,10 @@ public function test_get_namespace(): void { * @uses \Parsely\REST_API\Base_API_Controller::__construct */ public function test_prefix_route(): void { - self::assertEquals( 'my-route', $this->test_controller->prefix_route( 'my-route' ) ); + self::assertEquals( 'test/my-route', $this->test_controller->prefix_route( 'my-route' ) ); $parsely = self::createMock( Parsely::class ); - $controller_with_prefix = new class($parsely) extends Base_API_Controller { + $controller_without_prefix = new class($parsely) extends Base_API_Controller { /** * Initialize the test controller. * @@ -135,18 +162,19 @@ protected function get_namespace(): string { } /** - * Get the version for the API. + * Checks if a specific endpoint is available to the current user. * * @since 3.17.0 * - * @return string The version. + * @param string $endpoint The endpoint to check. + * @return bool True if the controller is available to the current user, false otherwise. */ - public static function get_route_prefix(): string { - return 'prefix'; + public function is_available_to_current_user( string $endpoint ): bool { + return true; } }; - self::assertEquals( 'prefix/my-route', $controller_with_prefix->prefix_route( 'my-route' ) ); + self::assertEquals( 'my-route', $controller_without_prefix->prefix_route( 'my-route' ) ); } /** @@ -160,13 +188,15 @@ public static function get_route_prefix(): string { */ public function test_register_endpoint(): void { $endpoint = self::createMock( Base_Endpoint::class ); - $endpoint->expects( self::once() )->method( 'init' ); + $endpoint->expects( self::once() ) + ->method( 'get_endpoint_slug' ) + ->willReturn( 'test' ); $this->test_controller->testable_register_endpoint( $endpoint ); // @phpstan-ignore-line $endpoints = $this->test_controller->get_endpoints(); self::assertCount( 1, $endpoints ); - self::assertSame( $endpoint, $endpoints[0] ); + self::assertSame( $endpoint, $endpoints['test'] ); } /** @@ -181,17 +211,60 @@ public function test_register_endpoint(): void { */ public function test_register_multiple_endpoints(): void { $endpoint1 = self::createMock( Base_Endpoint::class ); - $endpoint1->expects( self::once() )->method( 'init' ); + $endpoint1->expects( self::once() ) + ->method( 'get_endpoint_slug' ) + ->willReturn( 'test1' ); $endpoint2 = self::createMock( Base_Endpoint::class ); - $endpoint2->expects( self::once() )->method( 'init' ); + $endpoint2->expects( self::once() ) + ->method( 'get_endpoint_slug' ) + ->willReturn( 'test2' ); + $this->test_controller->testable_register_endpoints( array( $endpoint1, $endpoint2 ) ); // @phpstan-ignore-line $endpoints = $this->test_controller->get_endpoints(); self::assertCount( 2, $endpoints ); - self::assertSame( $endpoint1, $endpoints[0] ); - self::assertSame( $endpoint2, $endpoints[1] ); + self::assertSame( $endpoint1, $endpoints['test1'] ); + self::assertSame( $endpoint2, $endpoints['test2'] ); + } + + /** + * Tests that the get_endpoint_slug method returns the correct value. + * + * @since 3.17.0 + * + * @covers \Parsely\REST_API\Base_Endpoint::get_endpoint_slug + * @uses \Parsely\REST_API\Base_Endpoint::__construct + * @uses \Parsely\REST_API\Base_Endpoint::get_endpoint_slug + * @uses \Parsely\REST_API\Base_Endpoint::init + * @uses \Parsely\Utils\Utils::convert_endpoint_to_filter_key + */ + public function test_get_endpoint_slug(): void { + // Create a mocked endpoint. + $endpoint = new class( $this->test_controller ) extends Base_Endpoint { + /** + * Get the endpoint name. + * + * @since 3.17.0 + * + * @return string The endpoint name. + */ + public static function get_endpoint_name(): string { + return 'test-endpoint'; + } + + /** + * Register the routes for the endpoints. + * + * @since 3.17.0 + */ + public function register_routes(): void {} + }; + + $this->test_controller->testable_register_endpoint( $endpoint ); // @phpstan-ignore-line + + self::assertEquals( 'test/test-endpoint', $endpoint->get_endpoint_slug() ); } } diff --git a/tests/Integration/RestAPI/BaseEndpointTest.php b/tests/Integration/RestAPI/BaseEndpointTest.php index 7e0ff45137..29544e51ac 100644 --- a/tests/Integration/RestAPI/BaseEndpointTest.php +++ b/tests/Integration/RestAPI/BaseEndpointTest.php @@ -15,7 +15,7 @@ use Parsely\REST_API\Base_Endpoint; use Parsely\REST_API\REST_API_Controller; use Parsely\Tests\Integration\TestCase; -use ReflectionException; +use Parsely\Tests\Traits\TestsReflection; use WP_Error; /** @@ -26,6 +26,8 @@ * @covers \Parsely\REST_API\Base_Endpoint */ class BaseEndpointTest extends TestCase { + use TestsReflection; + /** * The test endpoint instance. * @@ -224,7 +226,6 @@ public function test_route_is_registered(): void { * @covers \Parsely\REST_API\Base_Endpoint::register_routes * @covers \Parsely\REST_API\Base_Endpoint::get_full_endpoint * @covers \Parsely\REST_API\Base_Endpoint::get_registered_routes - * @uses \Parsely\Endpoints\Base_Endpoint::__construct * @uses \Parsely\Parsely::__construct * @uses \Parsely\Parsely::allow_parsely_remote_requests * @uses \Parsely\Parsely::api_secret_is_set @@ -308,7 +309,6 @@ public function test_endpoint_is_registered_based_on_filter(): void { * @uses \Parsely\Parsely::site_id_is_set * @uses \Parsely\Permissions::build_pch_permissions_settings_array * @uses \Parsely\Permissions::get_user_roles_with_edit_posts_cap - * @uses \Parsely\Endpoints\Base_Endpoint::__construct * @uses \Parsely\Permissions::current_user_can_use_pch_feature * @uses \Parsely\REST_API\Base_Endpoint::__construct * @uses \Parsely\REST_API\Base_Endpoint::init @@ -349,7 +349,6 @@ public function test_is_available_to_current_user_returns_error_site_id_not_set( * @uses \Parsely\Parsely::site_id_is_set * @uses \Parsely\Permissions::build_pch_permissions_settings_array * @uses \Parsely\Permissions::get_user_roles_with_edit_posts_cap - * @uses \Parsely\Endpoints\Base_Endpoint::__construct * @uses \Parsely\Permissions::current_user_can_use_pch_feature * @uses \Parsely\REST_API\Base_Endpoint::__construct * @uses \Parsely\REST_API\Base_Endpoint::init @@ -380,7 +379,6 @@ public function test_is_available_to_current_user_returns_error_api_secret_not_s * @uses \Parsely\Parsely::allow_parsely_remote_requests * @uses \Parsely\Parsely::are_credentials_managed * @uses \Parsely\Parsely::set_managed_options - * @uses \Parsely\Endpoints\Base_Endpoint::__construct * @uses \Parsely\REST_API\Base_Endpoint::__construct * @uses \Parsely\REST_API\Base_Endpoint::init */ @@ -414,7 +412,6 @@ function () { * @uses \Parsely\Parsely::site_id_is_set * @uses \Parsely\Permissions::build_pch_permissions_settings_array * @uses \Parsely\Permissions::get_user_roles_with_edit_posts_cap - * @uses \Parsely\Endpoints\Base_Endpoint::__construct * @uses \Parsely\REST_API\Base_Endpoint::__construct * @uses \Parsely\REST_API\Base_Endpoint::init * @uses \Parsely\Utils\Utils::convert_endpoint_to_filter_key @@ -432,27 +429,6 @@ public function test_validate_site_id_and_secret_returns_true(): void { self::assertTrue( $result ); } - /** - * Sets the value of a protected or private property on a given object using reflection. - * - * This method is useful for testing purposes where you need to modify or inject dependencies - * into protected or private properties of a class. - * - * @since 3.17.0 - * - * @param object $obj The object instance on which the property should be set. - * @param string $property_name The name of the property to be set. - * @param mixed $value The value to set on the property. - * - * @throws ReflectionException If the property does not exist. - */ - protected function set_protected_property( $obj, string $property_name, $value ): void { - $reflection = new \ReflectionClass( $obj ); - $property = $reflection->getProperty( $property_name ); - $property->setAccessible( true ); - $property->setValue( $obj, $value ); - } - /** * Initializes the REST endpoint. * diff --git a/tests/Integration/RestAPI/ContentHelper/ContentHelperControllerTest.php b/tests/Integration/RestAPI/ContentHelper/ContentHelperControllerTest.php index 75a939feed..29f4300f48 100644 --- a/tests/Integration/RestAPI/ContentHelper/ContentHelperControllerTest.php +++ b/tests/Integration/RestAPI/ContentHelper/ContentHelperControllerTest.php @@ -78,7 +78,6 @@ public function test_route_prefix(): void { * @since 3.17.0 * * @covers \Parsely\REST_API\Content_Helper\Content_Helper_Controller::init - * @uses \Parsely\Endpoints\Base_Endpoint::__construct * @uses \Parsely\REST_API\Base_API_Controller::__construct * @uses \Parsely\REST_API\Base_API_Controller::get_endpoints * @uses \Parsely\REST_API\Base_API_Controller::get_parsely @@ -99,8 +98,9 @@ public function test_init_registers_endpoints(): void { $endpoints = $this->content_helper_controller->get_endpoints(); self::assertCount( 3, $endpoints ); - self::assertInstanceOf( Endpoint_Smart_Linking::class, $endpoints[0] ); - self::assertInstanceOf( Endpoint_Excerpt_Generator::class, $endpoints[1] ); - self::assertInstanceOf( Endpoint_Title_Suggestions::class, $endpoints[2] ); + + self::assertInstanceOf( Endpoint_Smart_Linking::class, $endpoints['content-helper/smart-linking'] ); + self::assertInstanceOf( Endpoint_Excerpt_Generator::class, $endpoints['content-helper/excerpt-generator'] ); + self::assertInstanceOf( Endpoint_Title_Suggestions::class, $endpoints['content-helper/title-suggestions'] ); } } diff --git a/tests/Integration/RestAPI/ContentHelper/ContentHelperFeatureTestTrait.php b/tests/Integration/RestAPI/ContentHelper/ContentHelperFeatureTestTrait.php index 878cbdd44d..1493ee0ca5 100644 --- a/tests/Integration/RestAPI/ContentHelper/ContentHelperFeatureTestTrait.php +++ b/tests/Integration/RestAPI/ContentHelper/ContentHelperFeatureTestTrait.php @@ -28,7 +28,6 @@ trait ContentHelperFeatureTestTrait { * @since 3.17.0 * * @covers \Parsely\REST_API\Content_Helper\Content_Helper_Feature::is_available_to_current_user - * @uses \Parsely\Endpoints\Base_Endpoint::__construct * @uses \Parsely\Parsely::__construct * @uses \Parsely\Parsely::allow_parsely_remote_requests * @uses \Parsely\Parsely::api_secret_is_set @@ -47,7 +46,7 @@ trait ContentHelperFeatureTestTrait { * @uses \Parsely\REST_API\Base_Endpoint::init * @uses \Parsely\REST_API\Base_Endpoint::apply_capability_filters * @uses \Parsely\REST_API\Base_Endpoint::is_available_to_current_user - * @uses \Parsely\REST_API\Base_Endpoint::validate_apikey_and_secret + * @uses \Parsely\REST_API\Base_Endpoint::validate_site_id_and_secret * @uses \Parsely\Utils\Utils::convert_endpoint_to_filter_key */ public function test_is_available_to_current_user_returns_true_if_feature_enabled(): void { @@ -64,7 +63,6 @@ public function test_is_available_to_current_user_returns_true_if_feature_enable * @since 3.17.0 * * @covers \Parsely\REST_API\Content_Helper\Content_Helper_Feature::is_available_to_current_user - * @uses \Parsely\Endpoints\Base_Endpoint::__construct * @uses \Parsely\Parsely::__construct * @uses \Parsely\Parsely::allow_parsely_remote_requests * @uses \Parsely\Parsely::api_secret_is_set @@ -83,7 +81,7 @@ public function test_is_available_to_current_user_returns_true_if_feature_enable * @uses \Parsely\REST_API\Base_Endpoint::init * @uses \Parsely\REST_API\Base_Endpoint::apply_capability_filters * @uses \Parsely\REST_API\Base_Endpoint::is_available_to_current_user - * @uses \Parsely\REST_API\Base_Endpoint::validate_apikey_and_secret + * @uses \Parsely\REST_API\Base_Endpoint::validate_site_id_and_secret * @uses \Parsely\Utils\Utils::convert_endpoint_to_filter_key */ public function test_is_available_to_current_user_returns_error_if_feature_disabled(): void { @@ -100,7 +98,6 @@ public function test_is_available_to_current_user_returns_error_if_feature_disab * @since 3.17.0 * * @covers \Parsely\REST_API\Content_Helper\Content_Helper_Feature::is_available_to_current_user - * @uses \Parsely\Endpoints\Base_Endpoint::__construct * @uses \Parsely\Parsely::__construct * @uses \Parsely\Parsely::allow_parsely_remote_requests * @uses \Parsely\Parsely::api_secret_is_set @@ -119,7 +116,7 @@ public function test_is_available_to_current_user_returns_error_if_feature_disab * @uses \Parsely\REST_API\Base_Endpoint::init * @uses \Parsely\REST_API\Base_Endpoint::apply_capability_filters * @uses \Parsely\REST_API\Base_Endpoint::is_available_to_current_user - * @uses \Parsely\REST_API\Base_Endpoint::validate_apikey_and_secret + * @uses \Parsely\REST_API\Base_Endpoint::validate_site_id_and_secret * @uses \Parsely\Utils\Utils::convert_endpoint_to_filter_key */ public function test_is_available_to_current_user_returns_true_if_has_permissions(): void { @@ -146,7 +143,6 @@ public function test_is_available_to_current_user_returns_true_if_has_permission * @since 3.17.0 * * @covers \Parsely\REST_API\Content_Helper\Content_Helper_Feature::is_available_to_current_user - * @uses \Parsely\Endpoints\Base_Endpoint::__construct * @uses \Parsely\Parsely::__construct * @uses \Parsely\Parsely::allow_parsely_remote_requests * @uses \Parsely\Parsely::api_secret_is_set @@ -165,7 +161,7 @@ public function test_is_available_to_current_user_returns_true_if_has_permission * @uses \Parsely\REST_API\Base_Endpoint::init * @uses \Parsely\REST_API\Base_Endpoint::apply_capability_filters * @uses \Parsely\REST_API\Base_Endpoint::is_available_to_current_user - * @uses \Parsely\REST_API\Base_Endpoint::validate_apikey_and_secret + * @uses \Parsely\REST_API\Base_Endpoint::validate_site_id_and_secret * @uses \Parsely\Utils\Utils::convert_endpoint_to_filter_key */ public function test_is_available_to_current_user_returns_error_if_no_permissions(): void { @@ -189,7 +185,6 @@ public function test_is_available_to_current_user_returns_error_if_no_permission * @since 3.17.0 * * @covers \Parsely\REST_API\Content_Helper\Content_Helper_Feature::is_available_to_current_user - * @uses \Parsely\Endpoints\Base_Endpoint::__construct * @uses \Parsely\Parsely::api_secret_is_set * @uses \Parsely\Parsely::get_managed_credentials * @uses \Parsely\Parsely::get_options diff --git a/tests/Integration/RestAPI/ContentHelper/EndpointExcerptGeneratorTest.php b/tests/Integration/RestAPI/ContentHelper/EndpointExcerptGeneratorTest.php index 08f07e046c..17a12ebd34 100644 --- a/tests/Integration/RestAPI/ContentHelper/EndpointExcerptGeneratorTest.php +++ b/tests/Integration/RestAPI/ContentHelper/EndpointExcerptGeneratorTest.php @@ -12,7 +12,7 @@ use Parsely\REST_API\Content_Helper\Endpoint_Excerpt_Generator; use Parsely\REST_API\Content_Helper\Content_Helper_Controller; -use Parsely\RemoteAPI\ContentSuggestions\Suggest_Brief_API; +use Parsely\Services\Suggestions_API\Suggestions_API_Service; use Parsely\Tests\Integration\RestAPI\BaseEndpointTest; use WP_Error; use WP_REST_Request; @@ -67,7 +67,6 @@ public function get_endpoint(): \Parsely\REST_API\Base_Endpoint { * @since 3.17.0 * * @covers \Parsely\REST_API\Content_Helper\Endpoint_Excerpt_Generator::register_routes - * @uses \Parsely\Endpoints\Base_Endpoint::__construct * @uses \Parsely\Parsely::api_secret_is_set * @uses \Parsely\Parsely::get_managed_credentials * @uses \Parsely\Parsely::get_options @@ -112,21 +111,28 @@ public function test_route_is_registered(): void { * @since 3.17.0 * * @covers \Parsely\REST_API\Content_Helper\Endpoint_Excerpt_Generator::generate_excerpt - * @uses \Parsely\Endpoints\Base_Endpoint::__construct + * @uses \Parsely\Parsely::get_suggestions_api * @uses \Parsely\REST_API\Base_API_Controller::__construct * @uses \Parsely\REST_API\Base_API_Controller::get_parsely * @uses \Parsely\REST_API\Base_Endpoint::__construct * @uses \Parsely\REST_API\Base_Endpoint::init + * @uses \Parsely\Services\Base_API_Service::__construct + * @uses \Parsely\Services\Base_API_Service::register_endpoint + * @uses \Parsely\Services\Base_Service_Endpoint::__construct + * @uses \Parsely\Services\Suggestions_API\Endpoints\Endpoint_Suggest_Brief::get_endpoint + * @uses \Parsely\Services\Suggestions_API\Endpoints\Endpoint_Suggest_Headline::get_endpoint + * @uses \Parsely\Services\Suggestions_API\Endpoints\Endpoint_Suggest_Linked_Reference::get_endpoint + * @uses \Parsely\Services\Suggestions_API\Suggestions_API_Service::register_endpoints * @uses \Parsely\Utils\Utils::convert_endpoint_to_filter_key */ public function test_generate_excerpt_returns_valid_response(): void { // Mock the Suggest_Brief_API to control the response. - $mock_suggest_api = $this->createMock( Suggest_Brief_API::class ); - $mock_suggest_api->expects( self::once() ) - ->method( 'get_suggestion' ) - ->willReturn( array( 'summary' => 'This is a test excerpt.' ) ); + $mock_suggestions_api = $this->createMock( Suggestions_API_Service::class ); + $mock_suggestions_api->expects( self::once() ) + ->method( 'get_brief_suggestions' ) + ->willReturn( array( array( 'summary' => 'This is a test excerpt.' ) ) ); - $this->set_protected_property( $this->get_endpoint(), 'suggest_brief_api', $mock_suggest_api ); + self::set_protected_property( $this->get_endpoint(), 'suggestions_api', $mock_suggestions_api ); // Create a mock request. $request = new WP_REST_Request( 'POST', '/excerpt-generator/generate' ); @@ -157,21 +163,28 @@ public function test_generate_excerpt_returns_valid_response(): void { * @since 3.17.0 * * @covers \Parsely\REST_API\Content_Helper\Endpoint_Excerpt_Generator::generate_excerpt - * @uses \Parsely\Endpoints\Base_Endpoint::__construct + * @uses \Parsely\Parsely::get_suggestions_api * @uses \Parsely\REST_API\Base_API_Controller::__construct * @uses \Parsely\REST_API\Base_API_Controller::get_parsely * @uses \Parsely\REST_API\Base_Endpoint::__construct * @uses \Parsely\REST_API\Base_Endpoint::init + * @uses \Parsely\Services\Base_API_Service::__construct + * @uses \Parsely\Services\Base_API_Service::register_endpoint + * @uses \Parsely\Services\Base_Service_Endpoint::__construct + * @uses \Parsely\Services\Suggestions_API\Endpoints\Endpoint_Suggest_Brief::get_endpoint + * @uses \Parsely\Services\Suggestions_API\Endpoints\Endpoint_Suggest_Headline::get_endpoint + * @uses \Parsely\Services\Suggestions_API\Endpoints\Endpoint_Suggest_Linked_Reference::get_endpoint + * @uses \Parsely\Services\Suggestions_API\Suggestions_API_Service::register_endpoints * @uses \Parsely\Utils\Utils::convert_endpoint_to_filter_key */ public function test_generate_excerpt_returns_error_on_failure(): void { // Mock the Suggest_Brief_API to simulate a failure. - $mock_suggest_api = $this->createMock( Suggest_Brief_API::class ); - $mock_suggest_api->expects( self::once() ) - ->method( 'get_suggestion' ) + $mock_suggestions_api = $this->createMock( Suggestions_API_Service::class ); + $mock_suggestions_api->expects( self::once() ) + ->method( 'get_brief_suggestions' ) ->willReturn( new WP_Error( 'api_error', 'API request failed' ) ); - $this->set_protected_property( $this->get_endpoint(), 'suggest_brief_api', $mock_suggest_api ); + self::set_protected_property( $this->get_endpoint(), 'suggestions_api', $mock_suggestions_api ); // Create a mock request. $request = new WP_REST_Request( 'POST', '/excerpt-generator/generate' ); diff --git a/tests/Integration/RestAPI/ContentHelper/EndpointSmartLinkingTest.php b/tests/Integration/RestAPI/ContentHelper/EndpointSmartLinkingTest.php index 9fd983fefa..6e17109ee9 100644 --- a/tests/Integration/RestAPI/ContentHelper/EndpointSmartLinkingTest.php +++ b/tests/Integration/RestAPI/ContentHelper/EndpointSmartLinkingTest.php @@ -13,6 +13,8 @@ use Parsely\REST_API\Content_Helper\Endpoint_Smart_Linking; use Parsely\REST_API\Content_Helper\Content_Helper_Controller; use Parsely\RemoteAPI\ContentSuggestions\Suggest_Linked_Reference_API; +use Parsely\Services\Suggestions_API\Endpoints\Endpoint_Suggest_Linked_Reference; +use Parsely\Services\Suggestions_API\Suggestions_API_Service; use Parsely\Tests\Integration\RestAPI\BaseEndpointTest; use Parsely\Models\Smart_Link; use Parsely\Tests\Integration\TestCase; @@ -78,7 +80,6 @@ public function get_endpoint(): \Parsely\REST_API\Base_Endpoint { * @since 3.17.0 * * @covers \Parsely\REST_API\Content_Helper\Endpoint_Smart_Linking::register_routes - * @uses \Parsely\Endpoints\Base_Endpoint::__construct * @uses \Parsely\Parsely::api_secret_is_set * @uses \Parsely\Parsely::get_managed_credentials * @uses \Parsely\Parsely::get_options @@ -125,7 +126,6 @@ public function test_route_is_registered(): void { * @since 3.17.0 * * @covers \Parsely\REST_API\Content_Helper\Endpoint_Smart_Linking::generate_smart_links - * @uses \Parsely\Endpoints\Base_Endpoint::__construct * @uses \Parsely\Models\Base_Model::__construct * @uses \Parsely\Models\Smart_Link::__construct * @uses \Parsely\Models\Smart_Link::generate_uid @@ -139,10 +139,10 @@ public function test_route_is_registered(): void { * @uses \Parsely\Utils\Utils::convert_endpoint_to_filter_key */ public function test_generate_smart_links_returns_valid_response(): void { - // Mock the Suggest_Linked_Reference_API to control the response. - $mock_suggest_api = $this->createMock( Suggest_Linked_Reference_API::class ); - $mock_suggest_api->expects( self::once() ) - ->method( 'get_links' ) + // Create a mocked Suggestions API that returns two smart links. + $mock_suggestions_api = $this->createMock( Suggestions_API_Service::class ); + $mock_suggestions_api->expects( self::once() ) + ->method( 'get_smart_links' ) ->willReturn( array( new Smart_Link( 'link1', 'http://example.com/1', 'Example 1', 0, 0 ), @@ -150,7 +150,7 @@ public function test_generate_smart_links_returns_valid_response(): void { ) ); - $this->set_protected_property( $this->get_endpoint(), 'suggest_linked_reference_api', $mock_suggest_api ); + self::set_protected_property( $this->get_endpoint(), 'suggestions_api', $mock_suggestions_api ); // Create a mock request. $request = new WP_REST_Request( 'POST', '/smart-linking/generate' ); @@ -180,7 +180,6 @@ public function test_generate_smart_links_returns_valid_response(): void { * @since 3.17.0 * * @covers \Parsely\REST_API\Content_Helper\Endpoint_Smart_Linking::generate_smart_links - * @uses \Parsely\Endpoints\Base_Endpoint::__construct * @uses \Parsely\REST_API\Base_API_Controller::__construct * @uses \Parsely\REST_API\Base_API_Controller::get_parsely * @uses \Parsely\REST_API\Base_Endpoint::__construct @@ -188,13 +187,13 @@ public function test_generate_smart_links_returns_valid_response(): void { * @uses \Parsely\Utils\Utils::convert_endpoint_to_filter_key */ public function test_generate_smart_links_returns_error_on_failure(): void { - // Mock the Suggest_Linked_Reference_API to simulate a failure. - $mock_suggest_api = $this->createMock( Suggest_Linked_Reference_API::class ); - $mock_suggest_api->expects( self::once() ) - ->method( 'get_links' ) - ->willReturn( new WP_Error( 'api_error', 'API request failed' ) ); + // Mock the Suggestions API `get_smart_links` method to return a WP_Error. + $mock_suggestions_api = $this->createMock( Suggestions_API_Service::class ); + $mock_suggestions_api->expects( self::once() ) + ->method( 'get_smart_links' ) + ->willReturn( new WP_Error( 'api_error', 'API request failed' ) ); - $this->set_protected_property( $this->get_endpoint(), 'suggest_linked_reference_api', $mock_suggest_api ); + self::set_protected_property( $this->get_endpoint(), 'suggestions_api', $mock_suggestions_api ); // Create a mock request. $request = new WP_REST_Request( 'POST', '/smart-linking/generate' ); @@ -216,7 +215,6 @@ public function test_generate_smart_links_returns_error_on_failure(): void { * @since 3.17.0 * * @covers \Parsely\REST_API\Content_Helper\Endpoint_Smart_Linking::add_smart_link - * @uses \Parsely\Endpoints\Base_Endpoint::__construct * @uses \Parsely\Models\Base_Model::__construct * @uses \Parsely\Models\Base_Model::serialize * @uses \Parsely\Models\Smart_Link::__construct @@ -316,7 +314,6 @@ public function test_add_smart_link_returns_valid_response(): void { * @since 3.17.0 * * @covers \Parsely\REST_API\Content_Helper\Endpoint_Smart_Linking::add_multiple_smart_links - * @uses \Parsely\Endpoints\Base_Endpoint::__construct * @uses \Parsely\Models\Base_Model::__construct * @uses \Parsely\Models\Base_Model::serialize * @uses \Parsely\Models\Smart_Link::__construct diff --git a/tests/Integration/RestAPI/ContentHelper/EndpointTitleSuggestionsTest.php b/tests/Integration/RestAPI/ContentHelper/EndpointTitleSuggestionsTest.php index 508a1fbabe..34969bd2fb 100644 --- a/tests/Integration/RestAPI/ContentHelper/EndpointTitleSuggestionsTest.php +++ b/tests/Integration/RestAPI/ContentHelper/EndpointTitleSuggestionsTest.php @@ -13,6 +13,7 @@ use Parsely\REST_API\Content_Helper\Endpoint_Title_Suggestions; use Parsely\REST_API\Content_Helper\Content_Helper_Controller; use Parsely\RemoteAPI\ContentSuggestions\Suggest_Headline_API; +use Parsely\Services\Suggestions_API\Suggestions_API_Service; use Parsely\Tests\Integration\RestAPI\BaseEndpointTest; use WP_Error; use WP_REST_Request; @@ -53,8 +54,9 @@ public function set_up(): void { /** * Gets the test endpoint instance. * - * @return Endpoint_Title_Suggestions * @since 3.17.0 + * + * @return Endpoint_Title_Suggestions */ public function get_endpoint(): \Parsely\REST_API\Base_Endpoint { return $this->endpoint; @@ -66,30 +68,7 @@ public function get_endpoint(): \Parsely\REST_API\Base_Endpoint { * @since 3.17.0 * * @covers \Parsely\REST_API\Content_Helper\Endpoint_Title_Suggestions::register_routes - * @uses \Parsely\Endpoints\Base_Endpoint::__construct - * @uses \Parsely\Parsely::api_secret_is_set - * @uses \Parsely\Parsely::get_managed_credentials - * @uses \Parsely\Parsely::get_options - * @uses \Parsely\Parsely::set_default_content_helper_settings_values - * @uses \Parsely\Parsely::set_default_full_metadata_in_non_posts - * @uses \Parsely\Parsely::site_id_is_set - * @uses \Parsely\Permissions::build_pch_permissions_settings_array - * @uses \Parsely\Permissions::current_user_can_use_pch_feature - * @uses \Parsely\Permissions::get_user_roles_with_edit_posts_cap - * @uses \Parsely\REST_API\Base_API_Controller::__construct - * @uses \Parsely\REST_API\Base_API_Controller::get_full_namespace - * @uses \Parsely\REST_API\Base_API_Controller::get_parsely - * @uses \Parsely\REST_API\Base_API_Controller::prefix_route - * @uses \Parsely\REST_API\Base_Endpoint::__construct * @uses \Parsely\REST_API\Base_Endpoint::get_full_endpoint - * @uses \Parsely\REST_API\Base_Endpoint::init - * @uses \Parsely\REST_API\Base_Endpoint::is_available_to_current_user - * @uses \Parsely\REST_API\Base_Endpoint::register_rest_route - * @uses \Parsely\REST_API\Base_Endpoint::validate_site_id_and_secret - * @uses \Parsely\REST_API\Content_Helper\Content_Helper_Controller::get_route_prefix - * @uses \Parsely\REST_API\REST_API_Controller::get_namespace - * @uses \Parsely\REST_API\REST_API_Controller::get_version - * @uses \Parsely\Utils\Utils::convert_endpoint_to_filter_key */ public function test_route_is_registered(): void { $routes = rest_get_server()->get_routes(); @@ -109,25 +88,28 @@ public function test_route_is_registered(): void { * @since 3.17.0 * * @covers \Parsely\REST_API\Content_Helper\Endpoint_Title_Suggestions::generate_titles - * @uses \Parsely\Endpoints\Base_Endpoint::__construct - * @uses \Parsely\Parsely::__construct - * @uses \Parsely\Parsely::allow_parsely_remote_requests - * @uses \Parsely\Parsely::are_credentials_managed - * @uses \Parsely\Parsely::set_managed_options + * @uses \Parsely\Parsely::get_suggestions_api * @uses \Parsely\REST_API\Base_API_Controller::__construct * @uses \Parsely\REST_API\Base_API_Controller::get_parsely * @uses \Parsely\REST_API\Base_Endpoint::__construct * @uses \Parsely\REST_API\Base_Endpoint::init + * @uses \Parsely\Services\Base_API_Service::__construct + * @uses \Parsely\Services\Base_API_Service::register_endpoint + * @uses \Parsely\Services\Base_Service_Endpoint::__construct + * @uses \Parsely\Services\Suggestions_API\Endpoints\Endpoint_Suggest_Brief::get_endpoint + * @uses \Parsely\Services\Suggestions_API\Endpoints\Endpoint_Suggest_Headline::get_endpoint + * @uses \Parsely\Services\Suggestions_API\Endpoints\Endpoint_Suggest_Linked_Reference::get_endpoint + * @uses \Parsely\Services\Suggestions_API\Suggestions_API_Service::register_endpoints * @uses \Parsely\Utils\Utils::convert_endpoint_to_filter_key */ public function test_generate_titles_returns_valid_response(): void { - // Mock the Suggest_Headline_API to control the response. - $mock_suggest_api = $this->createMock( Suggest_Headline_API::class ); - $mock_suggest_api->expects( self::once() ) - ->method( 'get_titles' ) - ->willReturn( array( 'title1', 'title2', 'title3' ) ); + // Mock the Suggestions API `get_title_suggestions` method to return a list of titles. + $mock_suggestions_api = $this->createMock( Suggestions_API_Service::class ); + $mock_suggestions_api->expects( self::once() ) + ->method( 'get_title_suggestions' ) + ->willReturn( array( 'title1', 'title2', 'title3' ) ); - $this->set_protected_property( $this->get_endpoint(), 'suggest_headline_api', $mock_suggest_api ); + self::set_protected_property( $this->get_endpoint(), 'suggestions_api', $mock_suggestions_api ); // Create a mock request. $request = new WP_REST_Request( 'POST', '/title-suggestions/generate' ); @@ -158,25 +140,28 @@ public function test_generate_titles_returns_valid_response(): void { * @since 3.17.0 * * @covers \Parsely\REST_API\Content_Helper\Endpoint_Title_Suggestions::generate_titles - * @uses \Parsely\Endpoints\Base_Endpoint::__construct - * @uses \Parsely\Parsely::__construct - * @uses \Parsely\Parsely::allow_parsely_remote_requests - * @uses \Parsely\Parsely::are_credentials_managed - * @uses \Parsely\Parsely::set_managed_options + * @uses \Parsely\Parsely::get_suggestions_api * @uses \Parsely\REST_API\Base_API_Controller::__construct * @uses \Parsely\REST_API\Base_API_Controller::get_parsely * @uses \Parsely\REST_API\Base_Endpoint::__construct * @uses \Parsely\REST_API\Base_Endpoint::init + * @uses \Parsely\Services\Base_API_Service::__construct + * @uses \Parsely\Services\Base_API_Service::register_endpoint + * @uses \Parsely\Services\Base_Service_Endpoint::__construct + * @uses \Parsely\Services\Suggestions_API\Endpoints\Endpoint_Suggest_Brief::get_endpoint + * @uses \Parsely\Services\Suggestions_API\Endpoints\Endpoint_Suggest_Headline::get_endpoint + * @uses \Parsely\Services\Suggestions_API\Endpoints\Endpoint_Suggest_Linked_Reference::get_endpoint + * @uses \Parsely\Services\Suggestions_API\Suggestions_API_Service::register_endpoints * @uses \Parsely\Utils\Utils::convert_endpoint_to_filter_key */ public function test_generate_titles_returns_error_on_failure(): void { - // Mock the Suggest_Headline_API to simulate a failure. - $mock_suggest_api = $this->createMock( Suggest_Headline_API::class ); - $mock_suggest_api->expects( self::once() ) - ->method( 'get_titles' ) - ->willReturn( new WP_Error( 'api_error', 'API request failed' ) ); + // Mock the Suggestions API `get_title_suggestions` method to return an error. + $mock_suggestions_api = $this->createMock( Suggestions_API_Service::class ); + $mock_suggestions_api->expects( self::once() ) + ->method( 'get_title_suggestions' ) + ->willReturn( new WP_Error( 'api_error', 'API request failed' ) ); - $this->set_protected_property( $this->get_endpoint(), 'suggest_headline_api', $mock_suggest_api ); + self::set_protected_property( $this->get_endpoint(), 'suggestions_api', $mock_suggestions_api ); // Create a mock request. $request = new WP_REST_Request( 'POST', '/title-suggestions/generate' ); diff --git a/tests/Integration/RestAPI/Stats/EndpointPostTest.php b/tests/Integration/RestAPI/Stats/EndpointPostTest.php index 013e6e2c1f..a990af6933 100644 --- a/tests/Integration/RestAPI/Stats/EndpointPostTest.php +++ b/tests/Integration/RestAPI/Stats/EndpointPostTest.php @@ -63,7 +63,6 @@ public function get_endpoint(): \Parsely\REST_API\Base_Endpoint { * @since 3.17.0 * * @covers \Parsely\REST_API\Stats\Endpoint_Post::register_routes - * @uses \Parsely\Endpoints\Base_Endpoint::__construct * @uses \Parsely\Parsely::api_secret_is_set * @uses \Parsely\Parsely::get_managed_credentials * @uses \Parsely\Parsely::get_options @@ -113,7 +112,6 @@ public function test_route_is_registered(): void { * Verifies that the endpoint is not available if the API Secret is not set. * * @covers \Parsely\REST_API\Stats\Endpoint_Post::is_available_to_current_user - * @uses \Parsely\Endpoints\Base_Endpoint::__construct * @uses \Parsely\Parsely::api_secret_is_set * @uses \Parsely\Parsely::get_managed_credentials * @uses \Parsely\Parsely::get_options @@ -170,7 +168,6 @@ public function test_access_error_if_api_secret_is_not_set(): void { * capabilities. * * @covers \Parsely\REST_API\Stats\Endpoint_Post::is_available_to_current_user - * @uses \Parsely\Endpoints\Base_Endpoint::__construct * @uses \Parsely\Parsely::api_secret_is_set * @uses \Parsely\Parsely::get_managed_credentials * @uses \Parsely\Parsely::get_options @@ -238,7 +235,6 @@ public function test_access_of_stats_post_endpoint_is_forbidden(): void { * @since 3.17.0 * * @covers \Parsely\REST_API\Stats\Endpoint_Post::get_post_details - * @uses \Parsely\Endpoints\Base_Endpoint::__construct * @uses \Parsely\Parsely::api_secret_is_set * @uses \Parsely\Parsely::get_api_secret * @uses \Parsely\Parsely::get_dash_url @@ -275,10 +271,6 @@ public function test_access_of_stats_post_endpoint_is_forbidden(): void { * @uses \Parsely\REST_API\Stats\Stats_Controller::get_route_prefix * @uses \Parsely\REST_API\Use_Post_ID_Parameter_Trait::register_rest_route_with_post_id * @uses \Parsely\REST_API\Use_Post_ID_Parameter_Trait::validate_post_id - * @uses \Parsely\RemoteAPI\Base_Endpoint_Remote::get_api_url - * @uses \Parsely\RemoteAPI\Base_Endpoint_Remote::get_items - * @uses \Parsely\RemoteAPI\Base_Endpoint_Remote::get_request_options - * @uses \Parsely\RemoteAPI\Base_Endpoint_Remote::validate_required_constraints * @uses \Parsely\Utils\Utils::convert_endpoint_to_filter_key * @uses \Parsely\Utils\Utils::get_formatted_duration * @uses \Parsely\Utils\Utils::parsely_is_https_supported @@ -351,7 +343,6 @@ function () use ( &$dispatched ): array { * @since 3.17.0 * * @covers \Parsely\REST_API\Stats\Endpoint_Post::get_post_referrers - * @uses \Parsely\Endpoints\Base_Endpoint::__construct * @uses \Parsely\Parsely::api_secret_is_set * @uses \Parsely\Parsely::get_api_secret * @uses \Parsely\Parsely::get_managed_credentials @@ -388,10 +379,6 @@ function () use ( &$dispatched ): array { * @uses \Parsely\REST_API\Stats\Stats_Controller::get_route_prefix * @uses \Parsely\REST_API\Use_Post_ID_Parameter_Trait::register_rest_route_with_post_id * @uses \Parsely\REST_API\Use_Post_ID_Parameter_Trait::validate_post_id - * @uses \Parsely\RemoteAPI\Base_Endpoint_Remote::get_api_url - * @uses \Parsely\RemoteAPI\Base_Endpoint_Remote::get_items - * @uses \Parsely\RemoteAPI\Base_Endpoint_Remote::get_request_options - * @uses \Parsely\RemoteAPI\Base_Endpoint_Remote::validate_required_constraints * @uses \Parsely\Utils\Utils::convert_endpoint_to_filter_key * @uses \Parsely\Utils\Utils::convert_to_positive_integer */ @@ -470,61 +457,61 @@ function () use ( &$dispatched ): array { } ); - $expected_top = (object) array( - 'direct' => (object) array( + $expected_top = array( + 'direct' => array( 'views' => '770', 'viewsPercentage' => '30.80', 'datasetViewsPercentage' => '31.43', ), - 'google' => (object) array( + 'google' => array( 'views' => '1,500', 'viewsPercentage' => '60.00', 'datasetViewsPercentage' => '61.22', ), - 'blog.parse.ly' => (object) array( + 'blog.parse.ly' => array( 'views' => '100', 'viewsPercentage' => '4.00', 'datasetViewsPercentage' => '4.08', ), - 'bing' => (object) array( + 'bing' => array( 'views' => '50', 'viewsPercentage' => '2.00', 'datasetViewsPercentage' => '2.04', ), - 'facebook.com' => (object) array( + 'facebook.com' => array( 'views' => '30', 'viewsPercentage' => '1.20', 'datasetViewsPercentage' => '1.22', ), - 'totals' => (object) array( + 'totals' => array( 'views' => '2,450', 'viewsPercentage' => '98.00', 'datasetViewsPercentage' => '100.00', ), ); - $expected_types = (object) array( - 'social' => (object) array( + $expected_types = array( + 'social' => array( 'views' => '30', 'viewsPercentage' => '1.20', ), - 'search' => (object) array( + 'search' => array( 'views' => '1,570', 'viewsPercentage' => '62.80', ), - 'other' => (object) array( + 'other' => array( 'views' => '20', 'viewsPercentage' => '0.80', ), - 'internal' => (object) array( + 'internal' => array( 'views' => '110', 'viewsPercentage' => '4.40', ), - 'direct' => (object) array( + 'direct' => array( 'views' => '770', 'viewsPercentage' => '30.80', ), - 'totals' => (object) array( + 'totals' => array( 'views' => '2,500', 'viewsPercentage' => '100.00', ), @@ -560,7 +547,6 @@ function () use ( &$dispatched ): array { * @since 3.17.0 * * @covers \Parsely\REST_API\Stats\Endpoint_Post::get_related_posts - * @uses \Parsely\Endpoints\Base_Endpoint::__construct * @uses \Parsely\Parsely::api_secret_is_set * @uses \Parsely\Parsely::get_api_secret * @uses \Parsely\Parsely::get_managed_credentials @@ -596,10 +582,6 @@ function () use ( &$dispatched ): array { * @uses \Parsely\REST_API\Stats\Stats_Controller::get_route_prefix * @uses \Parsely\REST_API\Use_Post_ID_Parameter_Trait::register_rest_route_with_post_id * @uses \Parsely\REST_API\Use_Post_ID_Parameter_Trait::validate_post_id - * @uses \Parsely\RemoteAPI\Base_Endpoint_Remote::get_api_url - * @uses \Parsely\RemoteAPI\Base_Endpoint_Remote::get_items - * @uses \Parsely\RemoteAPI\Base_Endpoint_Remote::get_request_options - * @uses \Parsely\RemoteAPI\Base_Endpoint_Remote::validate_required_constraints * @uses \Parsely\Utils\Utils::convert_endpoint_to_filter_key */ public function test_get_related_posts(): void { @@ -651,13 +633,13 @@ function () use ( &$dispatched ): array { self::assertSame( 200, $response->get_status() ); self::assertEquals( array( - (object) array( + array( 'image_url' => 'https://example.com/img.png', 'thumb_url_medium' => 'https://example.com/thumb.png', 'title' => 'something', 'url' => 'https://example.com', ), - (object) array( + array( 'image_url' => 'https://example.com/img2.png', 'thumb_url_medium' => 'https://example.com/thumb2.png', 'title' => 'something2', diff --git a/tests/Integration/RestAPI/Stats/EndpointPostsTest.php b/tests/Integration/RestAPI/Stats/EndpointPostsTest.php index 62d7fd39e6..0cde8e31f9 100644 --- a/tests/Integration/RestAPI/Stats/EndpointPostsTest.php +++ b/tests/Integration/RestAPI/Stats/EndpointPostsTest.php @@ -64,7 +64,6 @@ public function get_endpoint(): \Parsely\REST_API\Base_Endpoint { * @since 3.17.0 * * @covers \Parsely\REST_API\Stats\Endpoint_Posts::register_routes - * @uses \Parsely\Endpoints\Base_Endpoint::__construct * @uses \Parsely\Parsely::api_secret_is_set * @uses \Parsely\Parsely::get_managed_credentials * @uses \Parsely\Parsely::get_options @@ -104,7 +103,6 @@ public function test_route_is_registered(): void { * Verifies that the endpoint is not available if the API Secret is not set. * * @covers \Parsely\REST_API\Stats\Endpoint_Posts::is_available_to_current_user - * @uses \Parsely\Endpoints\Base_Endpoint::__construct * @uses \Parsely\Parsely::api_secret_is_set * @uses \Parsely\Parsely::get_managed_credentials * @uses \Parsely\Parsely::get_options @@ -153,7 +151,6 @@ public function test_access_error_if_api_secret_is_not_set(): void { * capabilities. * * @covers \Parsely\REST_API\Stats\Endpoint_Posts::is_available_to_current_user - * @uses \Parsely\Endpoints\Base_Endpoint::__construct * @uses \Parsely\Parsely::api_secret_is_set * @uses \Parsely\Parsely::get_managed_credentials * @uses \Parsely\Parsely::get_options @@ -211,7 +208,6 @@ public function test_access_of_stats_posts_endpoint_is_forbidden(): void { * expected format. * * @covers \Parsely\REST_API\Stats\Endpoint_Posts::get_posts - * @uses \Parsely\Endpoints\Base_Endpoint::__construct * @uses \Parsely\Parsely::api_secret_is_set * @uses \Parsely\Parsely::get_api_secret * @uses \Parsely\Parsely::get_dash_url @@ -238,10 +234,6 @@ public function test_access_of_stats_posts_endpoint_is_forbidden(): void { * @uses \Parsely\REST_API\REST_API_Controller::get_namespace * @uses \Parsely\REST_API\REST_API_Controller::get_version * @uses \Parsely\REST_API\Stats\Stats_Controller::get_route_prefix - * @uses \Parsely\RemoteAPI\Analytics_Posts_API::get_request_options - * @uses \Parsely\RemoteAPI\Base_Endpoint_Remote::get_api_url - * @uses \Parsely\RemoteAPI\Base_Endpoint_Remote::get_items - * @uses \Parsely\RemoteAPI\Base_Endpoint_Remote::validate_required_constraints * @uses \Parsely\Utils\Utils::convert_endpoint_to_filter_key * @uses \Parsely\Utils\Utils::get_date_format * @uses \Parsely\Utils\Utils::parsely_is_https_supported diff --git a/tests/Integration/RestAPI/Stats/EndpointRelatedTest.php b/tests/Integration/RestAPI/Stats/EndpointRelatedTest.php index 4232b82f76..616818d988 100644 --- a/tests/Integration/RestAPI/Stats/EndpointRelatedTest.php +++ b/tests/Integration/RestAPI/Stats/EndpointRelatedTest.php @@ -61,7 +61,6 @@ public function get_endpoint(): \Parsely\REST_API\Base_Endpoint { * @since 3.17.0 * * @covers \Parsely\REST_API\Stats\Endpoint_Related::register_routes - * @uses \Parsely\Endpoints\Base_Endpoint::__construct * @uses \Parsely\REST_API\Base_API_Controller::__construct * @uses \Parsely\REST_API\Base_API_Controller::get_full_namespace * @uses \Parsely\REST_API\Base_API_Controller::get_parsely @@ -99,7 +98,6 @@ public function test_route_is_registered(): void { * @since 3.17.0 * * @covers \Parsely\REST_API\Stats\Endpoint_Related::is_available_to_current_user - * @uses \Parsely\Endpoints\Base_Endpoint::__construct * @uses \Parsely\Parsely::api_secret_is_set * @uses \Parsely\Parsely::get_api_secret * @uses \Parsely\Parsely::get_managed_credentials @@ -130,10 +128,6 @@ public function test_route_is_registered(): void { * @uses \Parsely\REST_API\Stats\Related_Posts_Trait::get_related_posts_of_url * @uses \Parsely\REST_API\Stats\Related_Posts_Trait::get_related_posts_param_args * @uses \Parsely\REST_API\Stats\Stats_Controller::get_route_prefix - * @uses \Parsely\RemoteAPI\Base_Endpoint_Remote::get_api_url - * @uses \Parsely\RemoteAPI\Base_Endpoint_Remote::get_items - * @uses \Parsely\RemoteAPI\Base_Endpoint_Remote::get_request_options - * @uses \Parsely\RemoteAPI\Base_Endpoint_Remote::validate_required_constraints * @uses \Parsely\Utils\Utils::convert_endpoint_to_filter_key */ public function test_access_of_related_posts_is_available_to_everyone(): void { @@ -196,7 +190,6 @@ function () use ( &$dispatched ): array { * @since 3.17.0 * * @covers \Parsely\REST_API\Stats\Endpoint_Related::get_related_posts - * @uses \Parsely\Endpoints\Base_Endpoint::__construct * @uses \Parsely\Parsely::api_secret_is_set * @uses \Parsely\Parsely::get_api_secret * @uses \Parsely\Parsely::get_managed_credentials @@ -227,10 +220,6 @@ function () use ( &$dispatched ): array { * @uses \Parsely\REST_API\Stats\Related_Posts_Trait::get_related_posts_of_url * @uses \Parsely\REST_API\Stats\Related_Posts_Trait::get_related_posts_param_args * @uses \Parsely\REST_API\Stats\Stats_Controller::get_route_prefix - * @uses \Parsely\RemoteAPI\Base_Endpoint_Remote::get_api_url - * @uses \Parsely\RemoteAPI\Base_Endpoint_Remote::get_items - * @uses \Parsely\RemoteAPI\Base_Endpoint_Remote::get_request_options - * @uses \Parsely\RemoteAPI\Base_Endpoint_Remote::validate_required_constraints * @uses \Parsely\Utils\Utils::convert_endpoint_to_filter_key */ public function test_get_related_posts(): void { @@ -283,13 +272,13 @@ function () use ( &$dispatched ): array { self::assertSame( 200, $response->get_status() ); self::assertEquals( array( - (object) array( + array( 'image_url' => 'https://example.com/img.png', 'thumb_url_medium' => 'https://example.com/thumb.png', 'title' => 'something', 'url' => 'https://example.com', ), - (object) array( + array( 'image_url' => 'https://example.com/img2.png', 'thumb_url_medium' => 'https://example.com/thumb2.png', 'title' => 'something2', diff --git a/tests/Integration/Services/BaseAPIServiceTestCase.php b/tests/Integration/Services/BaseAPIServiceTestCase.php new file mode 100644 index 0000000000..30d71d8028 --- /dev/null +++ b/tests/Integration/Services/BaseAPIServiceTestCase.php @@ -0,0 +1,57 @@ +> + */ + abstract public function data_registered_endpoints(): iterable; + + /** + * Runs once before all tests. + * + * @since 3.17.0 + */ + public static function set_up_before_class(): void { + static::initialize(); + } +} diff --git a/tests/Integration/Services/BaseServiceEndpointTestCase.php b/tests/Integration/Services/BaseServiceEndpointTestCase.php new file mode 100644 index 0000000000..7d55190c6e --- /dev/null +++ b/tests/Integration/Services/BaseServiceEndpointTestCase.php @@ -0,0 +1,136 @@ + + */ + abstract public function data_api_url(): iterable; + + /** + * Returns the endpoint for the API request. + * + * @since 3.17.0 + * + * @return Base_Service_Endpoint + */ + abstract public function get_service_endpoint(): Base_Service_Endpoint; + + /** + * Runs once before all tests. + * + * @since 3.17.0 + */ + public static function set_up_before_class(): void { + static::initialize(); + } + + /** + * Verifies that the truncate_array_content() method truncates long text in + * any member of the array. + * + * @since 3.17.0 + * + * @covers \Parsely\Services\Base_Service_Endpoint::truncate_array_content + */ + public function test_truncate_body_content(): void { + $endpoint = $this->get_service_endpoint(); + + $body = array( + 'output_params' => array( + 'some_param' => true, + 'other_param' => 'Hello', + 'recursive' => array( + 'key' => 'value', + ), + ), + 'text' => $this->generate_content_with_length( 30000 ), + 'something' => 'else', + ); + + $truncate_array_content = self::get_method( 'truncate_array_content', $endpoint ); + $truncated_array = $truncate_array_content->invoke( $endpoint, $body ); + + self::assertIsArray( $truncated_array ); + self::assertArrayHasKey( 'output_params', $truncated_array ); + self::assertArrayHasKey( 'text', $truncated_array ); + self::assertLessThanOrEqual( 25000, strlen( $truncated_array['text'] ) ); + + // Assert that the truncated text is the beginning of the original text. + self::assertStringStartsWith( $truncated_array['text'], $body['text'] ); + + // Assert that the other keys are the same in both arrays. + self::assertEquals( $body['output_params'], $truncated_array['output_params'] ); + self::assertEquals( $body['something'], $truncated_array['something'] ); + } + + /** + * Generates content with a specific length. + * + * @since 3.17.0 + * + * @param int $length Length of the generated content. + * @return string The generated content. + */ + private function generate_content_with_length( int $length ): string { + $words = array( 'lorem', 'ipsum', 'dolor', 'sit', 'amet', 'consectetur', 'adipiscing', 'elit' ); + $string = ''; + $current_length = 0; + + while ( $current_length < $length ) { + $word = $words[ array_rand( $words ) ]; + if ( $current_length > 0 ) { + if ( $current_length + strlen( $word ) + 1 > $length ) { + break; + } + $string .= ' '; + ++$current_length; + } + $string .= $word; + $current_length += strlen( $word ); + } + + return $string; + } +} diff --git a/tests/Integration/Services/ContentAPI/ContentApiServiceTestCase.php b/tests/Integration/Services/ContentAPI/ContentApiServiceTestCase.php new file mode 100644 index 0000000000..4b03397ed8 --- /dev/null +++ b/tests/Integration/Services/ContentAPI/ContentApiServiceTestCase.php @@ -0,0 +1,133 @@ + + */ + private static $endpoints; + + /** + * Initializes all required values for the test. + * + * @since 3.17.0 + */ + public static function initialize(): void { + self::$api_service = new Content_API_Service( new Parsely() ); + + // Get the endpoints from the protected $endpoints property using reflection. + $endpoints_prop = self::get_property( 'endpoints', self::$api_service ); + + /** @var array $endpoints */ + $endpoints = $endpoints_prop->getValue( self::$api_service ); + + // Store it. + self::$endpoints = $endpoints; + } + + /** + * Provides data for test_endpoint_is_registered(). + * + * Should return an array of arrays, each containing the endpoint name and + * class. + * + * @since 3.17.0 + * + * @return array> + */ + public function data_registered_endpoints(): iterable { + return array( + 'analytics/posts' => array( + 'endpoint' => '/analytics/posts', + 'class' => Endpoint_Analytics_Posts::class, + ), + 'analytics/post/details' => array( + 'endpoint' => '/analytics/post/detail', + 'class' => Endpoint_Analytics_Post_Details::class, + ), + 'related' => array( + 'endpoint' => '/related', + 'class' => Endpoint_Related::class, + ), + 'referrers/post/details' => array( + 'endpoint' => '/referrers/post/detail', + 'class' => Endpoint_Referrers_Post_Detail::class, + ), + 'validate/secret' => array( + 'endpoint' => '/validate/secret', + 'class' => Endpoint_Validate::class, + ), + ); + } + + /** + * Verifies that the number of registered endpoints is as expected. + * + * @since 3.17.0 + * + * @covers \Parsely\Services\Suggestions_API\Suggestions_API_Service::register_endpoint + */ + public function test_number_of_registered_endpoints_is_as_expected(): void { + self::assertSameSize( $this->data_registered_endpoints(), self::$endpoints ); + } + + /** + * Tests that the endpoint is registered and is an instance of the expected class. + * + * @since 3.17.0 + * + * @dataProvider data_registered_endpoints + * @covers \Parsely\Services\Base_API_Service::get_endpoint + * @uses \Parsely\Services\Cached_Service_Endpoint::get_uncached_endpoint + * + * @param string $endpoint The endpoint name to check. + * @param class-string $class_name The endpoint class to check. + */ + public function test_endpoint_is_registered( string $endpoint, string $class_name ): void { + // Check that the endpoint exists and is an instance of the expected class. + self::assertArrayHasKey( $endpoint, self::$endpoints, "Endpoint $endpoint is not registered." ); + $endpoint = self::$api_service->get_endpoint( $endpoint ); + self::assertInstanceOf( Base_Service_Endpoint::class, $endpoint ); + + // If the endpoint is cached, check the inner endpoint. + if ( $endpoint instanceof Cached_Service_Endpoint ) { + self::assertInstanceOf( $class_name, $endpoint->get_uncached_endpoint() ); + } else { + self::assertInstanceOf( $class_name, $endpoint ); + } + } +} diff --git a/tests/Integration/Services/ContentAPI/Endpoints/ContentAPIBaseEndpointTestCase.php b/tests/Integration/Services/ContentAPI/Endpoints/ContentAPIBaseEndpointTestCase.php new file mode 100644 index 0000000000..6233563deb --- /dev/null +++ b/tests/Integration/Services/ContentAPI/Endpoints/ContentAPIBaseEndpointTestCase.php @@ -0,0 +1,129 @@ + $query Test query arguments. + * @param string $url Expected generated URL. + */ + public function test_api_url( array $query, string $url ): void { + // Get the endpoint object. + $endpoint = $this->get_service_endpoint(); + self::assertInstanceOf( Base_Service_Endpoint::class, $endpoint ); + + self::set_options( + array( + 'apikey' => 'my-key', + 'api_secret' => 'my-secret', + ) + ); + + self::assertSame( $url, $endpoint->get_endpoint_url( $query ) ); + } + + /** + * Verifies the basic generation of the API headers. + * + * @since 3.17.0 + * + * @covers \Parsely\Tests\Integration\Services\ContentAPI\Endpoints\Content_API_Base_Endpoint::get_request_options + */ + public function test_api_authentication(): void { + // Assume self::$api_service is always an instance of Suggestions_API_Service. + // If there's any doubt, this should be handled in the test setup or constructor, + // not within individual tests. + self::assertInstanceOf( Content_API_Service::class, self::$api_service ); + + // Get the endpoint object. + $endpoint = $this->get_service_endpoint(); + self::assertInstanceOf( Base_Service_Endpoint::class, $endpoint ); + + // Set options - this assumes set_options() does not produce side effects that affect other tests. + // Consider resetting any global or static state in a tearDown() method if necessary. + self::set_options( + array( + 'apikey' => 'my-key', + 'api_secret' => 'my-secret', + ) + ); + + // Call the protected method get_query_args() using reflection. + $get_query_args = self::get_method( 'get_query_args', $endpoint ); + $query_args = $get_query_args->invoke( $endpoint ); + + // Ensure that $query_args is an array. + self::assertIsArray( $query_args ); + + // Verify the API key and secret are present in the query args and their values + // match the ones set in the options. + self::assertArrayHasKey( 'apikey', $query_args ); + self::assertEquals( 'my-key', $query_args['apikey'] ); + + self::assertArrayHasKey( 'secret', $query_args ); + self::assertEquals( 'my-secret', $query_args['secret'] ); + } +} diff --git a/tests/Integration/Services/ContentAPI/Endpoints/EndpointAnalyticsPostsTest.php b/tests/Integration/Services/ContentAPI/Endpoints/EndpointAnalyticsPostsTest.php new file mode 100644 index 0000000000..4d522fe481 --- /dev/null +++ b/tests/Integration/Services/ContentAPI/Endpoints/EndpointAnalyticsPostsTest.php @@ -0,0 +1,48 @@ +get_content_api()->get_endpoint( '/analytics/posts' ); + } + + /** + * Provides data for test_api_url(). + * + * @since 3.17.0 + * + * @return \ArrayIterator + */ + public function data_api_url(): iterable { + yield 'Basic (Expected data)' => array( + array( + 'limit' => 5, + ), + Content_API_Service::get_base_url() . '/analytics/posts?limit=5&apikey=my-key&secret=my-secret', + ); + } +} diff --git a/tests/Integration/Services/ContentAPI/Endpoints/EndpointRelatedTest.php b/tests/Integration/Services/ContentAPI/Endpoints/EndpointRelatedTest.php new file mode 100644 index 0000000000..2b39a5717b --- /dev/null +++ b/tests/Integration/Services/ContentAPI/Endpoints/EndpointRelatedTest.php @@ -0,0 +1,54 @@ +get_content_api()->get_endpoint( '/related' ); + } + + /** + * Provides data for test_api_url(). + * + * @return \ArrayIterator + */ + public function data_api_url(): iterable { + yield 'Basic (Expected data)' => array( + array( + 'limit' => 5, + ), + Content_API_Service::get_base_url() . '/related?limit=5&apikey=my-key&secret=my-secret', + ); + yield 'published_within value of 0' => array( + array( + 'apikey' => 'my-key', + 'sort' => 'score', + 'limit' => 5, + ), + Content_API_Service::get_base_url() . '/related?apikey=my-key&sort=score&limit=5&secret=my-secret', + ); + } +} diff --git a/tests/Integration/Services/SuggestionsAPI/Endpoints/EndpointSuggestBriefTest.php b/tests/Integration/Services/SuggestionsAPI/Endpoints/EndpointSuggestBriefTest.php new file mode 100644 index 0000000000..d6e8fb5688 --- /dev/null +++ b/tests/Integration/Services/SuggestionsAPI/Endpoints/EndpointSuggestBriefTest.php @@ -0,0 +1,132 @@ +get_suggestions_api()->get_endpoint( '/suggest-brief' ); + } + + /** + * Provides data for test_api_url(). + * + * @since 3.17.0 + * + * @covers \Parsely\Services\Base_Service_Endpoint::get_endpoint_url + * @uses \Parsely\Services\Suggestions_API\Endpoints\Endpoint_Suggest_Brief::get_endpoint + * @uses \Parsely\Services\Suggestions_API\Suggestions_API_Service::get_base_url + * + * @return \ArrayIterator + */ + public function data_api_url(): iterable { + yield 'Basic (Expected data)' => array( + array( + 'apikey' => 'my-key', + ), + Suggestions_API_Service::get_base_url() . '/suggest-brief?apikey=my-key', + ); + } + + /** + * Mocks a successful HTTP response to the Content Suggestion suggest-brief + * API endpoint. + * + * @since 3.17.0 + * + * @param string $response The response to mock. + * @param array $args The arguments passed to the HTTP request. + * @param string $url The URL of the HTTP request. + * @return array|false The mocked response. + * + * @phpstan-ignore-next-line + */ + public function mock_successful_suggest_brief_response( + string $response, + array $args, + string $url + ) { + if ( ! str_contains( $url, 'suggest-brief' ) ) { + return false; + } + + $response = array( + 'result' => array( + 'Lorem ipsum dolor sit amet, consectetur adipiscing elit.', + ), + ); + + return array( + 'headers' => array(), + 'cookies' => array(), + 'filename' => null, + 'response' => array( + 'code' => 200, + 'message' => 'OK', + ), + 'status_code' => 200, + 'success' => true, + 'body' => $this->wp_json_encode( $response ), + ); + } + + /** + * Tests getting meta description from the API with some generic content. + * + * @since 3.17.0 + * + * @covers \Parsely\Services\Suggestions_API\Endpoints\Endpoint_Suggest_Brief::get_suggestion + */ + public function test_get_suggestion(): void { + $title = 'Lorem Ipsum is a random title'; + $content = '

Lorem ipsum dolor sit amet, consectetur adipiscing elit.

'; + $persona = 'journalist'; + $style = 'neutral'; + + // Mock API result. + add_filter( 'pre_http_request', array( $this, 'mock_successful_suggest_brief_response' ), 10, 3 ); + + // Get the brief suggestion from the Suggestions API. + $brief = $this->get_suggestions_api()->get_brief_suggestions( + $title, + $content, + array( + 'persona' => $persona, + 'style' => $style, + 'max_items' => 1, + ) + ); + + self::assertIsArray( $brief ); + self::assertCount( 1, $brief ); + self::assertIsString( $brief[0] ); + self::assertEquals( 'Lorem ipsum dolor sit amet, consectetur adipiscing elit.', $brief[0] ); + + // Remove mock. + remove_filter( 'pre_http_request', array( $this, 'mock_successful_suggest_brief_response' ) ); + } +} diff --git a/tests/Integration/RemoteAPI/ContentSuggestions/SuggestHeadlineAPITest.php b/tests/Integration/Services/SuggestionsAPI/Endpoints/EndpointSuggestHeadlineTest.php similarity index 57% rename from tests/Integration/RemoteAPI/ContentSuggestions/SuggestHeadlineAPITest.php rename to tests/Integration/Services/SuggestionsAPI/Endpoints/EndpointSuggestHeadlineTest.php index d6d879f46f..ce2236cdec 100644 --- a/tests/Integration/RemoteAPI/ContentSuggestions/SuggestHeadlineAPITest.php +++ b/tests/Integration/Services/SuggestionsAPI/Endpoints/EndpointSuggestHeadlineTest.php @@ -1,48 +1,41 @@ get_suggestions_api()->get_endpoint( '/suggest-headline' ); } /** * Provides data for test_api_url(). * - * @since 3.12.0 + * @since 3.17.0 * * @return \ArrayIterator */ @@ -51,8 +44,7 @@ public function data_api_url(): iterable { array( 'apikey' => 'my-key', ), - Parsely::PUBLIC_SUGGESTIONS_API_BASE_URL . - '/suggest-headline?apikey=my-key', + Suggestions_API_Service::get_base_url() . '/suggest-headline?apikey=my-key', ); } @@ -60,7 +52,7 @@ public function data_api_url(): iterable { * Mocks a successful HTTP response to the Content Suggestion suggest-headline * API endpoint. * - * @since 3.12.0 + * @since 3.17.0 * * @param string $response The response to mock. * @param array $args The arguments passed to the HTTP request. @@ -103,14 +95,11 @@ public function mock_successful_suggest_headline_response( /** * Tests getting titles from the API with some generic content. * - * @since 3.12.0 + * @since 3.17.0 * - * @covers \Parsely\RemoteAPI\ContentSuggestions\Suggest_Headline_API::get_titles - * @uses \Parsely\RemoteAPI\ContentSuggestions\Suggest_Headline_API::__construct - * @uses \Parsely\RemoteAPI\ContentSuggestions\Suggest_Headline_API::get_titles - * @uses \Parsely\RemoteAPI\ContentSuggestions\Content_Suggestions_Base_API::__construct - * @uses \Parsely\RemoteAPI\ContentSuggestions\Content_Suggestions_Base_API::get_api_url - * @uses \Parsely\RemoteAPI\Remote_API_Base::__construct + * @covers \Parsely\Services\Suggestions_API\Suggestions_API_Service::get_title_suggestions + * @covers \Parsely\Services\Suggestions_API\Endpoints\Endpoint_Suggest_Headline::get_headlines + * @uses \Parsely\Services\Suggestions_API\Endpoints\Endpoint_Suggest_Headline::call */ public function test_get_titles(): void { $content = '

@@ -123,7 +112,12 @@ public function test_get_titles(): void { add_filter( 'pre_http_request', array( $this, 'mock_successful_suggest_headline_response' ), 10, 3 ); // Test getting three titles. - $titles = self::$suggest_headline_api->get_titles( $content, 3 ); + $titles = $this->get_suggestions_api()->get_title_suggestions( + $content, + array( + 'max_items' => 3, + ) + ); self::assertIsArray( $titles ); self::assertEquals( 3, count( $titles ) ); diff --git a/tests/Integration/RemoteAPI/ContentSuggestions/SuggestLinkedReferenceAPITest.php b/tests/Integration/Services/SuggestionsAPI/Endpoints/EndpointSuggestLinkedReferenceTest.php similarity index 57% rename from tests/Integration/RemoteAPI/ContentSuggestions/SuggestLinkedReferenceAPITest.php rename to tests/Integration/Services/SuggestionsAPI/Endpoints/EndpointSuggestLinkedReferenceTest.php index 21d2d3c8d2..de0f663fb2 100644 --- a/tests/Integration/RemoteAPI/ContentSuggestions/SuggestLinkedReferenceAPITest.php +++ b/tests/Integration/Services/SuggestionsAPI/Endpoints/EndpointSuggestLinkedReferenceTest.php @@ -1,50 +1,45 @@ get_suggestions_api()->get_endpoint( '/suggest-linked-reference' ); } /** * Provides data for test_api_url(). * - * @uses \Parsely\RemoteAPI\Base_Endpoint_Remote::validate_required_constraints - * @uses \Parsely\RemoteAPI\ContentSuggestions\Content_Suggestions_Base_API::get_api_url - - * @since 3.14.0 + * @since 3.17.0 + * * @return \ArrayIterator */ public function data_api_url(): iterable { @@ -52,8 +47,7 @@ public function data_api_url(): iterable { array( 'apikey' => 'my-key', ), - Parsely::PUBLIC_SUGGESTIONS_API_BASE_URL . - '/suggest-linked-reference?apikey=my-key', + Suggestions_API_Service::get_base_url() . '/suggest-linked-reference?apikey=my-key', ); } @@ -61,7 +55,7 @@ public function data_api_url(): iterable { * Mocks a successful HTTP response to the Content Suggestion suggest-links * API endpoint. * - * @since 3.14.0 + * @since 3.17.0 * * @param string $response The response to mock. * @param array $args The arguments passed to the HTTP request. @@ -119,20 +113,35 @@ public function mock_successful_suggest_links_response( /** * Tests getting smart links suggestions from the API. * - * @since 3.14.0 + * @since 3.17.0 * - * @covers \Parsely\RemoteAPI\ContentSuggestions\Suggest_Linked_Reference_API::get_links + * @covers \Parsely\Services\Suggestions_API\Suggestions_API_Service::get_smart_links + * @uses \Parsely\Models\Base_Model::__construct + * @uses Smart_Link::__construct + * @uses Smart_Link::generate_uid + * @uses Smart_Link::get_post_id_by_url + * @uses Smart_Link::set_href * @uses \Parsely\Parsely::api_secret_is_set * @uses \Parsely\Parsely::get_managed_credentials * @uses \Parsely\Parsely::get_options * @uses \Parsely\Parsely::get_site_id + * @uses \Parsely\Parsely::set_default_full_metadata_in_non_posts * @uses \Parsely\Parsely::set_default_track_as_values * @uses \Parsely\Parsely::site_id_is_set - * @uses \Parsely\RemoteAPI\Base_Endpoint_Remote::get_api_url - * @uses \Parsely\RemoteAPI\ContentSuggestions\Content_Suggestions_Base_API::get_request_options - * @uses \Parsely\RemoteAPI\ContentSuggestions\Content_Suggestions_Base_API::post_request - * @uses \Parsely\RemoteAPI\Base_Endpoint_Remote::validate_required_constraints - * @uses \Parsely\RemoteAPI\ContentSuggestions\Content_Suggestions_Base_API::get_api_url + * @uses \Parsely\Services\Base_API_Service::get_api_url + * @uses \Parsely\Services\Base_API_Service::get_endpoint + * @uses \Parsely\Services\Base_API_Service::get_parsely + * @uses Base_Service_Endpoint::get_endpoint_url + * @uses Base_Service_Endpoint::get_parsely + * @uses Base_Service_Endpoint::get_query_args + * @uses Base_Service_Endpoint::request + * @uses Base_Service_Endpoint::truncate_array_content + * @uses \Parsely\Services\Suggestions_API\Endpoints\Endpoint_Suggest_Linked_Reference::get_endpoint + * @uses \Parsely\Services\Suggestions_API\Endpoints\Endpoint_Suggest_Linked_Reference::get_links + * @uses \Parsely\Services\Suggestions_API\Endpoints\Suggestions_API_Base_Endpoint::get_query_args + * @uses \Parsely\Services\Suggestions_API\Endpoints\Suggestions_API_Base_Endpoint::get_request_options + * @uses \Parsely\Services\Suggestions_API\Endpoints\Suggestions_API_Base_Endpoint::process_response + * @uses Suggestions_API_Service::get_base_url */ public function test_get_links(): void { $content = '

@@ -145,7 +154,7 @@ public function test_get_links(): void { add_filter( 'pre_http_request', array( $this, 'mock_successful_suggest_links_response' ), 10, 3 ); // Test getting three titles. - $suggested_links = self::$suggest_linked_reference_api->get_links( $content ); + $suggested_links = $this->get_suggestions_api()->get_smart_links( $content ); self::assertIsArray( $suggested_links ); self::assertEquals( 3, count( $suggested_links ) ); diff --git a/tests/Integration/Services/SuggestionsAPI/Endpoints/SuggestionsAPIBaseEndpointTestCase.php b/tests/Integration/Services/SuggestionsAPI/Endpoints/SuggestionsAPIBaseEndpointTestCase.php new file mode 100644 index 0000000000..54086555d6 --- /dev/null +++ b/tests/Integration/Services/SuggestionsAPI/Endpoints/SuggestionsAPIBaseEndpointTestCase.php @@ -0,0 +1,131 @@ + $query Test query arguments. + * @param string $url Expected generated URL. + */ + public function test_api_url( array $query, string $url ): void { + // Get the endpoint object. + $endpoint = $this->get_service_endpoint(); + self::assertInstanceOf( Base_Service_Endpoint::class, $endpoint ); + + self::set_options( array( 'apikey' => 'my-key' ) ); + self::assertSame( $url, $endpoint->get_endpoint_url( $query ) ); + } + + /** + * Verifies the basic generation of the API headers. + * + * @since 3.17.0 + * + * @covers \Parsely\Services\Suggestions_API\Suggestions_API_Service::get_request_options + * @uses \Parsely\Parsely::api_secret_is_set() + * @uses \Parsely\Parsely::get_api_secret() + * @uses \Parsely\Parsely::get_managed_credentials() + * @uses \Parsely\Parsely::get_options() + */ + public function test_api_headers(): void { + // Assume self::$api_service is always an instance of Suggestions_API_Service. + // If there's any doubt, this should be handled in the test setup or constructor, + // not within individual tests. + self::assertInstanceOf( Suggestions_API_Service::class, self::$api_service ); + + // Get the endpoint object. + $endpoint = $this->get_service_endpoint(); + self::assertInstanceOf( Base_Service_Endpoint::class, $endpoint ); + + // Set options - this assumes set_options() does not produce side effects that affect other tests. + // Consider resetting any global or static state in a tearDown() method if necessary. + self::set_options( + array( + 'apikey' => 'my-key', + 'api_secret' => 'my-secret', + ) + ); + + // Call the protected method get_request_options() using reflection. + $get_request_options = self::get_method( 'get_request_options', $endpoint ); + $request_options = $get_request_options->invoke( $endpoint, 'GET' ); + + // Ensure that $request_options is an array and 'headers' key exists. + self::assertIsArray( $request_options ); + self::assertArrayHasKey( 'headers', $request_options ); + + $headers = $request_options['headers']; + self::assertIsArray( $headers ); // Ensures $headers is indeed an array. + + // Verify the Content-Type header is present and its value is application/json. + self::assertArrayHasKey( 'Content-Type', $headers ); + self::assertEquals( 'application/json; charset=utf-8', $headers['Content-Type'] ); + + // Verify the API key is present in the headers and its value matches the one set in the options. + self::assertArrayHasKey( 'X-APIKEY-SECRET', $headers ); + self::assertEquals( 'my-secret', $headers['X-APIKEY-SECRET'] ); + } +} diff --git a/tests/Integration/Services/SuggestionsAPI/SuggestionsApiServiceTestCase.php b/tests/Integration/Services/SuggestionsAPI/SuggestionsApiServiceTestCase.php new file mode 100644 index 0000000000..392791e851 --- /dev/null +++ b/tests/Integration/Services/SuggestionsAPI/SuggestionsApiServiceTestCase.php @@ -0,0 +1,115 @@ + + */ + private static $endpoints; + + /** + * Initializes all required values for the test. + * + * @since 3.17.0 + */ + public static function initialize(): void { + self::$api_service = new Suggestions_API_Service( new Parsely() ); + + // Get the endpoints from the protected $endpoints property using reflection. + $endpoints_prop = self::get_property( 'endpoints', self::$api_service ); + + /** @var array $endpoints */ + $endpoints = $endpoints_prop->getValue( self::$api_service ); + + // Store it. + self::$endpoints = $endpoints; + } + + /** + * Provides data for test_endpoint_is_registered(). + * + * Should return an array of arrays, each containing the endpoint name and + * class. + * + * @since 3.17.0 + * + * @return array> + */ + public function data_registered_endpoints(): iterable { + return array( + 'suggest-linked-reference' => array( + 'endpoint' => '/suggest-linked-reference', + 'class' => Endpoint_Suggest_Linked_Reference::class, + ), + 'suggest-brief' => array( + 'endpoint' => '/suggest-brief', + 'class' => Endpoint_Suggest_Brief::class, + ), + 'suggest-headline' => array( + 'endpoint' => '/suggest-headline', + 'class' => Endpoint_Suggest_Headline::class, + ), + ); + } + + /** + * Verifies that the number of registered endpoints is as expected. + * + * @since 3.17.0 + * + * @covers \Parsely\Services\Suggestions_API\Suggestions_API_Service::register_endpoint + */ + public function test_number_of_registered_endpoints_is_as_expected(): void { + self::assertSameSize( $this->data_registered_endpoints(), self::$endpoints ); + } + + /** + * Tests that the endpoint is registered and is an instance of the expected + * class. + * + * @since 3.17.0 + * + * @dataProvider data_registered_endpoints + * @covers \Parsely\Services\Base_API_Service::get_endpoint + * + * @param string $endpoint The endpoint name to check. + * @param class-string $class_name The endpoint class to check. + */ + public function test_endpoint_is_registered( string $endpoint, string $class_name ): void { + // Check that the endpoint exists and is an instance of the expected class. + self::assertArrayHasKey( $endpoint, self::$endpoints, "Endpoint $endpoint is not registered." ); + $endpoint = self::$api_service->get_endpoint( $endpoint ); + self::assertInstanceOf( $class_name, $endpoint ); + } +} diff --git a/tests/Traits/TestsReflection.php b/tests/Traits/TestsReflection.php index 5223b4b0e0..4c1484bbe8 100644 --- a/tests/Traits/TestsReflection.php +++ b/tests/Traits/TestsReflection.php @@ -12,14 +12,15 @@ use Parsely\Parsely; use ReflectionException; use ReflectionMethod; +use ReflectionProperty; trait TestsReflection { /** * Gets a method from a class. This should be used when trying to access a * private method for testing. * - * @param string $method_name Name of the method to get. - * @param class-string $class_name Name of the class the method is in. + * @param string $method_name Name of the method to get. + * @param class-string|object $class_name Name of the class the method is in. * @throws ReflectionException The method does not exist in the class. * @return ReflectionMethod */ @@ -30,6 +31,24 @@ public static function get_method( string $method_name, $class_name = Parsely::c return $method; } + /** + * Gets a property from a class. This should be used when trying to access a + * private property for testing. + * + * @since 3.17.0 + * + * @param string $property_name Name of the property to get. + * @param class-string|object $class_name Name of the class the property is in. + * @throws ReflectionException The property does not exist in the class. + * @return ReflectionProperty + */ + public static function get_property( string $property_name, $class_name = Parsely::class ) { + $property = ( new \ReflectionClass( $class_name ) )->getProperty( $property_name ); + $property->setAccessible( true ); + + return $property; + } + /** * Overrides the value of a private property on a given object. This is * useful when mocking the internals of a class. @@ -37,19 +56,39 @@ public static function get_method( string $method_name, $class_name = Parsely::c * Note that the property will no longer be private after setAccessible is * called. * - * @param class-string $class_name The fully qualified class name, including namespace. - * @param object $object_instance The object instance on which to set the value. - * @param string $property_name The name of the private property to override. - * @param mixed $value The value to set. + * @since 3.17.0 Changed the method signature. + * + * @param object $obj The object instance on which to set the value. + * @param string $property_name The name of the private property to override. + * @param mixed $value The value to set. + * + * @throws ReflectionException The property does not exist in the class. */ - public static function set_private_property( - string $class_name, - $object_instance, - string $property_name, - $value - ): void { - $property = ( new \ReflectionClass( $class_name ) )->getProperty( $property_name ); + public static function set_private_property( $obj, string $property_name, $value ): void { + $property = ( new \ReflectionClass( $obj ) )->getProperty( $property_name ); + $property->setAccessible( true ); + $property->setValue( $obj, $value ); + } + + /** + * Overrides the value of a protected property on a given object. This is + * useful when mocking the internals of a class. + * + * Note that the property will no longer be protected after setAccessible is + * called. + * + * @since 3.17.0 + * + * @param object $obj The object instance on which to set the value. + * @param string $property_name The name of the protected property to override. + * @param mixed $value The value to set. + * + * @throws ReflectionException The property does not exist in the class. + */ + public static function set_protected_property( $obj, string $property_name, $value ): void { + $reflection = new \ReflectionClass( $obj ); + $property = $reflection->getProperty( $property_name ); $property->setAccessible( true ); - $property->setValue( $object_instance, $value ); + $property->setValue( $obj, $value ); } } From f66f6cf0ed6f4af3cc9e01a6c163f60527ff5b03 Mon Sep 17 00:00:00 2001 From: Henrique Mouta Date: Fri, 25 Oct 2024 09:22:59 +0100 Subject: [PATCH 46/49] Rebuild assets --- build/content-helper/editor-sidebar.asset.php | 2 +- build/content-helper/editor-sidebar.js | 4 ++-- .../editor-sidebar/performance-stats/provider.ts | 8 ++++---- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/build/content-helper/editor-sidebar.asset.php b/build/content-helper/editor-sidebar.asset.php index 781f6179b9..6858edd207 100644 --- a/build/content-helper/editor-sidebar.asset.php +++ b/build/content-helper/editor-sidebar.asset.php @@ -1 +1 @@ - array('react', 'wp-api-fetch', 'wp-block-editor', 'wp-blocks', 'wp-components', 'wp-compose', 'wp-core-data', 'wp-data', 'wp-dom-ready', 'wp-editor', 'wp-element', 'wp-hooks', 'wp-i18n', 'wp-plugins', 'wp-primitives', 'wp-url'), 'version' => '4d5d7667e71460a1ce64'); + array('react', 'wp-api-fetch', 'wp-block-editor', 'wp-blocks', 'wp-components', 'wp-compose', 'wp-core-data', 'wp-data', 'wp-dom-ready', 'wp-editor', 'wp-element', 'wp-hooks', 'wp-i18n', 'wp-plugins', 'wp-primitives', 'wp-url'), 'version' => '6e668f9bfad0f3f02bf6'); diff --git a/build/content-helper/editor-sidebar.js b/build/content-helper/editor-sidebar.js index cf9fb35a33..57b55c601c 100644 --- a/build/content-helper/editor-sidebar.js +++ b/build/content-helper/editor-sidebar.js @@ -1,6 +1,6 @@ !function(){"use strict";var e={20:function(e,t,n){var r=n(609),i=Symbol.for("react.element"),s=Symbol.for("react.fragment"),o=Object.prototype.hasOwnProperty,a=r.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED.ReactCurrentOwner,l={key:!0,ref:!0,__self:!0,__source:!0};function c(e,t,n){var r,s={},c=null,u=null;for(r in void 0!==n&&(c=""+n),void 0!==t.key&&(c=""+t.key),void 0!==t.ref&&(u=t.ref),t)o.call(t,r)&&!l.hasOwnProperty(r)&&(s[r]=t[r]);if(e&&e.defaultProps)for(r in t=e.defaultProps)void 0===s[r]&&(s[r]=t[r]);return{$$typeof:i,type:e,key:c,ref:u,props:s,_owner:a.current}}t.Fragment=s,t.jsx=c,t.jsxs=c},848:function(e,t,n){e.exports=n(20)},609:function(e){e.exports=window.React}},t={};function n(r){var i=t[r];if(void 0!==i)return i.exports;var s=t[r]={exports:{}};return e[r](s,s.exports,n),s.exports}n.n=function(e){var t=e&&e.__esModule?function(){return e.default}:function(){return e};return n.d(t,{a:t}),t},n.d=function(e,t){for(var r in t)n.o(t,r)&&!n.o(e,r)&&Object.defineProperty(e,r,{enumerable:!0,get:t[r]})},n.o=function(e,t){return Object.prototype.hasOwnProperty.call(e,t)},function(){n.d({},{_:function(){return ur}});var e,t,r,i,s,o,a,l,c,u,p,d=n(848),f=window.wp.components,h=window.wp.data,v=window.wp.domReady,g=n.n(v);void 0!==window.wp&&(null!==(t=null===(e=window.wp.editor)||void 0===e?void 0:e.PluginDocumentSettingPanel)&&void 0!==t||(null!==(i=null===(r=window.wp.editPost)||void 0===r?void 0:r.PluginDocumentSettingPanel)&&void 0!==i||(null===(s=window.wp.editSite)||void 0===s||s.PluginDocumentSettingPanel)),p=null!==(a=null===(o=window.wp.editor)||void 0===o?void 0:o.PluginSidebar)&&void 0!==a?a:null!==(c=null===(l=window.wp.editPost)||void 0===l?void 0:l.PluginSidebar)&&void 0!==c?c:null===(u=window.wp.editSite)||void 0===u?void 0:u.PluginSidebar);var y,m,w,b=window.wp.element,_=window.wp.i18n,x=window.wp.primitives,k=(0,d.jsx)(x.SVG,{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 24 24",children:(0,d.jsx)(x.Path,{fillRule:"evenodd",d:"M11.25 5h1.5v15h-1.5V5zM6 10h1.5v10H6V10zm12 4h-1.5v6H18v-6z",clipRule:"evenodd"})}),S=window.wp.plugins,j=function(){function e(){this._tkq=[],this.isLoaded=!1,this.isEnabled=!1,"undefined"!=typeof wpParselyTracksTelemetry&&(this.isEnabled=!0,this.loadTrackingLibrary())}return e.getInstance=function(){return window.wpParselyTelemetryInstance||Object.defineProperty(window,"wpParselyTelemetryInstance",{value:new e,writable:!1,configurable:!1,enumerable:!1}),window.wpParselyTelemetryInstance},e.prototype.loadTrackingLibrary=function(){var e=this,t=document.createElement("script");t.async=!0,t.src="//stats.wp.com/w.js",t.onload=function(){e.isLoaded=!0,e._tkq=window._tkq||[]},document.head.appendChild(t)},e.trackEvent=function(t){return n=this,r=arguments,s=function(t,n){var r;return void 0===n&&(n={}),function(e,t){var n,r,i,s={label:0,sent:function(){if(1&i[0])throw i[1];return i[1]},trys:[],ops:[]},o=Object.create(("function"==typeof Iterator?Iterator:Object).prototype);return o.next=a(0),o.throw=a(1),o.return=a(2),"function"==typeof Symbol&&(o[Symbol.iterator]=function(){return this}),o;function a(a){return function(l){return function(a){if(n)throw new TypeError("Generator is already executing.");for(;o&&(o=0,a[0]&&(s=0)),s;)try{if(n=1,r&&(i=2&a[0]?r.return:a[0]?r.throw||((i=r.return)&&i.call(r),0):r.next)&&!(i=i.call(r,a[1])).done)return i;switch(r=0,i&&(a=[2&a[0],i.value]),a[0]){case 0:case 1:i=a;break;case 4:return s.label++,{value:a[1],done:!1};case 5:s.label++,r=a[1],a=[0];continue;case 7:a=s.ops.pop(),s.trys.pop();continue;default:if(!((i=(i=s.trys).length>0&&i[i.length-1])||6!==a[0]&&2!==a[0])){s=0;continue}if(3===a[0]&&(!i||a[1]>i[0]&&a[1]=1e4&&(clearInterval(s),n("Telemetry library not loaded"))}),100);else n("Telemetry not enabled")}))},e.prototype.trackEvent=function(t,n){var r;this.isLoaded?(0!==t.indexOf(e.TRACKS_PREFIX)&&(t=e.TRACKS_PREFIX+t),this.isEventNameValid(t)?(n=this.prepareProperties(n),null===(r=this._tkq)||void 0===r||r.push(["recordEvent",t,n])):console.error("Error tracking event: Invalid event name")):console.error("Error tracking event: Telemetry not loaded")},e.prototype.isTelemetryEnabled=function(){return this.isEnabled},e.prototype.isProprietyValid=function(t){return e.PROPERTY_REGEX.test(t)},e.prototype.isEventNameValid=function(t){return e.EVENT_NAME_REGEX.test(t)},e.prototype.prepareProperties=function(e){return(e=this.sanitizeProperties(e)).parsely_version=wpParselyTracksTelemetry.version,wpParselyTracksTelemetry.user&&(e._ut=wpParselyTracksTelemetry.user.type,e._ui=wpParselyTracksTelemetry.user.id),wpParselyTracksTelemetry.vipgo_env&&(e.vipgo_env=wpParselyTracksTelemetry.vipgo_env),this.sanitizeProperties(e)},e.prototype.sanitizeProperties=function(e){var t=this,n={};return Object.keys(e).forEach((function(r){t.isProprietyValid(r)&&(n[r]=e[r])})),n},e.TRACKS_PREFIX="wpparsely_",e.EVENT_NAME_REGEX=/^(([a-z0-9]+)_){2}([a-z0-9_]+)$/,e.PROPERTY_REGEX=/^[a-z_][a-z0-9_]*$/,e}(),P=(j.trackEvent,function(){return(0,d.jsx)(f.SVG,{"aria-hidden":"true",version:"1.1",viewBox:"0 0 15 15",width:"15",height:"15",xmlns:"http://www.w3.org/2000/svg",children:(0,d.jsx)(f.Path,{d:"M0 14.0025V11.0025L7.5 3.5025L10.5 6.5025L3 14.0025H0ZM12 5.0025L13.56 3.4425C14.15 2.8525 14.15 1.9025 13.56 1.3225L12.68 0.4425C12.09 -0.1475 11.14 -0.1475 10.56 0.4425L9 2.0025L12 5.0025Z"})})}),T=function(e){var t=e.size,n=void 0===t?24:t,r=e.className,i=void 0===r?"wp-parsely-icon":r;return(0,d.jsxs)(f.SVG,{className:i,height:n,viewBox:"0 0 60 65",width:n,xmlns:"http://www.w3.org/2000/svg",children:[(0,d.jsx)(f.Path,{fill:"#5ba745",d:"M23.72,51.53c0-.18,0-.34-.06-.52a13.11,13.11,0,0,0-2.1-5.53A14.74,14.74,0,0,0,19.12,43c-.27-.21-.5-.11-.51.22l-.24,3.42c0,.33-.38.35-.49,0l-1.5-4.8a1.4,1.4,0,0,0-.77-.78,23.91,23.91,0,0,0-3.1-.84c-1.38-.24-3.39-.39-3.39-.39-.34,0-.45.21-.25.49l2.06,3.76c.2.27,0,.54-.29.33l-4.51-3.6a3.68,3.68,0,0,0-2.86-.48c-1,.16-2.44.46-2.44.46a.68.68,0,0,0-.39.25.73.73,0,0,0-.14.45S.41,43,.54,44a3.63,3.63,0,0,0,1.25,2.62L6.48,50c.28.2.09.49-.23.37l-4.18-.94c-.32-.12-.5,0-.4.37,0,0,.69,1.89,1.31,3.16a24,24,0,0,0,1.66,2.74,1.34,1.34,0,0,0,1,.52l5,.13c.33,0,.41.38.1.48L7.51,58c-.31.1-.34.35-.07.55a14.29,14.29,0,0,0,3.05,1.66,13.09,13.09,0,0,0,5.9.5,25.13,25.13,0,0,0,4.34-1,9.55,9.55,0,0,1-.08-1.2,9.32,9.32,0,0,1,3.07-6.91"}),(0,d.jsx)(f.Path,{fill:"#5ba745",d:"M59.7,41.53a.73.73,0,0,0-.14-.45.68.68,0,0,0-.39-.25s-1.43-.3-2.44-.46a3.64,3.64,0,0,0-2.86.48l-4.51,3.6c-.26.21-.49-.06-.29-.33l2.06-3.76c.2-.28.09-.49-.25-.49,0,0-2,.15-3.39.39a23.91,23.91,0,0,0-3.1.84,1.4,1.4,0,0,0-.77.78l-1.5,4.8c-.11.32-.48.3-.49,0l-.24-3.42c0-.33-.24-.43-.51-.22a14.74,14.74,0,0,0-2.44,2.47A13.11,13.11,0,0,0,36.34,51c0,.18,0,.34-.06.52a9.26,9.26,0,0,1,3,8.1,24.1,24.1,0,0,0,4.34,1,13.09,13.09,0,0,0,5.9-.5,14.29,14.29,0,0,0,3.05-1.66c.27-.2.24-.45-.07-.55l-3.22-1.17c-.31-.1-.23-.47.1-.48l5-.13a1.38,1.38,0,0,0,1-.52A24.6,24.6,0,0,0,57,52.92c.61-1.27,1.31-3.16,1.31-3.16.1-.33-.08-.49-.4-.37l-4.18.94c-.32.12-.51-.17-.23-.37l4.69-3.34A3.63,3.63,0,0,0,59.46,44c.13-1,.24-2.47.24-2.47"}),(0,d.jsx)(f.Path,{fill:"#5ba745",d:"M46.5,25.61c0-.53-.35-.72-.8-.43l-4.86,2.66c-.45.28-.56-.27-.23-.69l4.66-6.23a2,2,0,0,0,.28-1.68,36.51,36.51,0,0,0-2.19-4.89,34,34,0,0,0-2.81-3.94c-.33-.41-.74-.35-.91.16l-2.28,5.68c-.16.5-.6.48-.59-.05l.28-8.93a2.54,2.54,0,0,0-.66-1.64S35,4.27,33.88,3.27,30.78.69,30.78.69a1.29,1.29,0,0,0-1.54,0s-1.88,1.49-3.12,2.59-2.48,2.35-2.48,2.35A2.5,2.5,0,0,0,23,7.27l.27,8.93c0,.53-.41.55-.58.05l-2.29-5.69c-.17-.5-.57-.56-.91-.14a35.77,35.77,0,0,0-3,4.2,35.55,35.55,0,0,0-2,4.62,2,2,0,0,0,.27,1.67l4.67,6.24c.33.42.23,1-.22.69l-4.87-2.66c-.45-.29-.82-.1-.82.43a18.6,18.6,0,0,0,.83,5.07,20.16,20.16,0,0,0,5.37,7.77c3.19,3,5.93,7.8,7.45,11.08A9.6,9.6,0,0,1,30,49.09a9.31,9.31,0,0,1,2.86.45c1.52-3.28,4.26-8.11,7.44-11.09a20.46,20.46,0,0,0,5.09-7,19,19,0,0,0,1.11-5.82"}),(0,d.jsx)(f.Path,{fill:"#5ba745",d:"M36.12,58.44A6.12,6.12,0,1,1,30,52.32a6.11,6.11,0,0,1,6.12,6.12"})]})},L=function(){return L=Object.assign||function(e){for(var t,n=1,r=arguments.length;n0?"".concat(i," ").concat(n):n)||this).hint=null,s.name=s.constructor.name,s.code=r;var o=[$.AccessToFeatureDisabled,$.ParselyApiForbidden,$.ParselyApiResponseContainsError,$.ParselyApiReturnedNoData,$.ParselyApiReturnedTooManyResults,$.PluginCredentialsNotSetMessageDetected,$.PluginSettingsApiSecretNotSet,$.PluginSettingsSiteIdNotSet,$.PostIsNotPublished,$.UnknownError,$.ParselySuggestionsApiAuthUnavailable,$.ParselySuggestionsApiNoAuthentication,$.ParselySuggestionsApiNoAuthorization,$.ParselySuggestionsApiNoData,$.ParselySuggestionsApiSchemaError];return s.retryFetch=!o.includes(s.code),Object.setPrototypeOf(s,t.prototype),s.code===$.AccessToFeatureDisabled?s.message=(0,_.__)("Access to this feature is disabled by the site's administration.","wp-parsely"):s.code===$.ParselySuggestionsApiNoAuthorization?s.message=(0,_.__)('This AI-powered feature is opt-in. To gain access, please submit a request here.',"wp-parsely"):s.code===$.ParselySuggestionsApiOpenAiError||s.code===$.ParselySuggestionsApiOpenAiUnavailable?s.message=(0,_.__)("The Parse.ly API returned an internal server error. Please retry with a different input, or try again later.","wp-parsely"):s.code===$.HttpRequestFailed&&s.message.includes("cURL error 28")?s.message=(0,_.__)("The Parse.ly API did not respond in a timely manner. Please try again later.","wp-parsely"):s.code===$.ParselySuggestionsApiSchemaError?s.message=(0,_.__)("The Parse.ly API returned a validation error. Please try again with different parameters.","wp-parsely"):s.code===$.ParselySuggestionsApiNoData?s.message=(0,_.__)("The Parse.ly API couldn't find any relevant data to fulfill the request. Please retry with a different input.","wp-parsely"):s.code===$.ParselySuggestionsApiOpenAiSchema?s.message=(0,_.__)("The Parse.ly API returned an incorrect response. Please try again later.","wp-parsely"):s.code===$.ParselySuggestionsApiAuthUnavailable&&(s.message=(0,_.__)("The Parse.ly API is currently unavailable. Please try again later.","wp-parsely")),s}return re(t,e),t.prototype.Message=function(e){return void 0===e&&(e=null),[$.PluginCredentialsNotSetMessageDetected,$.PluginSettingsSiteIdNotSet,$.PluginSettingsApiSecretNotSet].includes(this.code)?K(e):(this.code===$.FetchError&&(this.hint=this.Hint((0,_.__)("This error can sometimes be caused by ad-blockers or browser tracking protections. Please add this site to any applicable allow lists and try again.","wp-parsely"))),this.code!==$.ParselyApiForbidden&&this.code!==$.ParselySuggestionsApiNoAuthentication||(this.hint=this.Hint((0,_.__)("Please ensure that the Site ID and API Secret given in the plugin's settings are correct.","wp-parsely"))),this.code===$.HttpRequestFailed&&(this.hint=this.Hint((0,_.__)("The Parse.ly API cannot be reached. Please verify that you are online.","wp-parsely"))),(0,d.jsx)(W,{className:null==e?void 0:e.className,testId:"error",children:"

".concat(this.message,"

").concat(this.hint?this.hint:"")}))},t.prototype.Hint=function(e){return'

'.concat((0,_.__)("Hint:","wp-parsely")," ").concat(e,"

")},t.prototype.createErrorSnackbar=function(){//.test(this.message)||(0,h.dispatch)("core/notices").createNotice("error",this.message,{type:"snackbar"})},t}(Error),se=function(e){var t=e.isDetectingEnabled,n=e.onLinkChange,r=e.onLinkRemove,i=e.onLinkAdd,s=e.debounceValue,o=void 0===s?500:s,a=(0,h.useSelect)((function(e){return{blocks:(0,e("core/block-editor").getBlocks)()}}),[]).blocks,l=(0,b.useRef)(a),c=(0,b.useRef)(t);return(0,b.useEffect)((function(){var e=(0,z.debounce)((function(){for(var t=[],s=0;s0)return r(e.innerBlocks,t[s].innerBlocks);if(JSON.stringify(e)!==JSON.stringify(t[s])){var o=t[s],a=i.parseFromString(e.attributes.content||"","text/html"),l=i.parseFromString((null==o?void 0:o.attributes.content)||"","text/html"),c=Array.from(a.querySelectorAll("a[data-smartlink]")),u=Array.from(l.querySelectorAll("a[data-smartlink]")),p=c.filter((function(e){return!u.some((function(t){return t.dataset.smartlink===e.dataset.smartlink}))})),d=u.filter((function(e){return!c.some((function(t){return t.dataset.smartlink===e.dataset.smartlink}))})),f=c.filter((function(e){var t=u.find((function(t){return t.dataset.smartlink===e.dataset.smartlink}));return t&&t.outerHTML!==e.outerHTML}));(p.length>0||d.length>0||f.length>0)&&n.push({block:e,prevBlock:o,addedLinks:p,removedLinks:d,changedLinks:f})}}}))};return r(e,t),n}(a,l.current);o.length>0&&(o.forEach((function(e){e.changedLinks.length>0&&n&&n(e),e.addedLinks.length>0&&i&&i(e),e.removedLinks.length>0&&r&&r(e)})),l.current=a)}),o);return e(t),function(){e.cancel()}}),[a,o,t,i,n,r]),null},oe=function(e){var t=e.value,n=e.onChange,r=e.max,i=e.min,s=e.suffix,o=e.size,a=e.label,l=e.initialPosition,c=e.disabled,u=e.className;return(0,d.jsxs)("div",{className:"parsely-inputrange-control ".concat(u||""),children:[(0,d.jsx)(f.__experimentalHeading,{className:"parsely-inputrange-control__label",level:3,children:a}),(0,d.jsxs)("div",{className:"parsely-inputrange-control__controls",children:[(0,d.jsx)(f.__experimentalNumberControl,{disabled:c,value:t,suffix:(0,d.jsx)(f.__experimentalInputControlSuffixWrapper,{children:s}),size:null!=o?o:"__unstable-large",min:i,max:r,onChange:function(e){var t=parseInt(e,10);isNaN(t)||n(t)}}),(0,d.jsx)(f.RangeControl,{disabled:c,value:t,showTooltip:!1,initialPosition:l,onChange:function(e){n(e)},withInputField:!1,min:i,max:r})]})]})},ae=function(e,t,n,r){return new(n||(n=Promise))((function(i,s){function o(e){try{l(r.next(e))}catch(e){s(e)}}function a(e){try{l(r.throw(e))}catch(e){s(e)}}function l(e){var t;e.done?i(e.value):(t=e.value,t instanceof n?t:new n((function(e){e(t)}))).then(o,a)}l((r=r.apply(e,t||[])).next())}))},le=function(e,t){var n,r,i,s={label:0,sent:function(){if(1&i[0])throw i[1];return i[1]},trys:[],ops:[]},o=Object.create(("function"==typeof Iterator?Iterator:Object).prototype);return o.next=a(0),o.throw=a(1),o.return=a(2),"function"==typeof Symbol&&(o[Symbol.iterator]=function(){return this}),o;function a(a){return function(l){return function(a){if(n)throw new TypeError("Generator is already executing.");for(;o&&(o=0,a[0]&&(s=0)),s;)try{if(n=1,r&&(i=2&a[0]?r.return:a[0]?r.throw||((i=r.return)&&i.call(r),0):r.next)&&!(i=i.call(r,a[1])).done)return i;switch(r=0,i&&(a=[2&a[0],i.value]),a[0]){case 0:case 1:i=a;break;case 4:return s.label++,{value:a[1],done:!1};case 5:s.label++,r=a[1],a=[0];continue;case 7:a=s.ops.pop(),s.trys.pop();continue;default:if(!((i=(i=s.trys).length>0&&i[i.length-1])||6!==a[0]&&2!==a[0])){s=0;continue}if(3===a[0]&&(!i||a[1]>i[0]&&a[1]0&&i[i.length-1])||6!==a[0]&&2!==a[0])){s=0;continue}if(3===a[0]&&(!i||a[1]>i[0]&&a[1]0&&i[i.length-1])||6!==a[0]&&2!==a[0])){s=0;continue}if(3===a[0]&&(!i||a[1]>i[0]&&a[1]0&&i[i.length-1])||6!==a[0]&&2!==a[0])){s=0;continue}if(3===a[0]&&(!i||a[1]>i[0]&&a[1]0&&i[i.length-1])||6!==a[0]&&2!==a[0])){s=0;continue}if(3===a[0]&&(!i||a[1]>i[0]&&a[1]

","\n\x3c!-- /wp:paragraph --\x3e");t&&h((0,Q.parse)(n))}),[s]),(0,d.jsxs)("div",{className:"smart-linking-review-suggestion",children:[(0,d.jsx)(f.KeyboardShortcuts,{shortcuts:{left:o,right:a,up:o,down:a}}),(0,d.jsx)("div",{className:"review-suggestion-post-title",children:null===(t=s.post_data)||void 0===t?void 0:t.title}),(0,d.jsxs)("div",{className:"review-suggestion-preview",children:[!(null===(n=s.post_data)||void 0===n?void 0:n.is_first_paragraph)&&(0,d.jsx)($e,{topOrBottom:"top"}),(0,d.jsx)(Ze,{block:p[0],link:s,useOriginalBlock:!0}),!(null===(r=s.post_data)||void 0===r?void 0:r.is_last_paragraph)&&(0,d.jsx)($e,{topOrBottom:"bottom"})]}),(0,d.jsx)(f.__experimentalDivider,{}),(0,d.jsx)(We,{link:s}),(0,d.jsxs)("div",{className:"review-controls",children:[(0,d.jsx)(f.Tooltip,{shortcut:"←",text:(0,_.__)("Previous","wp-parsely"),children:(0,d.jsx)(f.Button,{disabled:!l,className:"wp-parsely-review-suggestion-previous",onClick:o,icon:He,children:(0,_.__)("Previous","wp-parsely")})}),(0,d.jsx)("div",{className:"reviews-controls-middle",children:(0,d.jsx)(f.Button,{target:"_blank",href:(null===(i=s.post_data)||void 0===i?void 0:i.edit_link)+"&smart-link="+s.uid,variant:"secondary",onClick:function(){j.trackEvent("smart_linking_open_in_editor_pressed",{type:"inbound",uid:s.uid})},children:(0,_.__)("Open in the Editor","wp-parsely")})}),(0,d.jsx)(f.Tooltip,{shortcut:"→",text:(0,_.__)("Next","wp-parsely"),children:(0,d.jsxs)(f.Button,{disabled:!c,onClick:a,className:"wp-parsely-review-suggestion-next",children:[(0,_.__)("Next","wp-parsely"),(0,d.jsx)(X,{icon:ze})]})})]})]})},Ye=function(e){var t=e.size,n=void 0===t?24:t,r=e.className,i=void 0===r?"wp-parsely-icon":r;return(0,d.jsxs)(f.SVG,{xmlns:"http://www.w3.org/2000/svg",className:i,width:n,height:n,viewBox:"0 0 24 24",fill:"none",children:[(0,d.jsx)(f.Path,{d:"M8.18983 5.90381L8.83642 7.54325L10.4758 8.18983L8.83642 8.8364L8.18983 10.4759L7.54324 8.8364L5.90381 8.18983L7.54324 7.54325L8.18983 5.90381Z"}),(0,d.jsx)(f.Path,{d:"M15.048 5.90381L15.9101 8.08972L18.0961 8.95186L15.9101 9.81397L15.048 11.9999L14.1859 9.81397L12 8.95186L14.1859 8.08972L15.048 5.90381Z"}),(0,d.jsx)(f.Path,{d:"M11.238 10.4761L12.3157 13.2085L15.048 14.2861L12.3157 15.3638L11.238 18.0962L10.1603 15.3638L7.42798 14.2861L10.1603 13.2085L11.238 10.4761Z"})]})},Je=function(e,t,n){if(n||2===arguments.length)for(var r,i=0,s=t.length;ii.bottom)&&(n.scrollTop=r.offsetTop-n.offsetTop)}}}}),[t,l]);var u=function(){var e=document.querySelector(".smart-linking-review-sidebar-tabs [data-active-item]"),t=null==e?void 0:e.nextElementSibling;t||(t=document.querySelector('.smart-linking-review-sidebar-tabs [role="tab"]')),t&&t.click()},p=(0,d.jsxs)("span",{className:"smart-linking-menu-label",children:[(0,_.__)("NEW","wp-parsely"),(0,d.jsx)(Ye,{})]}),h=[];n&&n.length>0&&h.push({name:"outbound",title:(0,_.__)("Outbound","wp-parsely")}),r&&r.length>0&&h.push({name:"inbound",title:(0,_.__)("Inbound","wp-parsely")});var v="outbound";return h=h.filter((function(e){return"outbound"===e.name&&r&&0===r.length&&(e.title=(0,_.__)("Outbound Smart Links","wp-parsely"),v="outbound"),"inbound"===e.name&&n&&0===n.length&&(e.title=(0,_.__)("Inbound Smart Links","wp-parsely"),v="inbound"),e})),(0,d.jsxs)("div",{className:"smart-linking-review-sidebar",ref:s,children:[(0,d.jsx)(f.KeyboardShortcuts,{shortcuts:{tab:function(){return u()},"shift+tab":function(){return u()}}}),(0,d.jsx)(f.TabPanel,{className:"smart-linking-review-sidebar-tabs",initialTabName:v,tabs:h,onSelect:function(e){var t,s;"outbound"===e&&n&&n.length>0&&i(n[0]),"inbound"===e&&r&&r.length>0&&i(r[0]),j.trackEvent("smart_linking_modal_tab_selected",{tab:e,total_inbound:null!==(t=null==r?void 0:r.length)&&void 0!==t?t:0,total_outbound:null!==(s=null==n?void 0:n.length)&&void 0!==s?s:0})},children:function(e){return(0,d.jsxs)(d.Fragment,{children:["outbound"===e.name&&(0,d.jsx)(d.Fragment,{children:n&&0!==n.length?n.map((function(e,n){return(0,d.jsxs)(f.MenuItem,{ref:function(e){o.current[n]=e},className:(null==t?void 0:t.uid)===e.uid?"is-selected":"",role:"menuitemradio",isSelected:(null==t?void 0:t.uid)===e.uid,onClick:function(){return i(e)},children:[(0,d.jsx)("span",{className:"smart-linking-menu-item",children:e.text}),!e.applied&&p]},e.uid)})):(0,d.jsxs)(d.Fragment,{children:[" ",(0,_.__)("No outbound links found.","wp-parsely")]})}),"inbound"===e.name&&(0,d.jsxs)(d.Fragment,{children:[(0,d.jsx)("div",{className:"review-sidebar-tip",children:(0,_.__)("This section shows external posts that link back to the current post.","wp-parsely")}),r&&0!==r.length?r.map((function(e,r){var s;return(0,d.jsx)(f.MenuItem,{ref:function(e){o.current[(n?n.length:0)+r]=e},className:(null==t?void 0:t.uid)===e.uid?"is-selected":"",role:"menuitemradio",isSelected:(null==t?void 0:t.uid)===e.uid,onClick:function(){return i(e)},children:(0,d.jsx)("span",{className:"smart-linking-menu-item",children:null===(s=e.post_data)||void 0===s?void 0:s.title})},e.uid)})):(0,d.jsxs)(d.Fragment,{children:[" ",(0,_.__)("No inbound links found.","wp-parsely")]})]})]})}})]})},Xe=(0,d.jsx)(x.SVG,{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 24 24",children:(0,d.jsx)(x.Path,{d:"M12 13.06l3.712 3.713 1.061-1.06L13.061 12l3.712-3.712-1.06-1.06L12 10.938 8.288 7.227l-1.061 1.06L10.939 12l-3.712 3.712 1.06 1.061L12 13.061z"})}),et=(0,d.jsx)(x.SVG,{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 24 24",children:(0,d.jsx)(x.Path,{d:"M16.7 7.1l-6.3 8.5-3.3-2.5-.9 1.2 4.5 3.4L17.9 8z"})}),tt=function(e){var t,n,r,i,s=null===(t=e.link.match)||void 0===t?void 0:t.blockId,o=(0,h.useSelect)((function(e){var t=e("core/block-editor"),n=t.getBlock,r=t.getBlockParents;return s?{block:n(s),parents:r(s).map((function(e){return n(e)})).filter((function(e){return void 0!==e}))}:{block:void 0,parents:[]}}),[s]),a=o.block,l=o.parents;return a?(0,d.jsxs)("div",{className:"review-suggestions-breadcrumbs",children:[l.map((function(e,t){var n;return(0,d.jsxs)("span",{children:[(0,d.jsx)("span",{className:"breadcrumbs-parent-block",children:null===(n=(0,Q.getBlockType)(e.name))||void 0===n?void 0:n.title}),(0,d.jsx)("span",{className:"breadcrumbs-parent-separator",children:" / "})]},t)})),(0,d.jsxs)("span",{className:"breadcrumbs-current-block",children:[(0,d.jsx)("span",{className:"breadcrumbs-current-block-type",children:null===(n=(0,Q.getBlockType)(a.name))||void 0===n?void 0:n.title}),(null===(i=null===(r=a.attributes)||void 0===r?void 0:r.metadata)||void 0===i?void 0:i.name)&&(0,d.jsx)("span",{className:"breadcrumbs-current-block-name",children:a.attributes.metadata.name})]})]}):(0,d.jsx)(d.Fragment,{})},nt=function(e){var t,n=e.link,r=(0,b.useState)(n.href),i=r[0],s=r[1],o=(0,b.useState)(null===(t=n.destination)||void 0===t?void 0:t.post_type),a=o[0],l=o[1],c=(0,b.useRef)(null),u=(0,h.useDispatch)(Te).updateSmartLink;return(0,b.useEffect)((function(){n.destination?l(n.destination.post_type):(l((0,_.__)("External","wp-parsely")),De.getInstance().getPostTypeByURL(n.href).then((function(e){e&&l(e.post_type),n.destination=e,u(n)})))}),[n,u]),(0,b.useEffect)((function(){var e=function(){if(c.current){var e=c.current.offsetWidth,t=Math.floor(e/8);s(function(e,t){var n=e.replace(/(^\w+:|^)\/\//,"").replace(/^www\./,"");if(!t||n.length<=t)return n;var r=n.split("/")[0],i=n.substring(r.length);t-=r.length;var s=Math.floor((t-3)/2),o=i.substring(0,s),a=i.substring(i.length-s);return"".concat(r).concat(o,"...").concat(a)}(n.href,t))}};return e(),window.addEventListener("resize",e),function(){window.removeEventListener("resize",e)}}),[n]),(0,d.jsx)(f.MenuItem,{ref:c,info:i,iconPosition:"left",icon:Ge,shortcut:a,className:"block-editor-link-control__search-item wp-parsely-link-suggestion-link-details",children:n.title})},rt=function(e){var t=e.link,n=e.onNext,r=e.onPrevious,i=e.onAccept,s=e.onReject,o=e.onRemove,a=e.onSelectInEditor,l=e.hasPrevious,c=e.hasNext;if(t&&void 0!==t.post_data)return(0,d.jsx)(Ke,{link:t,onNext:n,onPrevious:r,onAccept:i,onReject:s,onRemove:o,onSelectInEditor:a,hasPrevious:l,hasNext:c});if(!(null==t?void 0:t.match))return(0,d.jsx)(d.Fragment,{children:(0,_.__)("This Smart Link does not have any matches in the current content.","wp-parsely")});var u=t.match.blockId,p=(0,h.select)("core/block-editor").getBlock(u),v=t.applied;return p?(0,d.jsxs)("div",{className:"smart-linking-review-suggestion",children:[(0,d.jsx)(f.KeyboardShortcuts,{shortcuts:{left:r,right:n,up:r,down:n,a:function(){t&&!t.applied&&i()},r:function(){t&&(t.applied?o():s())}}}),(0,d.jsx)(tt,{link:t}),(0,d.jsx)("div",{className:"review-suggestion-preview",children:(0,d.jsx)(Ze,{block:p,link:t})}),(0,d.jsx)(f.__experimentalDivider,{}),(0,d.jsx)(nt,{link:t}),(0,d.jsxs)("div",{className:"review-controls",children:[(0,d.jsx)(f.Tooltip,{shortcut:"←",text:(0,_.__)("Previous","wp-parsely"),children:(0,d.jsx)(f.Button,{disabled:!l,className:"wp-parsely-review-suggestion-previous",onClick:r,icon:He,children:(0,_.__)("Previous","wp-parsely")})}),(0,d.jsxs)("div",{className:"reviews-controls-middle",children:[!v&&(0,d.jsxs)(d.Fragment,{children:[(0,d.jsx)(f.Tooltip,{shortcut:"R",text:(0,_.__)("Reject","wp-parsely"),children:(0,d.jsx)(f.Button,{className:"wp-parsely-review-suggestion-reject",icon:Xe,onClick:s,variant:"secondary",children:(0,_.__)("Reject","wp-parsely")})}),(0,d.jsx)(f.Tooltip,{shortcut:"A",text:(0,_.__)("Accept","wp-parsely"),children:(0,d.jsx)(f.Button,{className:"wp-parsely-review-suggestion-accept",icon:et,onClick:i,variant:"secondary",children:(0,_.__)("Accept","wp-parsely")})})]}),v&&(0,d.jsxs)(d.Fragment,{children:[(0,d.jsx)(f.Tooltip,{shortcut:"R",text:(0,_.__)("Remove","wp-parsely"),children:(0,d.jsx)(f.Button,{className:"wp-parsely-review-suggestion-reject",icon:Xe,onClick:o,variant:"secondary",children:(0,_.__)("Remove","wp-parsely")})}),(0,d.jsx)(f.Button,{className:"wp-parsely-review-suggestion-accept",onClick:a,variant:"secondary",children:(0,_.__)("Select in Editor","wp-parsely")})]})]}),(0,d.jsx)(f.Tooltip,{shortcut:"→",text:(0,_.__)("Next","wp-parsely"),children:(0,d.jsxs)(f.Button,{disabled:!c,onClick:n,className:"wp-parsely-review-suggestion-next",children:[(0,_.__)("Next","wp-parsely"),(0,d.jsx)(X,{icon:ze})]})})]})]}):(0,d.jsx)(d.Fragment,{children:(0,_.__)("No block is selected.","wp-parsely")})},it=function(e,t,n,r){return new(n||(n=Promise))((function(i,s){function o(e){try{l(r.next(e))}catch(e){s(e)}}function a(e){try{l(r.throw(e))}catch(e){s(e)}}function l(e){var t;e.done?i(e.value):(t=e.value,t instanceof n?t:new n((function(e){e(t)}))).then(o,a)}l((r=r.apply(e,t||[])).next())}))},st=function(e,t){var n,r,i,s={label:0,sent:function(){if(1&i[0])throw i[1];return i[1]},trys:[],ops:[]},o=Object.create(("function"==typeof Iterator?Iterator:Object).prototype);return o.next=a(0),o.throw=a(1),o.return=a(2),"function"==typeof Symbol&&(o[Symbol.iterator]=function(){return this}),o;function a(a){return function(l){return function(a){if(n)throw new TypeError("Generator is already executing.");for(;o&&(o=0,a[0]&&(s=0)),s;)try{if(n=1,r&&(i=2&a[0]?r.return:a[0]?r.throw||((i=r.return)&&i.call(r),0):r.next)&&!(i=i.call(r,a[1])).done)return i;switch(r=0,i&&(a=[2&a[0],i.value]),a[0]){case 0:case 1:i=a;break;case 4:return s.label++,{value:a[1],done:!1};case 5:s.label++,r=a[1],a=[0];continue;case 7:a=s.ops.pop(),s.trys.pop();continue;default:if(!((i=(i=s.trys).length>0&&i[i.length-1])||6!==a[0]&&2!==a[0])){s=0;continue}if(3===a[0]&&(!i||a[1]>i[0]&&a[1]0&&(a=o[0],(l=a.parentNode)&&(c=document.createTextNode(null!==(u=a.textContent)&&void 0!==u?u:""),l.replaceChild(c,a),te.updateBlockAttributes(n,{content:s.innerHTML}))),[4,E(t.uid)]):[2]):[2];case 1:return p.sent(),[2]}}))}))},C=(0,b.useCallback)((function(){c(!1),w().filter((function(e){return!e.applied})).length>0?o(!0):(ne.unlockPostAutosaving("smart-linking-review-modal"),t())}),[w,t]),A=function(e){o(!1),e?(c(!1),T().then((function(){C()}))):c(!0)},O=function(){if(ue(k)){var e=g.indexOf(k);if(!g[t=e+1])return;S(g[t])}else{var t;if(e=v.indexOf(k),!v[t=e+1])return;S(v[t])}},I=function(){if(ue(k)){var e=g.indexOf(k);if(!g[t=e-1])return;S(g[t])}else{var t;if(e=v.indexOf(k),!v[t=e-1])return;S(v[t])}};return(0,b.useEffect)((function(){l?ne.lockPostAutosaving("smart-linking-review-modal"):l&&0===p.length&&C()}),[l,t,p,C]),(0,b.useEffect)((function(){c(n)}),[n]),(0,d.jsxs)(d.Fragment,{children:[l&&(0,d.jsx)(f.Modal,{title:(0,_.__)("Review Smart Links","wp-parsely"),className:"wp-parsely-smart-linking-review-modal",onRequestClose:C,shouldCloseOnClickOutside:!1,shouldCloseOnEsc:!1,children:(0,d.jsxs)("div",{className:"smart-linking-modal-body",children:[(0,d.jsx)(Qe,{outboundLinks:v,inboundLinks:g,activeLink:k,setSelectedLink:S}),k&&(ue(k)?(0,d.jsx)(Ke,{link:k,onNext:O,onPrevious:I,hasNext:g.indexOf(k)0}):(0,d.jsx)(rt,{link:k,hasNext:m().indexOf(k)0,onNext:O,onPrevious:I,onAccept:function(){return it(void 0,void 0,void 0,(function(){var e,t;return st(this,(function(n){switch(n.label){case 0:return k.match?(r(k),[4,(i=k.match.blockId,s=k,it(void 0,void 0,void 0,(function(){var e,t;return st(this,(function(n){switch(n.label){case 0:return(e=document.createElement("a")).href=s.href,e.title=s.title,e.setAttribute("data-smartlink",s.uid),(t=(0,h.select)("core/block-editor").getBlock(i))?(fe(t,s,e),s.applied=!0,[4,L(s)]):[2];case 1:return n.sent(),[2]}}))})))]):[2];case 1:return n.sent(),j.trackEvent("smart_linking_link_accepted",{link:k.href,title:k.title,text:k.text,uid:k.uid}),0===y().length?(C(),[2]):(e=v.indexOf(k),v[t=e+1]?S(v[t]):S(v[0]),[2])}var i,s}))}))},onReject:function(){return it(void 0,void 0,void 0,(function(){var e,t;return st(this,(function(n){switch(n.label){case 0:return e=v.indexOf(k),v[t=e+1]?S(v[t]):v[0]?S(v[0]):C(),[4,E(k.uid)];case 1:return n.sent(),j.trackEvent("smart_linking_link_rejected",{link:k.href,title:k.title,text:k.text,uid:k.uid}),[2]}}))}))},onRemove:function(){return it(void 0,void 0,void 0,(function(){var e,t,n,r;return st(this,(function(i){switch(i.label){case 0:return k.match?(e=(0,h.select)("core/block-editor").getBlock(k.match.blockId))?(t=m(),n=t.indexOf(k),r=n-1,[4,N(e,k)]):[3,2]:[2];case 1:if(i.sent(),j.trackEvent("smart_linking_link_removed",{link:k.href,title:k.title,text:k.text,uid:k.uid}),0===(t=m()).length&&g.length>0)return S(g[0]),[2];if(0===t.length&&0===g.length)return C(),[2];if(t[r])return S(t[r]),[2];S(t[0]),i.label=2;case 2:return[2]}}))}))},onSelectInEditor:function(){if(k.match){var e=(0,h.select)("core/block-editor").getBlock(k.match.blockId);if(e){te.selectBlock(e.clientId);var t=document.querySelector('[data-block="'.concat(e.clientId,'"]'));t&&ke(t,k.uid),j.trackEvent("smart_linking_select_in_editor_pressed",{type:"outbound",uid:k.uid}),C()}}}}))]})}),s&&(0,d.jsxs)(f.Modal,{title:(0,_.__)("Review Smart Links","wp-parsely"),onRequestClose:function(){return A(!1)},className:"wp-parsely-smart-linking-close-dialog",children:[(0,_.__)("Are you sure you want to close? All un-accepted smart links will not be added.","wp-parsely"),(0,d.jsxs)("div",{className:"smart-linking-close-dialog-actions",children:[(0,d.jsx)(f.Button,{variant:"secondary",onClick:function(){return A(!1)},children:(0,_.__)("Go Back","wp-parsely")}),(0,d.jsx)(f.Button,{variant:"primary",onClick:function(){return A(!0)},children:(0,_.__)("Close","wp-parsely")})]})]})]})})),at=function(){return at=Object.assign||function(e){for(var t,n=1,r=arguments.length;n0&&i[i.length-1])||6!==a[0]&&2!==a[0])){s=0;continue}if(3===a[0]&&(!i||a[1]>i[0]&&a[1]0&&k("success",/* translators: %d: number of smart links applied */ /* translators: %d: number of smart links applied */ -(0,_.sprintf)((0,_.__)("%s smart links successfully applied.","wp-parsely"),g),{type:"snackbar"}):y(0)}),[w]),(0,b.useEffect)((function(){if(!(Object.keys(I).length>0)){var e={maxLinksPerPost:a.SmartLinking.MaxLinks};te(e)}}),[te,a]);var he=(0,h.useSelect)((function(e){var t=e("core/block-editor"),r=t.getSelectedBlock,i=t.getBlock,s=t.getBlocks,o=e("core/editor"),a=o.getEditedPostContent,l=o.getCurrentPostAttribute;return{allBlocks:s(),selectedBlock:n?i(n):r(),postContent:a(),postPermalink:l("link")}}),[n]),ve=he.allBlocks,me=he.selectedBlock,xe=he.postContent,ke=he.postPermalink,Se=function(e){return lt(void 0,void 0,void 0,(function(){var t,n,r,i,s;return ct(this,(function(o){switch(o.label){case 0:t=[],o.label=1;case 1:return o.trys.push([1,4,,9]),[4,re((n=E||!me)?_e.All:_e.Selected)];case 2:return o.sent(),a=ke.replace(/^https?:\/\//i,""),r=["http://"+a,"https://"+a],i=function(e){return e.map((function(e){return e.href}))}(F),r.push.apply(r,i),[4,De.getInstance().generateSmartLinks(me&&!n?(0,Q.getBlockContent)(me):xe,O,r)];case 3:return t=o.sent(),[3,9];case 4:if((s=o.sent()).code&&s.code===$.ParselyAborted)throw s.numRetries=3-e,s;return e>0&&s.retryFetch?(console.error(s),[4,ce(!0)]):[3,8];case 5:return o.sent(),[4,ue()];case 6:return o.sent(),[4,Se(e-1)];case 7:return[2,o.sent()];case 8:throw s;case 9:return[2,t]}var a}))}))},je=function(){for(var e=[],t=0;t[type="button"]').forEach((function(e){e.setAttribute("disabled","disabled")}))},Ne=function(){document.querySelectorAll('.edit-post-header__settings>[type="button"]').forEach((function(e){e.removeAttribute("disabled")})),ne.unlockPostSaving("wp-parsely-block-overlay")};return(0,d.jsxs)("div",{className:"wp-parsely-smart-linking",children:[(0,d.jsx)(se,{isDetectingEnabled:!L,onLinkRemove:function(e){!function(e){ae(this,void 0,void 0,(function(){var t,n,r;return le(this,(function(i){switch(i.label){case 0:return[4,we((0,Q.getBlockContent)(e),e.clientId)];case 1:return t=i.sent(),n=t.missingSmartLinks,r=t.didAnyFixes,n.forEach((function(e){(0,h.dispatch)(Te).removeSmartLink(e.uid)})),[2,r]}}))}))}(e.block)}}),(0,d.jsxs)(f.PanelRow,{className:t,children:[(0,d.jsxs)("div",{className:"smart-linking-text",children:[(0,_.__)("Automatically insert links to your most relevant, top performing content.","wp-parsely"),(0,d.jsxs)(f.Button,{href:"https://docs.parse.ly/plugin-content-helper/#h-smart-linking-beta",target:"_blank",variant:"link",children:[(0,_.__)("Learn more about Parse.ly AI","wp-parsely"),(0,d.jsx)(X,{icon:ee,size:18,className:"parsely-external-link-icon"})]})]}),C&&(0,d.jsx)(f.Notice,{status:"info",onRemove:function(){return Z(null)},className:"wp-parsely-content-helper-error",children:C.Message()}),w&&g>0&&(0,d.jsx)(f.Notice,{status:"success",onRemove:function(){return x(!1)},className:"wp-parsely-smart-linking-suggested-links",children:(0,_.sprintf)(/* translators: 1 - number of smart links generated */ /* translators: 1 - number of smart links generated */ +(0,_.sprintf)((0,_.__)("%s smart links successfully applied.","wp-parsely"),g),{type:"snackbar"}):y(0)}),[w]),(0,b.useEffect)((function(){if(!(Object.keys(I).length>0)){var e={maxLinksPerPost:a.SmartLinking.MaxLinks};te(e)}}),[te,a]);var he=(0,h.useSelect)((function(e){var t=e("core/block-editor"),r=t.getSelectedBlock,i=t.getBlock,s=t.getBlocks,o=e("core/editor"),a=o.getEditedPostContent,l=o.getCurrentPostAttribute;return{allBlocks:s(),selectedBlock:n?i(n):r(),postContent:a(),postPermalink:l("link")}}),[n]),ve=he.allBlocks,me=he.selectedBlock,xe=he.postContent,ke=he.postPermalink,Se=function(e){return lt(void 0,void 0,void 0,(function(){var t,n,r,i,s;return ct(this,(function(o){switch(o.label){case 0:t=[],o.label=1;case 1:return o.trys.push([1,4,,9]),[4,re((n=E||!me)?_e.All:_e.Selected)];case 2:return o.sent(),a=ke.replace(/^https?:\/\//i,""),r=["http://"+a,"https://"+a],i=function(e){return e.map((function(e){return e.href}))}(F),r.push.apply(r,i),[4,De.getInstance().generateSmartLinks(me&&!n?(0,Q.getBlockContent)(me):xe,O,r)];case 3:return t=o.sent(),[3,9];case 4:if((s=o.sent()).code&&s.code===$.ParselyAborted)throw s.numRetries=3-e,s;return e>0&&s.retryFetch?(console.error(s),[4,ce(!0)]):[3,8];case 5:return o.sent(),[4,ue()];case 6:return o.sent(),[4,Se(e-1)];case 7:return[2,o.sent()];case 8:throw s;case 9:return[2,t]}var a}))}))},je=function(){for(var e=[],t=0;t[type="button"]').forEach((function(e){e.setAttribute("disabled","disabled")}))},Ne=function(){document.querySelectorAll('.edit-post-header__settings>[type="button"]').forEach((function(e){e.removeAttribute("disabled")})),ne.unlockPostSaving("wp-parsely-block-overlay")};return(0,d.jsxs)("div",{className:"wp-parsely-smart-linking",children:[(0,d.jsx)(se,{isDetectingEnabled:!L,onLinkRemove:function(e){!function(e){ae(this,void 0,void 0,(function(){var t,n,r;return le(this,(function(i){switch(i.label){case 0:return[4,we((0,Q.getBlockContent)(e),e.clientId)];case 1:return t=i.sent(),n=t.missingSmartLinks,r=t.didAnyFixes,n.forEach((function(e){(0,h.dispatch)(Te).removeSmartLink(e.uid)})),[2,r]}}))}))}(e.block)}}),(0,d.jsxs)(f.PanelRow,{className:t,children:[(0,d.jsxs)("div",{className:"smart-linking-text",children:[(0,_.__)("Automatically insert links to your most relevant, top performing content.","wp-parsely"),(0,d.jsxs)(f.Button,{href:"https://docs.wpvip.com/parse-ly/wp-parsely-features/smart-linking/",target:"_blank",variant:"link",children:[(0,_.__)("Learn more about Parse.ly AI","wp-parsely"),(0,d.jsx)(X,{icon:ee,size:18,className:"parsely-external-link-icon"})]})]}),C&&(0,d.jsx)(f.Notice,{status:"info",onRemove:function(){return Z(null)},className:"wp-parsely-content-helper-error",children:C.Message()}),w&&g>0&&(0,d.jsx)(f.Notice,{status:"success",onRemove:function(){return x(!1)},className:"wp-parsely-smart-linking-suggested-links",children:(0,_.sprintf)(/* translators: 1 - number of smart links generated */ /* translators: 1 - number of smart links generated */ (0,_.__)("Successfully added %s smart links.","wp-parsely"),g>0?g:A.length)}),(0,d.jsx)(Ce,{disabled:T,selectedBlock:me,onSettingChange:function(e,t){var n;p({SmartLinking:at(at({},a.SmartLinking),(n={},n[e]=t,n))}),"MaxLinks"===e&&oe(t)}}),(0,d.jsx)("div",{className:"smart-linking-generate",children:(0,d.jsx)(f.Button,{onClick:function(){return lt(void 0,void 0,void 0,(function(){var e,t,n,r,s,o,a,l;return ct(this,(function(c){switch(c.label){case 0:return[4,q(!0)];case 1:return c.sent(),[4,pe()];case 2:return c.sent(),[4,Z(null)];case 3:return c.sent(),x(!1),j.trackEvent("smart_linking_generate_pressed",{is_full_content:E,selected_block:null!==(o=null==me?void 0:me.name)&&void 0!==o?o:"none",context:i}),[4,je(E?"all":null==me?void 0:me.clientId)];case 4:c.sent(),e=setTimeout((function(){var e;q(!1),j.trackEvent("smart_linking_generate_timeout",{is_full_content:E,selected_block:null!==(e=null==me?void 0:me.name)&&void 0!==e?e:"none",context:i}),Pe(E?"all":null==me?void 0:me.clientId)}),18e4),t=R,c.label=5;case 5:return c.trys.push([5,8,10,15]),[4,Se(3)];case 6:return n=c.sent(),[4,(u=n,lt(void 0,void 0,void 0,(function(){var e;return ct(this,(function(t){switch(t.label){case 0:return u=u.filter((function(e){return!F.some((function(t){return t.uid===e.uid&&t.applied}))})),e=ke.replace(/^https?:\/\//,"").replace(/\/+$/,""),u=(u=u.filter((function(t){return!t.href.includes(e)||(console.warn("PCH Smart Linking: Skipping self-reference link: ".concat(t.href)),!1)}))).filter((function(e){return!F.some((function(t){return t.href===e.href?(console.warn("PCH Smart Linking: Skipping duplicate link: ".concat(e.href)),!0):t.text===e.text&&t.offset!==e.offset&&(console.warn("PCH Smart Linking: Skipping duplicate link text: ".concat(e.text)),!0)}))})),u=(u=ge(E?ve:[me],u,{}).filter((function(e){return e.match}))).filter((function(e){if(!e.match)return!1;var t=e.match.blockLinkPosition,n=t+e.text.length;return!F.some((function(r){if(!r.match)return!1;if(e.match.blockId!==r.match.blockId)return!1;var i=r.match.blockLinkPosition,s=i+r.text.length;return t>=i&&n<=s}))})),[4,W(u)];case 1:return t.sent(),[2,u]}}))})))];case 7:if(0===c.sent().length)throw new ie((0,_.__)("No smart links were generated.","wp-parsely"),$.ParselySuggestionsApiNoData,"");return de(!0),[3,15];case 8:return r=c.sent(),s=new ie(null!==(a=r.message)&&void 0!==a?a:"An unknown error has occurred.",null!==(l=r.code)&&void 0!==l?l:$.UnknownError),r.code&&r.code===$.ParselyAborted&&(s.message=(0,_.sprintf)(/* translators: %d: number of retry attempts, %s: attempt plural */ /* translators: %d: number of retry attempts, %s: attempt plural */ (0,_.__)("The Smart Linking process was cancelled after %1$d %2$s.","wp-parsely"),r.numRetries,(0,_._n)("attempt","attempts",r.numRetries,"wp-parsely"))),console.error(r),[4,Z(s)];case 9:return c.sent(),s.createErrorSnackbar(),[3,15];case 10:return[4,q(!1)];case 11:return c.sent(),[4,re(t)];case 12:return c.sent(),[4,ce(!1)];case 13:return c.sent(),[4,Pe(E?"all":null==me?void 0:me.clientId)];case 14:return c.sent(),clearTimeout(e),[7];case 15:return[2]}var u}))}))},variant:"primary",isBusy:T,disabled:T,children:M?(0,_.sprintf)(/* translators: %1$d: number of retry attempts, %2$d: maximum number of retries */ /* translators: %1$d: number of retry attempts, %2$d: maximum number of retries */ (0,_.__)("Retrying… Attempt %1$d of %2$d","wp-parsely"),D,3):T?(0,_.__)("Generating Smart Links…","wp-parsely"):(0,_.__)("Add Smart Links","wp-parsely")})}),(G.length>0||V.length>0)&&(0,d.jsx)("div",{className:"smart-linking-manage",children:(0,d.jsx)(f.Button,{onClick:function(){return lt(void 0,void 0,void 0,(function(){var e,t;return ct(this,(function(n){switch(n.label){case 0:return[4,be()];case 1:return e=n.sent(),t=ye(),[4,W(t)];case 2:return n.sent(),de(!0),j.trackEvent("smart_linking_review_pressed",{num_smart_links:F.length,has_fixed_links:e,context:i}),[2]}}))}))},variant:"secondary",disabled:T,children:(0,_.__)("Review Smart Links","wp-parsely")})})]}),L&&(0,d.jsx)(ot,{isOpen:L,onAppliedLink:function(){y((function(e){return e+1}))},onClose:function(){x(!0),de(!1)}})]})},ft=function(){return ft=Object.assign||function(e){for(var t,n=1,r=arguments.length;n0&&i[i.length-1])||6!==a[0]&&2!==a[0])){s=0;continue}if(3===a[0]&&(!i||a[1]>i[0]&&a[1]0)&&(t(),e())}))}))]}))},new((n=void 0)||(n=Promise))((function(i,s){function o(e){try{l(r.next(e))}catch(e){s(e)}}function a(e){try{l(r.throw(e))}catch(e){s(e)}}function l(e){var t;e.done?i(e.value):(t=e.value,t instanceof n?t:new n((function(e){e(t)}))).then(o,a)}l((r=r.apply(e,t||[])).next())}));var e,t,n,r}().then((function(){var t=document.querySelector(".wp-block-post-content");ke(t,e)}))})))},jt=(0,d.jsx)(x.SVG,{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 24 24",children:(0,d.jsx)(x.Path,{d:"M7 11.5h10V13H7z"})}),Pt=(0,d.jsx)(x.SVG,{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 24 24",children:(0,d.jsx)(x.Path,{d:"M13 19h-2v-2h2v2zm0-6h-2v-2h2v2zm0-6h-2V5h2v2z"})}),Tt=function(e){var t=e.title,n=e.icon,r=e.subtitle,i=e.level,s=void 0===i?2:i,o=e.children,a=e.controls,l=e.onClick,c=e.isOpen,u=e.isLoading,p=e.dropdownChildren;return(0,d.jsxs)("div",{className:"performance-stat-panel",children:[(0,d.jsxs)(f.__experimentalHStack,{className:"panel-header level-"+s,children:[(0,d.jsx)(f.__experimentalHeading,{level:s,children:t}),r&&!c&&(0,d.jsx)("span",{className:"panel-subtitle",children:r}),a&&!p&&(0,d.jsx)(f.DropdownMenu,{icon:n,label:(0,_.__)("Settings","wp-parsely"),className:"panel-settings-button",controls:a}),p&&(0,d.jsx)(f.DropdownMenu,{icon:n,label:(0,_.__)("Settings","wp-parsely"),className:"panel-settings-button",children:p}),n&&!p&&!a&&(0,d.jsx)(f.Button,{icon:n,className:"panel-settings-button",isPressed:c,onClick:l})]}),(0,d.jsx)("div",{className:"panel-body",children:u?(0,d.jsx)("div",{className:"parsely-spinner-wrapper","data-testid":"parsely-spinner-wrapper",children:(0,d.jsx)(f.Spinner,{})}):o})]})};function Lt(e,t,n){void 0===t&&(t=1),void 0===n&&(n="");var r=parseInt(e.replace(/\D/g,""),10);if(r<1e3)return e;r<1e4&&(t=1);var i=r,s=r.toString(),o="",a=0;return Object.entries({1e3:"k","1,000,000":"M","1,000,000,000":"B","1,000,000,000,000":"T","1,000,000,000,000,000":"Q"}).forEach((function(e){var n=e[0],l=e[1],c=parseInt(n.replace(/\D/g,""),10);if(r>=c){var u=t;(i=r/c)%1>1/a&&(u=i>10?1:2),u=parseFloat(i.toFixed(2))===parseFloat(i.toFixed(0))?0:u,s=i.toFixed(u),o=l}a=c})),s+n+o}var Et=function(e){var t=e.data,n=e.isLoading,r=(0,b.useState)(m.Views),i=r[0],s=r[1],o=(0,b.useState)(!1),a=o[0],l=o[1];n||delete t.referrers.types.totals;var c=function(e){switch(e){case"social":return(0,_.__)("Social","wp-parsely");case"search":return(0,_.__)("Search","wp-parsely");case"other":return(0,_.__)("Other","wp-parsely");case"internal":return(0,_.__)("Internal","wp-parsely");case"direct":return(0,_.__)("Direct","wp-parsely")}return e},u=(0,_.sprintf)((0,_.__)("By %s","wp-parsely"),V(i)); @@ -25,4 +25,4 @@ message:(0,_.sprintf)((0,_.__)('by author "%1$s"',"wp-parsely"),n.value)};throw (0,_.__)("Top related posts by %1$s in the %2$s.","wp-parsely"),R.value,F(r,!0)):null!=E?E:""})}),P&&P.Message(),x&&(0,d.jsx)("div",{className:"related-posts-loading-message","data-testid":"parsely-related-posts-loading-message",children:(0,_.__)("Loading…","wp-parsely")}),!x&&!P&&0===A.length&&(0,d.jsx)("div",{className:"related-posts-empty","data-testid":"parsely-related-posts-empty",children:(0,_.__)("No related posts found.","wp-parsely")}),!x&&A.length>0&&(0,d.jsx)("div",{className:"related-posts-list",children:A.map((function(e){return(0,d.jsx)(hn,{metric:i,post:e,postContent:H},e.id)}))})]})]})]})},Pn=(0,d.jsx)(x.SVG,{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 24 24",children:(0,d.jsx)(x.Path,{d:"m19 7-3-3-8.5 8.5-1 4 4-1L19 7Zm-7 11.5H5V20h7v-1.5Z"})}),Tn=function(){return(0,d.jsx)(f.SVG,{xmlns:"http://www.w3.org/2000/svg",width:"18",height:"18",viewBox:"0 0 18 18",fill:"none",children:(0,d.jsx)(f.Path,{fillRule:"evenodd",clipRule:"evenodd",d:"M13.5034 7.91642L9 12.0104L4.49662 7.91642L5.25337 7.08398L8.99999 10.49L12.7466 7.08398L13.5034 7.91642Z",fill:"#1E1E1E"})})},Ln={journalist:{label:(0,_.__)("Journalist","wp-parsely")},editorialWriter:{label:(0,_.__)("Editorial Writer","wp-parsely")},investigativeReporter:{label:(0,_.__)("Investigative Reporter","wp-parsely")},techAnalyst:{label:(0,_.__)("Tech Analyst","wp-parsely")},businessAnalyst:{label:(0,_.__)("Business Analyst","wp-parsely")},culturalCommentator:{label:(0,_.__)("Cultural Commentator","wp-parsely")},scienceCorrespondent:{label:(0,_.__)("Science Correspondent","wp-parsely")},politicalAnalyst:{label:(0,_.__)("Political Analyst","wp-parsely")},healthWellnessAdvocate:{label:(0,_.__)("Health and Wellness Advocate","wp-parsely")},environmentalJournalist:{label:(0,_.__)("Environmental Journalist","wp-parsely")},custom:{label:(0,_.__)("Custom Persona","wp-parsely"),icon:Pn}},En=Object.keys(Ln),Nn=function(e){return"custom"===e||""===e?Ln.custom.label:Cn(e)?e:Ln[e].label},Cn=function(e){return!En.includes(e)||"custom"===e},An=function(e){var t=e.value,n=e.onChange,r=(0,b.useState)(""),i=r[0],s=r[1],o=(0,z.useDebounce)(n,500);return(0,d.jsx)("div",{className:"parsely-persona-selector-custom",children:(0,d.jsx)(f.TextControl,{value:i||t,placeholder:(0,_.__)("Enter a custom persona…","wp-parsely"),onChange:function(e){if(""===e)return n(""),void s("");e.length>32&&(e=e.slice(0,32)),o(e),s(e)}})})},On=function(e){var t=e.persona,n=e.value,r=void 0===n?(0,_.__)("Select a persona…","wp-parsely"):n,i=e.label,s=void 0===i?(0,_.__)("Persona","wp-parsely"):i,o=e.onChange,a=e.onDropdownChange,l=e.disabled,c=void 0!==l&&l,u=e.allowCustom,p=void 0!==u&&u;return(0,d.jsxs)(f.Disabled,{isDisabled:c,children:[s&&(0,d.jsx)("div",{className:"wp-parsely-dropdown-label",children:s}),(0,d.jsx)(f.DropdownMenu,{label:(0,_.__)("Persona","wp-parsely"),className:"parsely-persona-selector-dropdown"+(c?" is-disabled":""),popoverProps:{className:"wp-parsely-popover"},toggleProps:{children:(0,d.jsxs)(d.Fragment,{children:[(0,d.jsx)("div",{className:"parsely-persona-selector-label",children:Cn(t)?Ln.custom.label:r}),(0,d.jsx)(Tn,{})]})},children:function(e){var n=e.onClose;return(0,d.jsx)(f.MenuGroup,{label:(0,_.__)("Persona","wp-parsely"),children:(0,d.jsx)(d.Fragment,{children:En.map((function(e){if(!p&&"custom"===e)return null;var r=Ln[e],i=e===t||Cn(t)&&"custom"===e;return(0,d.jsxs)(f.MenuItem,{isSelected:i,className:i?"is-selected":"",role:"menuitemradio",onClick:function(){null==a||a(e),o(e),n(),"custom"===e&&setTimeout((function(){var e=document.querySelector(".parsely-persona-selector-custom input");e&&e.focus()}),0)},children:[r.icon&&(0,d.jsx)(X,{icon:r.icon}),r.label]},e)}))})})}}),p&&Cn(t)&&(0,d.jsx)(An,{onChange:function(e){o(""!==e?e:"custom")},value:"custom"===t?"":t})]})},In={neutral:{label:(0,_.__)("Neutral","wp-parsely")},formal:{label:(0,_.__)("Formal","wp-parsely")},humorous:{label:(0,_.__)("Humorous","wp-parsely")},confident:{label:(0,_.__)("Confident","wp-parsely")},provocative:{label:(0,_.__)("Provocative","wp-parsely")},serious:{label:(0,_.__)("Serious","wp-parsely")},inspirational:{label:(0,_.__)("Inspirational","wp-parsely")},skeptical:{label:(0,_.__)("Skeptical","wp-parsely")},conversational:{label:(0,_.__)("Conversational","wp-parsely")},analytical:{label:(0,_.__)("Analytical","wp-parsely")},custom:{label:(0,_.__)("Custom Tone","wp-parsely"),icon:Pn}},Rn=Object.keys(In),Bn=function(e){return"custom"===e||""===e?In.custom.label:Mn(e)?e:In[e].label},Mn=function(e){return!Rn.includes(e)||"custom"===e},Dn=function(e){var t=e.value,n=e.onChange,r=(0,b.useState)(""),i=r[0],s=r[1],o=(0,z.useDebounce)(n,500);return(0,d.jsx)("div",{className:"parsely-tone-selector-custom",children:(0,d.jsx)(f.TextControl,{value:i||t,placeholder:(0,_.__)("Enter a custom tone","wp-parsely"),onChange:function(e){if(""===e)return n(""),void s("");e.length>32&&(e=e.slice(0,32)),o(e),s(e)}})})},Fn=function(e){var t=e.tone,n=e.value,r=void 0===n?(0,_.__)("Select a tone","wp-parsely"):n,i=e.label,s=void 0===i?(0,_.__)("Tone","wp-parsely"):i,o=e.onChange,a=e.onDropdownChange,l=e.disabled,c=void 0!==l&&l,u=e.allowCustom,p=void 0!==u&&u;return(0,d.jsxs)(f.Disabled,{isDisabled:c,children:[(0,d.jsx)("div",{className:"wp-parsely-dropdown-label",children:s}),(0,d.jsx)(f.DropdownMenu,{label:(0,_.__)("Tone","wp-parsely"),className:"parsely-tone-selector-dropdown"+(c?" is-disabled":""),popoverProps:{className:"wp-parsely-popover"},toggleProps:{children:(0,d.jsxs)(d.Fragment,{children:[(0,d.jsx)("div",{className:"parsely-tone-selector-label",children:Mn(t)?In.custom.label:r}),(0,d.jsx)(Tn,{})]})},children:function(e){var n=e.onClose;return(0,d.jsx)(f.MenuGroup,{label:(0,_.__)("Select a tone","wp-parsely"),children:(0,d.jsx)(d.Fragment,{children:Rn.map((function(e){if(!p&&"custom"===e)return null;var r=In[e],i=e===t||Mn(t)&&"custom"===e;return(0,d.jsxs)(f.MenuItem,{isSelected:i,className:i?"is-selected":"",role:"menuitemradio",onClick:function(){null==a||a(e),o(e),n(),"custom"===e&&setTimeout((function(){var e=document.querySelector(".parsely-tone-selector-custom input");e&&e.focus()}),0)},children:[r.icon&&(0,d.jsx)(X,{icon:r.icon}),r.label]},e)}))})})}}),p&&Mn(t)&&(0,d.jsx)(Dn,{onChange:function(e){o(""!==e?e:"custom")},value:"custom"===t?"":t})]})},Vn=(0,d.jsx)(x.SVG,{width:"24",height:"24",viewBox:"0 0 24 24",xmlns:"http://www.w3.org/2000/svg",children:(0,d.jsx)(x.Path,{d:"M10.97 10.159a3.382 3.382 0 0 0-2.857.955l1.724 1.723-2.836 2.913L7 17h1.25l2.913-2.837 1.723 1.723a3.38 3.38 0 0 0 .606-.825c.33-.63.446-1.343.35-2.032L17 10.695 13.305 7l-2.334 3.159Z"})}),Gn=(0,d.jsx)(x.SVG,{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 24 24",children:(0,d.jsx)(x.Path,{d:"M18.3 11.7c-.6-.6-1.4-.9-2.3-.9H6.7l2.9-3.3-1.1-1-4.5 5L8.5 16l1-1-2.7-2.7H16c.5 0 .9.2 1.3.5 1 1 1 3.4 1 4.5v.3h1.5v-.2c0-1.5 0-4.3-1.5-5.7z"})}),Hn=(0,d.jsx)(x.SVG,{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 24 24",children:(0,d.jsx)(x.Path,{fillRule:"evenodd",clipRule:"evenodd",d:"M12 5.5A2.25 2.25 0 0 0 9.878 7h4.244A2.251 2.251 0 0 0 12 5.5ZM12 4a3.751 3.751 0 0 0-3.675 3H5v1.5h1.27l.818 8.997a2.75 2.75 0 0 0 2.739 2.501h4.347a2.75 2.75 0 0 0 2.738-2.5L17.73 8.5H19V7h-3.325A3.751 3.751 0 0 0 12 4Zm4.224 4.5H7.776l.806 8.861a1.25 1.25 0 0 0 1.245 1.137h4.347a1.25 1.25 0 0 0 1.245-1.137l.805-8.861Z"})}),zn=(0,d.jsx)(x.SVG,{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 24 24",children:(0,d.jsx)(x.Path,{d:"m21.5 9.1-6.6-6.6-4.2 5.6c-1.2-.1-2.4.1-3.6.7-.1 0-.1.1-.2.1-.5.3-.9.6-1.2.9l3.7 3.7-5.7 5.7v1.1h1.1l5.7-5.7 3.7 3.7c.4-.4.7-.8.9-1.2.1-.1.1-.2.2-.3.6-1.1.8-2.4.6-3.6l5.6-4.1zm-7.3 3.5.1.9c.1.9 0 1.8-.4 2.6l-6-6c.8-.4 1.7-.5 2.6-.4l.9.1L15 4.9 19.1 9l-4.9 3.6z"})}),Un=function(){return Un=Object.assign||function(e){for(var t,n=1,r=arguments.length;n0&&i[i.length-1])||6!==a[0]&&2!==a[0])){s=0;continue}if(3===a[0]&&(!i||a[1]>i[0]&&a[1]0&&i[i.length-1])||6!==a[0]&&2!==a[0])){s=0;continue}if(3===a[0]&&(!i||a[1]>i[0]&&a[1]0&&i[i.length-1])||6!==a[0]&&2!==a[0])){s=0;continue}if(3===a[0]&&(!i||a[1]>i[0]&&a[1]0?(0,d.jsx)("span",{className:"parsely-write-titles-text",children:(0,b.createInterpolateElement)( // translators: %1$s is the tone, %2$s is the persona. // translators: %1$s is the tone, %2$s is the persona. -(0,_.__)("We've generated a few titles based on the content of your post, written as a .","wp-parsely"),{tone:(0,d.jsx)("strong",{children:Bn(a)}),persona:(0,d.jsx)("strong",{children:Nn(u)})})}):(0,_.__)("Use Parse.ly AI to generate a title for your post.","wp-parsely"),(0,d.jsxs)(f.Button,{href:"https://docs.parse.ly/plugin-content-helper/#h-title-suggestions-beta",target:"_blank",variant:"link",children:[(0,_.__)("Learn more about Parse.ly AI","wp-parsely"),(0,d.jsx)(X,{icon:ee,size:18,className:"parsely-external-link-icon"})]})]}),i&&(0,d.jsx)(f.Notice,{className:"wp-parsely-content-helper-error",onRemove:function(){return s(void 0)},status:"info",children:i.Message()}),void 0!==k&&(0,d.jsx)(Jn,{title:k,type:fn.PostTitle,isOriginal:!0}),00&&(0,d.jsx)(Qn,{pinnedTitles:m,isOpen:!0}),y.length>0&&(0,d.jsx)(er,{suggestions:y,isOpen:!0,isLoading:g})]}),(0,d.jsx)(Xn,{isLoading:g,onPersonaChange:function(e){C("Persona",e),p(e)},onSettingChange:C,onToneChange:function(e){C("Tone",e),l(e)},persona:t.TitleSuggestions.Persona,tone:t.TitleSuggestions.Tone}),(0,d.jsx)("div",{className:"title-suggestions-generate",children:(0,d.jsxs)(f.Button,{variant:"primary",isBusy:g,disabled:g||"custom"===a||"custom"===u,onClick:function(){return ir(void 0,void 0,void 0,(function(){return sr(this,(function(e){switch(e.label){case 0:return s(void 0),!1!==g?[3,2]:(j.trackEvent("title_suggestions_generate_pressed",{request_more:y.length>0,total_titles:y.length,total_pinned:y.filter((function(e){return e.isPinned})).length,tone:a,persona:u}),[4,(t=fn.PostTitle,n=A,r=a,i=u,ir(void 0,void 0,void 0,(function(){var e,o,a;return sr(this,(function(l){switch(l.label){case 0:return[4,T(!0)];case 1:l.sent(),e=nr.getInstance(),l.label=2;case 2:return l.trys.push([2,5,,6]),[4,e.generateTitles(n,3,r,i)];case 3:return o=l.sent(),[4,P(t,o)];case 4:return l.sent(),[3,6];case 5:return a=l.sent(),s(a),P(t,[]),[3,6];case 6:return[4,T(!1)];case 7:return l.sent(),[2]}}))})))]);case 1:e.sent(),e.label=2;case 2:return[2]}var t,n,r,i}))}))},children:[g&&(0,_.__)("Generating Titles…","wp-parsely"),!g&&w.length>0&&(0,_.__)("Generate More","wp-parsely"),!g&&0===w.length&&(0,_.__)("Generate Titles","wp-parsely")]})})]})})},ar=function(){return ar=Object.assign||function(e){for(var t,n=1,r=arguments.length;n titles based on the content of your post, written as a .","wp-parsely"),{tone:(0,d.jsx)("strong",{children:Bn(a)}),persona:(0,d.jsx)("strong",{children:Nn(u)})})}):(0,_.__)("Use Parse.ly AI to generate a title for your post.","wp-parsely"),(0,d.jsxs)(f.Button,{href:"https://docs.wpvip.com/parse-ly/wp-parsely-features/title-suggestions/",target:"_blank",variant:"link",children:[(0,_.__)("Learn more about Parse.ly AI","wp-parsely"),(0,d.jsx)(X,{icon:ee,size:18,className:"parsely-external-link-icon"})]})]}),i&&(0,d.jsx)(f.Notice,{className:"wp-parsely-content-helper-error",onRemove:function(){return s(void 0)},status:"info",children:i.Message()}),void 0!==k&&(0,d.jsx)(Jn,{title:k,type:fn.PostTitle,isOriginal:!0}),00&&(0,d.jsx)(Qn,{pinnedTitles:m,isOpen:!0}),y.length>0&&(0,d.jsx)(er,{suggestions:y,isOpen:!0,isLoading:g})]}),(0,d.jsx)(Xn,{isLoading:g,onPersonaChange:function(e){C("Persona",e),p(e)},onSettingChange:C,onToneChange:function(e){C("Tone",e),l(e)},persona:t.TitleSuggestions.Persona,tone:t.TitleSuggestions.Tone}),(0,d.jsx)("div",{className:"title-suggestions-generate",children:(0,d.jsxs)(f.Button,{variant:"primary",isBusy:g,disabled:g||"custom"===a||"custom"===u,onClick:function(){return ir(void 0,void 0,void 0,(function(){return sr(this,(function(e){switch(e.label){case 0:return s(void 0),!1!==g?[3,2]:(j.trackEvent("title_suggestions_generate_pressed",{request_more:y.length>0,total_titles:y.length,total_pinned:y.filter((function(e){return e.isPinned})).length,tone:a,persona:u}),[4,(t=fn.PostTitle,n=A,r=a,i=u,ir(void 0,void 0,void 0,(function(){var e,o,a;return sr(this,(function(l){switch(l.label){case 0:return[4,T(!0)];case 1:l.sent(),e=nr.getInstance(),l.label=2;case 2:return l.trys.push([2,5,,6]),[4,e.generateTitles(n,3,r,i)];case 3:return o=l.sent(),[4,P(t,o)];case 4:return l.sent(),[3,6];case 5:return a=l.sent(),s(a),P(t,[]),[3,6];case 6:return[4,T(!1)];case 7:return l.sent(),[2]}}))})))]);case 1:e.sent(),e.label=2;case 2:return[2]}var t,n,r,i}))}))},children:[g&&(0,_.__)("Generating Titles…","wp-parsely"),!g&&w.length>0&&(0,_.__)("Generate More","wp-parsely"),!g&&0===w.length&&(0,_.__)("Generate Titles","wp-parsely")]})})]})})},ar=function(){return ar=Object.assign||function(e){for(var t,n=1,r=arguments.length;n} The post's referrer data. */ private async fetchReferrerDataFromWpEndpoint( - period: Period, postId: string, totalViews: string + period: Period, postId: string|number, totalViews: string ): Promise { const response = await this.fetch( { path: addQueryArgs( From 62115c582a8d64abc145fecf446db438e28c104f Mon Sep 17 00:00:00 2001 From: Alex Cicovic <23142906+acicovic@users.noreply.github.com> Date: Fri, 25 Oct 2024 19:25:51 +0300 Subject: [PATCH 47/49] Fix PHP 8.4 deprecation errors --- .../content-helper/trait-content-helper-feature.php | 2 +- src/services/content-api/class-content-api-service.php | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/rest-api/content-helper/trait-content-helper-feature.php b/src/rest-api/content-helper/trait-content-helper-feature.php index 383e7c2ce4..ab028ce885 100644 --- a/src/rest-api/content-helper/trait-content-helper-feature.php +++ b/src/rest-api/content-helper/trait-content-helper-feature.php @@ -56,7 +56,7 @@ protected function is_pch_feature_enabled_for_user(): bool { * @param WP_REST_Request|null $request The request object. * @return bool|WP_Error True if the endpoint is available. */ - public function is_available_to_current_user( WP_REST_Request $request = null ) { + public function is_available_to_current_user( ?WP_REST_Request $request = null ) { $can_use_feature = $this->is_pch_feature_enabled_for_user(); if ( ! $can_use_feature ) { diff --git a/src/services/content-api/class-content-api-service.php b/src/services/content-api/class-content-api-service.php index 83ceeb825c..7ca866c4fd 100644 --- a/src/services/content-api/class-content-api-service.php +++ b/src/services/content-api/class-content-api-service.php @@ -87,8 +87,8 @@ protected function register_endpoints(): void { */ public function get_post_details( string $url, - string $period_start = null, - string $period_end = null + ?string $period_start = null, + ?string $period_end = null ) { /** @var Endpoints\Endpoint_Analytics_Post_Details $endpoint */ $endpoint = $this->get_endpoint( '/analytics/post/detail' ); @@ -116,8 +116,8 @@ public function get_post_details( */ public function get_post_referrers( string $url, - string $period_start = null, - string $period_end = null + ?string $period_start = null, + ?string $period_end = null ) { /** @var Endpoints\Endpoint_Referrers_Post_Detail $endpoint */ $endpoint = $this->get_endpoint( '/referrers/post/detail' ); From 13e99872dc11811d6d8dddba942b11a375d6874d Mon Sep 17 00:00:00 2001 From: Henrique Mouta Date: Wed, 30 Oct 2024 11:38:53 +0000 Subject: [PATCH 48/49] Add missing const to the experimental phpunit config. --- phpunit-experimental.xml.dist | 1 + 1 file changed, 1 insertion(+) diff --git a/phpunit-experimental.xml.dist b/phpunit-experimental.xml.dist index 34eddfed83..c5503d831e 100644 --- a/phpunit-experimental.xml.dist +++ b/phpunit-experimental.xml.dist @@ -16,6 +16,7 @@ + From d7e63722ed02f63e10ff45b6925299637c98190b Mon Sep 17 00:00:00 2001 From: Alex Cicovic <23142906+acicovic@users.noreply.github.com> Date: Fri, 1 Nov 2024 07:42:03 +0200 Subject: [PATCH 49/49] integration-tests.yml: Improve testing logic --- .github/workflows/integration-tests.yml | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/.github/workflows/integration-tests.yml b/.github/workflows/integration-tests.yml index 1dd9181d97..e236f444b3 100644 --- a/.github/workflows/integration-tests.yml +++ b/.github/workflows/integration-tests.yml @@ -82,18 +82,18 @@ jobs: - name: Prepare environment for integration tests run: composer prepare-ci --no-interaction - - name: Run integration tests (single site) - if: ${{ matrix.php != 8.2 && ! matrix.experimental }} + - name: Run integration tests + if: ${{ !matrix.experimental && matrix.coverage == 'none' }} run: composer testwp --no-interaction - - name: Run integration tests (single site) experimental - if: ${{ matrix.experimental }} - run: composer testwp-experimental --no-interaction - - name: Run integration tests (multisite) - if: ${{ matrix.php != 8.2 && ! matrix.experimental }} + if: ${{ !matrix.experimental && matrix.coverage == 'none' }} run: composer testwp-ms --no-interaction - - name: Run integration tests (multisite site with code coverage) - if: ${{ matrix.php == 8.2 }} # To prevent unneeded test runs, use this version in the above if clauses. + - name: Run integration tests (experimental) + if: ${{ matrix.experimental }} + run: composer testwp-experimental --no-interaction + + - name: Run integration tests (multisite with code coverage) + if: ${{ matrix.coverage == 'pcov' }} run: composer coveragewp-ci --no-interaction