PluginProbe
Parse.ly / 3.19.2
Parse.ly v3.19.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 3.20.3 All 105 releases
wp-parsely / src / rest-api / stats / class-endpoint-posts.php

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

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