PluginProbe
Parse.ly / 3.24.2
Parse.ly v3.24.2
3.24.2 3.24.1 3.24.0 3.23.7 3.23.6 3.23.5 3.23.4 3.23.3 3.16.0 3.16.1 3.16.2 3.16.3 3.16.4 3.17.0 3.18.0 3.18.1 3.19.0 3.19.1 3.19.2 3.19.3 3.2.0 3.2.1 3.20.0 3.20.1 3.20.2 All 106 releases
wp-parsely / src / rest-api / stats / class-endpoint-posts.php

class-endpoint-posts.php in Parse.ly 3.24.2, at src/rest-api/stats/class-endpoint-posts.php

501 lines 14.0 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Stats API Endpoint: Posts
4 *
5 * @package Parsely
6 * @since 3.17.0
7 */
8
9 declare(strict_types=1);
10
11 namespace Parsely\REST_API\Stats;
12
13 use Parsely\REST_API\Base_Endpoint;
14 use Parsely\Services\Content_API\Content_API_Service;
15 use WP_Error;
16 use WP_REST_Request;
17 use WP_REST_Response;
18 use stdClass;
19
20 /**
21 * The Stats API Posts endpoint.
22 *
23 * Provides an endpoint for retrieving posts.
24 *
25 * It returns site-wide analytics, so it's available to any user with the
26 * default `publish_posts` capability, whoever authored the posts.
27 *
28 * @since 3.17.0
29 */
30 class Endpoint_Posts extends Base_Endpoint {
31 use Post_Data_Trait;
32
33 public const TOP_POSTS_DEFAULT_LIMIT = 5;
34 public const SORT_DEFAULT = 'views';
35 public const MAX_URLS = 500;
36
37 /**
38 * The metrics that can be sorted by.
39 *
40 * @since 3.17.0
41 *
42 * @var array<int, string>
43 * @see https://docs.parse.ly/api-available-metrics/
44 */
45 public const SORT_METRICS = array(
46 'views',
47 'mobile_views',
48 'tablet_views',
49 'desktop_views',
50 'visitors',
51 'visitors_new',
52 'visitors_returning',
53 'engaged_minutes',
54 'avg_engaged',
55 'avg_engaged_new',
56 'avg_engaged_returning',
57 'social_interactions',
58 'fb_interactions',
59 'tw_interactions',
60 'pi_interactions',
61 'social_referrals',
62 'fb_referrals',
63 'tw_referrals',
64 'pi_referrals',
65 'search_refs',
66 );
67
68 /**
69 * The Parse.ly Content API service.
70 *
71 * @since 3.17.0
72 *
73 * @var Content_API_Service
74 */
75 public $content_api;
76
77 /**
78 * Constructor.
79 *
80 * @since 3.17.0
81 *
82 * @param Stats_Controller $controller The stats controller.
83 */
84 public function __construct( Stats_Controller $controller ) {
85 parent::__construct( $controller );
86 $this->content_api = $this->parsely->get_content_api();
87 }
88
89 /**
90 * Returns the endpoint's name.
91 *
92 * @since 3.17.0
93 *
94 * @return string
95 */
96 public static function get_endpoint_name(): string {
97 return 'posts';
98 }
99
100 /**
101 * Registers the routes for the objects of the controller.
102 *
103 * @since 3.17.0
104 */
105 public function register_routes(): void {
106 /**
107 * GET /posts
108 * Retrieves posts for the given criteria.
109 */
110 $this->register_rest_route(
111 '/',
112 array( 'GET' ),
113 array( $this, 'get_posts' ),
114 array_merge(
115 array(
116 'use_wp_permalink' => array(
117 'description' => 'Whether to use the WordPress permalink.',
118 'type' => 'boolean',
119 'required' => false,
120 'default' => false,
121 ),
122 'period_start' => array(
123 'description' => 'The start of the period to query.',
124 'type' => 'string',
125 'required' => false,
126 ),
127 'period_end' => array(
128 'description' => 'The end of the period to query.',
129 'type' => 'string',
130 'required' => false,
131 ),
132 'pub_date_start' => array(
133 'description' => 'The start of the publication date range to query.',
134 'type' => 'string',
135 'required' => false,
136 ),
137 'pub_date_end' => array(
138 'description' => 'The end of the publication date range to query.',
139 'type' => 'string',
140 'required' => false,
141 ),
142 'limit' => array(
143 'description' => 'The number of posts to return.',
144 'type' => 'integer',
145 'required' => false,
146 'default' => self::TOP_POSTS_DEFAULT_LIMIT,
147 ),
148 'sort' => array(
149 'description' => 'The sort order of the posts.',
150 'type' => 'string',
151 'enum' => self::SORT_METRICS,
152 'default' => self::SORT_DEFAULT,
153 'required' => false,
154 ),
155 'page' => array(
156 'description' => 'The page to fetch.',
157 'type' => 'integer',
158 'required' => false,
159 'default' => 1,
160 ),
161 'author' => array(
162 'description' => 'The author to filter by.',
163 'type' => 'string',
164 'required' => false,
165 ),
166 'section' => array(
167 'description' => 'The section to filter by.',
168 'type' => 'string',
169 'required' => false,
170 ),
171 'tag' => array(
172 'description' => 'The tags to filter by (in comma-separated format).',
173 'type' => 'string',
174 'required' => false,
175 'validate_callback' => array( $this, 'validate_max_length_is_5' ),
176 'sanitize_callback' => array( $this, 'sanitize_string_to_array' ),
177 ),
178 'segment' => array(
179 'description' => 'The segment to filter by.',
180 'type' => 'string',
181 'required' => false,
182 ),
183 'urls' => array(
184 'description' => 'The URLs to fetch data for.',
185 'type' => 'array',
186 'items' => array( 'type' => 'string' ),
187 'maxItems' => self::MAX_URLS,
188 'sanitize_callback' => array( $this, 'sanitize_urls' ),
189 'validate_callback' => array( $this, 'validate_urls' ),
190 'required' => false,
191 ),
192 // Optional Campaign Parameters.
193 'campaign_id' => array(
194 'description' => 'The campaign to filter by.',
195 'type' => 'string',
196 'required' => false,
197 ),
198 'campaign_medium' => array(
199 'description' => 'The medium to filter by.',
200 'type' => 'string',
201 'required' => false,
202 ),
203 'campaign_source' => array(
204 'description' => 'The source to filter by.',
205 'type' => 'string',
206 'required' => false,
207 ),
208 'campaign_content' => array(
209 'description' => 'The content to filter by.',
210 'type' => 'string',
211 'required' => false,
212 ),
213 'campaign_term' => array(
214 'description' => 'The term to filter by.',
215 'type' => 'string',
216 'required' => false,
217 ),
218 ),
219 $this->get_itm_source_param_args()
220 )
221 );
222 }
223
224 /**
225 * Sanitizes a string to an array, splitting it by commas.
226 *
227 * @since 3.17.0
228 *
229 * @param string|array<string> $str The string to sanitize.
230 * @return array<string> The sanitized array.
231 */
232 public function sanitize_string_to_array( $str ): array {
233 if ( is_array( $str ) ) {
234 return $str;
235 }
236
237 return explode( ',', $str );
238 }
239
240 /**
241 * Sanitizes all the items of an array as URLs.
242 *
243 * @since 3.18.0
244 *
245 * @param array<string> $urls The array to sanitize.
246 * @return array<string> The sanitized array.
247 */
248 public function sanitize_urls( array $urls ): array {
249 return array_map( 'sanitize_url', $urls );
250 }
251
252 /**
253 * Validates if the provided array is a list of URLs.
254 *
255 * @since 3.19.0
256 * @since 3.24.2 Rejects non-arrays and more than `MAX_URLS` items.
257 *
258 * @param mixed $urls The raw parameter value, as validation runs before type checks.
259 * @return true|WP_Error
260 */
261 public function validate_urls( $urls ) {
262 if ( ! is_array( $urls ) ) {
263 return new WP_Error( 'invalid_param', __( 'The parameter must be a list of URLs.', 'wp-parsely' ) );
264 }
265
266 if ( count( $urls ) > self::MAX_URLS ) {
267 return new WP_Error(
268 'invalid_param',
269 sprintf(
270 /* translators: %d: The maximum number of items. */
271 __( 'The parameter must have at most %d items.', 'wp-parsely' ),
272 self::MAX_URLS
273 )
274 );
275 }
276
277 foreach ( $urls as $url ) {
278 if ( false === filter_var( $url, FILTER_VALIDATE_URL ) ) {
279 return new WP_Error( 'invalid_param', __( 'The parameter must be a list of URLs.', 'wp-parsely' ) );
280 }
281 }
282
283 return true;
284 }
285
286 /**
287 * Validates that the parameter has at most 5 items.
288 *
289 * @since 3.17.0
290 *
291 * @param string|array<string> $string_or_array The string or array to validate.
292 * @return true|WP_Error
293 */
294 public function validate_max_length_is_5( $string_or_array ) {
295 if ( is_string( $string_or_array ) ) {
296 $string_or_array = $this->sanitize_string_to_array( $string_or_array );
297 }
298
299 if ( count( $string_or_array ) > 5 ) {
300 return new WP_Error( 'invalid_param', __( 'The parameter must have at most 5 items.', 'wp-parsely' ) );
301 }
302
303 return true;
304 }
305
306 /**
307 * API Endpoint: GET /stats/posts
308 *
309 * Retrieves the posts with the given query parameters.
310 *
311 * @since 3.17.0
312 *
313 * @param WP_REST_Request $request The request.
314 * @return array<string, stdClass>|WP_Error|WP_REST_Response
315 */
316 public function get_posts( WP_REST_Request $request ) {
317 $params = $request->get_params();
318
319 // Setup the itm_source if it is provided.
320 $this->set_itm_source_from_request( $request );
321
322 // Determine if we should use the campaign parameters.
323 $use_campaign_params = false;
324 if ( isset( $params['campaign_id'] ) ||
325 isset( $params['campaign_medium'] ) ||
326 isset( $params['campaign_source'] ) ||
327 isset( $params['campaign_content'] ) ||
328 isset( $params['campaign_term'] ) ) {
329 $use_campaign_params = true;
330 }
331
332 // If we are using the WordPress permalink, generate a canonical URL for each URL.
333 if ( isset( $params['use_wp_permalink'] ) && $params['use_wp_permalink'] && isset( $params['urls'] ) && is_array( $params['urls'] ) ) {
334 $new_urls = array();
335
336 foreach ( $params['urls'] as $url ) {
337 // Generate a canonical URL for the WordPress permalink.
338 $new_urls[] = \Parsely\Parsely::get_canonical_url( $url );
339
340 // Also append the WordPress permalink to the new URLs as a fallback.
341 $new_urls[] = $url;
342 }
343
344 $params['urls'] = $new_urls;
345 }
346
347 // Build the request params.
348 $request_params = array(
349 'period_start' => $params['period_start'] ?? null,
350 'period_end' => $params['period_end'] ?? null,
351 'pub_date_start' => $params['pub_date_start'] ?? null,
352 'pub_date_end' => $params['pub_date_end'] ?? null,
353 'limit' => $params['limit'] ?? self::TOP_POSTS_DEFAULT_LIMIT,
354 'sort' => $params['sort'] ?? self::SORT_DEFAULT,
355 'page' => $params['page'] ?? 1,
356 'author' => $params['author'] ?? null,
357 'section' => $params['section'] ?? null,
358 'tag' => $params['tag'] ?? null,
359 'segment' => $params['segment'] ?? null,
360 'itm_source' => $params['itm_source'] ?? null,
361 'urls' => $params['urls'] ?? null,
362 );
363
364 /**
365 * The raw analytics data, received by the API.
366 *
367 * @var array<array<string, mixed>>|WP_Error $analytics_request
368 */
369 $analytics_request = $this->content_api->get_posts( $request_params );
370
371 if ( is_wp_error( $analytics_request ) ) {
372 return $analytics_request;
373 }
374
375 // If we are using campaign parameters, fetch the additional campaign data.
376 if ( $use_campaign_params ) {
377 $analytics_request = $this->fetch_campaign_data( $analytics_request, $params, $request_params );
378
379 if ( is_wp_error( $analytics_request ) ) {
380 return $analytics_request;
381 }
382 }
383
384 // Process the data.
385 $posts = array();
386
387 /**
388 * The analytics data object.
389 *
390 * @var array<string,array<mixed>> $analytics_request
391 */
392 foreach ( $analytics_request as $item ) {
393 $posts[] = $this->extract_post_data( $item );
394 }
395
396 $response_data = array(
397 'params' => $params,
398 'data' => $posts,
399 );
400
401 return new WP_REST_Response( $response_data, 200 );
402 }
403
404 /**
405 * Fetches the campaign data for the posts.
406 *
407 * @since 3.19.0
408 *
409 * @param array<int<0, max>, array<string, mixed>> $posts The posts.
410 * @param array<string, mixed> $params The parameters.
411 * @param array<string, mixed> $request_params The request parameters.
412 * @return array<int<0, max>, array<string, mixed>>|WP_Error The posts with the campaign parameters added.
413 */
414 public function fetch_campaign_data( array $posts, array $params, array $request_params = array() ) {
415 $campaign_params = array();
416
417 // Build the campaign params for the request.
418 if ( isset( $params['campaign_id'] ) ) {
419 $campaign_params['campaign_id'] = $params['campaign_id'];
420 }
421 if ( isset( $params['campaign_medium'] ) ) {
422 $campaign_params['campaign_medium'] = $params['campaign_medium'];
423 }
424 if ( isset( $params['campaign_source'] ) ) {
425 $campaign_params['campaign_source'] = $params['campaign_source'];
426 }
427 if ( isset( $params['campaign_content'] ) ) {
428 $campaign_params['campaign_content'] = $params['campaign_content'];
429 }
430 if ( isset( $params['campaign_term'] ) ) {
431 $campaign_params['campaign_term'] = $params['campaign_term'];
432 }
433
434 // Merge the campaign params with the request params.
435 /** @var array<string, array<string, mixed>> $request_params_with_campaign */
436 $request_params_with_campaign = array_merge( $campaign_params, $request_params );
437
438 $post_urls = array();
439 foreach ( $posts as $post ) {
440 if ( ! is_string( $post['link'] ) ) {
441 continue;
442 }
443
444 /**
445 * Post URL without ITM parameters.
446 *
447 * @var string $post_url
448 */
449 $post_url = \Parsely\Parsely::get_url_with_itm_source( $post['link'], null );
450 $post_urls[] = $post_url;
451 }
452
453 // Fill the URLs with the campaign params.
454 /** @var array<string, array<string, mixed>> $request_params_with_campaign */
455 $request_params_with_campaign['urls'] = $post_urls;
456
457 /**
458 * The raw analytics data, received by the API.
459 *
460 * @var array<array<string, mixed>>|WP_Error $campaign_request
461 */
462 $campaign_request = $this->content_api->get_posts( $request_params_with_campaign );
463
464 if ( is_wp_error( $campaign_request ) ) {
465 /** @var WP_Error $campaign_request */
466 return $campaign_request;
467 }
468
469 $posts_with_campaign_data = array();
470 foreach ( $posts as $post ) {
471 // Find the post by URL in the campaign request.
472 $campaign_post = array_filter(
473 $campaign_request,
474 function ( array $item ) use ( $post ) {
475 return $item['link'] === $post['link'];
476 }
477 );
478
479 if ( array() === $campaign_post ) {
480 // If there are no campaign metrics available, skip this one.
481 $posts_with_campaign_data[] = $post;
482 continue;
483 }
484
485 /** @var array<string, array<string, mixed>> $campaign_post */
486 $campaign_post = $campaign_post[0];
487
488 $post['campaign_metrics'] = array(
489 'views' => $campaign_post['metrics']['views'],
490 'visitors' => $campaign_post['metrics']['visitors'],
491 'recirculation_rate' => $campaign_post['metrics']['recirculation_rate'],
492 'avg_engaged' => $campaign_post['metrics']['avg_engaged'],
493 );
494
495 $posts_with_campaign_data[] = $post;
496 }
497
498 return $posts_with_campaign_data;
499 }
500 }
501