diff --git a/CHANGELOG.md b/CHANGELOG.md index e92dab212..33ebee49f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,14 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [3.6.2](https://github.com/Parsely/wp-parsely/compare/3.6.1...3.6.2) - 2023-02-13 + +### Fixed + +- Fix PHP 8 Incompatibilities ([#1362](https://github.com/Parsely/wp-parsely/pull/1362)) +- Improve checks on proxy endpoints +- Fix referral distribution in Performance Details panel ([#1382](https://github.com/Parsely/wp-parsely/pull/1382)) + ## [3.6.1](https://github.com/Parsely/wp-parsely/compare/3.6.0...3.6.1) - 2022-12-20 ### Fixed diff --git a/README.md b/README.md index 43053df7c..bde154ed8 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # Parse.ly -Stable tag: 3.6.1 +Stable tag: 3.6.2 Requires at least: 5.0 Tested up to: 6.1 Requires PHP: 7.1 diff --git a/package-lock.json b/package-lock.json index 0b07aeff5..d52e6261b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "wp-parsely", - "version": "3.6.1", + "version": "3.6.2", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "wp-parsely", - "version": "3.6.1", + "version": "3.6.2", "license": "GPL-2.0-or-later", "dependencies": { "@wordpress/dom-ready": "^3.9.0", diff --git a/package.json b/package.json index 5eec243f5..9d9f57893 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "wp-parsely", - "version": "3.6.1", + "version": "3.6.2", "private": true, "description": "The Parse.ly plugin facilitates real-time and historical analytics to your content through a platform designed and built for digital publishing.", "author": "parsely, hbbtstar, jblz, mikeyarce, GaryJ, parsely_mike, pauarge", diff --git a/src/Endpoints/class-analytics-post-detail-api-proxy.php b/src/Endpoints/class-analytics-post-detail-api-proxy.php index aff0fea00..1cf8e3d06 100644 --- a/src/Endpoints/class-analytics-post-detail-api-proxy.php +++ b/src/Endpoints/class-analytics-post-detail-api-proxy.php @@ -22,7 +22,7 @@ final class Analytics_Post_Detail_API_Proxy extends Base_API_Proxy { * Registers the endpoint's WP REST route. */ public function run(): void { - $this->register_endpoint( '/stats/post/detail' ); + $this->register_endpoint( '/stats/post/detail', 'publish_posts' ); } /** @@ -74,7 +74,7 @@ static function( stdClass $item ) use ( $stats_base_url ) { * @param float $time The time as a float number. * @return string The resulting formatted time duration. */ - private function get_duration( float $time ): string { + private static function get_duration( float $time ): string { $minutes = absint( $time ); $seconds = absint( round( fmod( $time, 1 ) * 60 ) ); @@ -85,14 +85,4 @@ private function get_duration( float $time ): string { return sprintf( '%2d:%02d', $minutes, $seconds ); } - - /** - * Determines if there are enough permissions to call the endpoint. - * - * @return bool - */ - public function permission_callback(): bool { - // Unauthenticated. - return true; - } } diff --git a/src/Endpoints/class-analytics-posts-api-proxy.php b/src/Endpoints/class-analytics-posts-api-proxy.php index c3ce3632d..61bc6e3d4 100644 --- a/src/Endpoints/class-analytics-posts-api-proxy.php +++ b/src/Endpoints/class-analytics-posts-api-proxy.php @@ -23,7 +23,7 @@ final class Analytics_Posts_API_Proxy extends Base_API_Proxy { * Registers the endpoint's WP REST route. */ public function run(): void { - $this->register_endpoint( '/stats/posts' ); + $this->register_endpoint( '/stats/posts', 'publish_posts' ); } /** @@ -64,14 +64,4 @@ static function( stdClass $item ) use ( $date_format, $stats_base_url ) { return $result; } - - /** - * Determines if there are enough permissions to call the endpoint. - * - * @return bool - */ - public function permission_callback(): bool { - // Unauthenticated. - return true; - } } diff --git a/src/Endpoints/class-base-api-proxy.php b/src/Endpoints/class-base-api-proxy.php index b783c4a4d..88b10531f 100644 --- a/src/Endpoints/class-base-api-proxy.php +++ b/src/Endpoints/class-base-api-proxy.php @@ -28,6 +28,15 @@ abstract class Base_API_Proxy { */ protected $parsely; + /** + * Capability of the user based on which we should allow access to endpoint. + * + * `null` should be used for all public endpoints. + * + * @var string|null + */ + protected $user_capability; + /** * Proxy object which does the actual calls to the Parse.ly API. * @@ -62,7 +71,19 @@ abstract public function get_items( WP_REST_Request $request ); * * @return bool */ - abstract public function permission_callback(): bool; + public function permission_callback(): bool { + // This endpoint does not require any capability checks. + if ( is_null( $this->user_capability ) ) { + return true; + } + + // The user has the required capability to access this endpoint. + if ( current_user_can( $this->user_capability ) ) { + return true; + } + + return false; + } /** * Constructor. @@ -79,9 +100,13 @@ public function __construct( Parsely $parsely, Proxy $proxy ) { /** * Registers the endpoint's WP REST route. * - * @param string $endpoint The endpoint's route (e.g. /stats/posts). + * @param string $endpoint The endpoint's route (e.g. /stats/posts). + * @param string|null $user_capability Capability of the user based on which we should allow access to endpoint. + * @param bool $show_in_index Show endpoint in /wp-json view if TRUE. */ - protected function register_endpoint( string $endpoint ): void { + protected function register_endpoint( string $endpoint, ?string $user_capability, $show_in_index = false ): void { + $this->user_capability = $user_capability; + $filter_key = trim( str_replace( '/', '_', $endpoint ), '_' ); if ( ! apply_filters( 'wp_parsely_enable_' . $filter_key . '_api_proxy', true ) ) { return; @@ -107,6 +132,7 @@ protected function register_endpoint( string $endpoint ): void { 'callback' => array( $this, 'get_items' ), 'permission_callback' => array( $this, 'permission_callback' ), 'args' => $get_items_args, + 'show_in_index' => $show_in_index, ), ); diff --git a/src/Endpoints/class-referrers-post-detail-api-proxy.php b/src/Endpoints/class-referrers-post-detail-api-proxy.php index 0dce878ba..bf11c0c8c 100644 --- a/src/Endpoints/class-referrers-post-detail-api-proxy.php +++ b/src/Endpoints/class-referrers-post-detail-api-proxy.php @@ -32,7 +32,7 @@ final class Referrers_Post_Detail_API_Proxy extends Base_API_Proxy { * @since 3.6.0 */ public function run(): void { - $this->register_endpoint( '/referrers/post/detail' ); + $this->register_endpoint( '/referrers/post/detail', 'publish_posts' ); } /** @@ -97,26 +97,37 @@ private function generate_referrer_types_data( array $response ): stdClass { // 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->views = 0; + $result->$key = (object) array( 'views' => 0 ); } // Set views and views totals. foreach ( $response as $referrer_data ) { - // Point by reference to the item to be processed, and set it to 0 - // when needed in order to avoid potential PHP warnings. - $current_type_views =& $result->{ $referrer_data->type }->views; - if ( ! isset( $current_type_views ) ) { - $current_type_views = 0; + /** + * Variable. + * + * @var int + */ + $current_views = isset( $referrer_data->metrics->referrers_views ) ? $referrer_data->metrics->referrers_views : 0; + $total_referrer_views += $current_views; + + /** + * Variable. + * + * @var string + */ + $current_key = isset( $referrer_data->type ) ? $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; } - - // Set the values. - $current_type_views += $referrer_data->metrics->referrers_views; - $total_referrer_views += $referrer_data->metrics->referrers_views; } // Add direct and total views to the object. $result->direct->views = $this->total_views - $total_referrer_views; - $result->totals->views = $this->total_views; + $result->totals = (object) array( 'views' => $this->total_views ); // Remove referrer types without views. foreach ( $referrer_type_keys as $key ) { @@ -167,11 +178,18 @@ private function generate_referrers_data( // Set views and views totals. $loop_count = $referrer_count > $limit ? $limit : $referrer_count; for ( $i = 0; $i < $loop_count; $i++ ) { - $data = $response[ $i ]; - $referrer_views = $data->metrics->referrers_views; - - $temp_views[ $data->name ] = $referrer_views; - $totals += $referrer_views; + $data = $response[ $i ]; + + /** + * Variable. + * + * @var int + */ + $referrer_views = isset( $data->metrics->referrers_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. @@ -187,9 +205,9 @@ private function generate_referrers_data( // Convert temporary array to result object and add totals. $result = new stdClass(); foreach ( $temp_views as $key => $value ) { - $result->$key->views = $value; + $result->$key = (object) array( 'views' => $value ); } - $result->totals->views = $totals; + $result->totals = (object) array( 'views' => $totals ); // Set percentages values and format numbers. foreach ( $result as $key => $value ) { @@ -227,16 +245,4 @@ private function get_i18n_percentage( int $number, int $total ) { return number_format_i18n( $number / $total * 100, 2 ); } - - /** - * Determines if there are enough permissions to call the endpoint. - * - * @since 3.6.0 - * - * @return bool - */ - public function permission_callback(): bool { - // Unauthenticated. - return true; - } } diff --git a/src/Endpoints/class-related-api-proxy.php b/src/Endpoints/class-related-api-proxy.php index 96fe05d2f..9833d4b96 100644 --- a/src/Endpoints/class-related-api-proxy.php +++ b/src/Endpoints/class-related-api-proxy.php @@ -22,7 +22,7 @@ final class Related_API_Proxy extends Base_API_Proxy { * Registers the endpoint's WP REST route. */ public function run(): void { - $this->register_endpoint( '/related' ); + $this->register_endpoint( '/related', null, true ); } /** @@ -57,14 +57,4 @@ static function( stdClass $item ) { return $result; } - - /** - * Determines if there are enough permissions to call the endpoint. - * - * @return bool - */ - public function permission_callback(): bool { - // Unauthenticated. - return true; - } } diff --git a/src/RemoteAPI/class-base-proxy.php b/src/RemoteAPI/class-base-proxy.php index 4446106e5..af8a8403a 100644 --- a/src/RemoteAPI/class-base-proxy.php +++ b/src/RemoteAPI/class-base-proxy.php @@ -43,6 +43,17 @@ public function __construct( Parsely $parsely ) { $this->parsely = $parsely; } + /** + * Gets Parse.ly API endpoint. + * + * @since 3.6.2 + * + * @return string + */ + public function get_endpoint(): string { + return static::ENDPOINT; + } + /** * Gets the URL for a particular Parse.ly API endpoint. * diff --git a/src/RemoteAPI/class-cached-proxy.php b/src/RemoteAPI/class-cached-proxy.php index f2e778189..ba2158ed8 100644 --- a/src/RemoteAPI/class-cached-proxy.php +++ b/src/RemoteAPI/class-cached-proxy.php @@ -50,8 +50,13 @@ public function __construct( Proxy $proxy, Cache $cache ) { * response is empty. */ public function get_items( array $query ) { - $cache_key = 'parsely_api_' . wp_hash( (string) wp_json_encode( $this->proxy ) ) . '_' . wp_hash( (string) wp_json_encode( $query ) ); - $items = $this->cache->get( $cache_key, self::CACHE_GROUP ); + $cache_key = ( + 'parsely_api_' . + wp_hash( $this->proxy->get_endpoint() ) . '_' . + wp_hash( (string) wp_json_encode( $query ) ) + ); + + $items = $this->cache->get( $cache_key, self::CACHE_GROUP ); if ( false === $items ) { $items = $this->proxy->get_items( $query ); diff --git a/tests/Integration/Endpoints/AnalyticsPostsProxyEndpointTest.php b/tests/Integration/Endpoints/AnalyticsPostsProxyEndpointTest.php index 520bd26c1..c37b0caab 100644 --- a/tests/Integration/Endpoints/AnalyticsPostsProxyEndpointTest.php +++ b/tests/Integration/Endpoints/AnalyticsPostsProxyEndpointTest.php @@ -68,6 +68,23 @@ public function test_do_not_register_route_when_proxy_is_disabled(): void { parent::test_do_not_register_route_when_proxy_is_disabled(); } + /** + * Verifies forbidden error when current user doesn't have proper capabilities. + * + * @covers \Parsely\Endpoints\Base_API_Proxy::permission_callback + * + * @uses \Parsely\Endpoints\Base_API_Proxy::register_endpoint + * @uses \Parsely\Endpoints\Analytics_Posts_API_Proxy::register_endpoint + */ + public function test_access_of_analytics_posts_endpoint_is_forbidden(): void { + $response = rest_get_server()->dispatch( new WP_REST_Request( 'GET', self::$route ) ); + $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 apikey is not populated @@ -85,6 +102,7 @@ public function test_do_not_register_route_when_proxy_is_disabled(): void { * @uses \Parsely\Endpoints\Base_API_Proxy::register_endpoint */ public function test_get_items_fails_without_apikey_set() { + $this->set_admin_user(); parent::test_get_items_fails_without_apikey_set(); } @@ -112,6 +130,7 @@ public function test_get_items_fails_without_apikey_set() { public function test_get_items() { TestCase::set_options( array( 'apikey' => 'example.com' ) ); TestCase::set_options( array( 'api_secret' => 'test' ) ); + $this->set_admin_user(); $dispatched = 0; $date_format = get_option( 'date_format' ); diff --git a/tests/Integration/RemoteAPITest.php b/tests/Integration/RemoteAPITest.php index 9a940906d..222f05a87 100644 --- a/tests/Integration/RemoteAPITest.php +++ b/tests/Integration/RemoteAPITest.php @@ -79,8 +79,9 @@ public function test_cached_proxy_returns_cached_value(): void { // If this method is called, that means our cache did not hit as // expected. $proxy_mock->expects( self::never() )->method( 'get_items' ); + $proxy_mock->method( 'get_endpoint' )->willReturn( self::$proxy->get_endpoint() ); // Passing call to non-mock method. - $cache_key = 'parsely_api_' . wp_hash( wp_json_encode( $proxy_mock ) ) . '_' . wp_hash( wp_json_encode( array() ) ); + $cache_key = 'parsely_api_' . wp_hash( self::$proxy->get_endpoint() ) . '_' . wp_hash( wp_json_encode( array() ) ); $object_cache = $this->createMock( Cache::class ); $object_cache->method( 'get' ) @@ -121,8 +122,9 @@ public function test_caching_decorator_returns_uncached_value(): void { // If this method is _NOT_ called, that means our cache did not miss as // expected. $proxy_mock->expects( self::once() )->method( 'get_items' ); + $proxy_mock->method( 'get_endpoint' )->willReturn( self::$proxy->get_endpoint() ); // Passing call to non-mock method. - $cache_key = 'parsely_api_' . wp_hash( wp_json_encode( $proxy_mock ) ) . '_' . wp_hash( wp_json_encode( array() ) ); + $cache_key = 'parsely_api_' . wp_hash( self::$proxy->get_endpoint() ) . '_' . wp_hash( wp_json_encode( array() ) ); $object_cache = $this->createMock( Cache::class ); $object_cache->method( 'get' ) diff --git a/tests/e2e/utils.js b/tests/e2e/utils.js index af6cf2ca5..767a27026 100644 --- a/tests/e2e/utils.js +++ b/tests/e2e/utils.js @@ -10,7 +10,7 @@ import { visitAdminPage, } from '@wordpress/e2e-test-utils'; -export const PLUGIN_VERSION = '3.6.1'; +export const PLUGIN_VERSION = '3.6.2'; export const waitForWpAdmin = () => page.waitForSelector( 'body.wp-admin' ); diff --git a/wp-parsely.php b/wp-parsely.php index a79c05ffa..f321a5aeb 100644 --- a/wp-parsely.php +++ b/wp-parsely.php @@ -11,7 +11,7 @@ * Plugin Name: Parse.ly * Plugin URI: https://www.parse.ly/help/integration/wordpress * Description: This plugin makes it a snap to add Parse.ly tracking code and metadata to your WordPress blog. - * Version: 3.6.1 + * Version: 3.6.2 * Author: Parse.ly * Author URI: https://www.parse.ly * Text Domain: wp-parsely @@ -56,7 +56,7 @@ return; } -const PARSELY_VERSION = '3.6.1'; +const PARSELY_VERSION = '3.6.2'; const PARSELY_FILE = __FILE__; require_once __DIR__ . '/src/class-parsely.php';