PluginProbe
Parse.ly / 3.24.1
Parse.ly v3.24.1
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.24.1, at src/rest-api/stats/class-endpoint-posts.php

479 lines 13.2 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's 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' => 'The author to filter by.',
159 'type' => 'string',
160 'required' => false,
161 ),
162 'section' => array(
163 'description' => 'The section to filter by.',
164 'type' => 'string',
165 'required' => false,
166 ),
167 'tag' => array(
168 'description' => 'The tags to filter by (in comma-separated format).',
169 'type' => 'string',
170 'required' => false,
171 'validate_callback' => array( $this, 'validate_max_length_is_5' ),
172 'sanitize_callback' => array( $this, 'sanitize_string_to_array' ),
173 ),
174 'segment' => array(
175 'description' => 'The segment to filter by.',
176 'type' => 'string',
177 'required' => false,
178 ),
179 'urls' => array(
180 'description' => 'The URLs to fetch data for.',
181 'type' => 'array',
182 'sanitize_callback' => array( $this, 'sanitize_urls' ),
183 'validate_callback' => array( $this, 'validate_urls' ),
184 'required' => false,
185 ),
186 // Optional Campaign Parameters.
187 'campaign_id' => array(
188 'description' => 'The campaign to filter by.',
189 'type' => 'string',
190 'required' => false,
191 ),
192 'campaign_medium' => array(
193 'description' => 'The medium to filter by.',
194 'type' => 'string',
195 'required' => false,
196 ),
197 'campaign_source' => array(
198 'description' => 'The source to filter by.',
199 'type' => 'string',
200 'required' => false,
201 ),
202 'campaign_content' => array(
203 'description' => 'The content to filter by.',
204 'type' => 'string',
205 'required' => false,
206 ),
207 'campaign_term' => array(
208 'description' => 'The term to filter by.',
209 'type' => 'string',
210 'required' => false,
211 ),
212 ),
213 $this->get_itm_source_param_args()
214 )
215 );
216 }
217
218 /**
219 * Sanitizes a string to an array, splitting it by commas.
220 *
221 * @since 3.17.0
222 *
223 * @param string|array<string> $str The string to sanitize.
224 * @return array<string> The sanitized array.
225 */
226 public function sanitize_string_to_array( $str ): array {
227 if ( is_array( $str ) ) {
228 return $str;
229 }
230
231 return explode( ',', $str );
232 }
233
234 /**
235 * Sanitizes all the items of an array as URLs.
236 *
237 * @since 3.18.0
238 *
239 * @param array<string> $urls The array to sanitize.
240 * @return array<string> The sanitized array.
241 */
242 public function sanitize_urls( array $urls ): array {
243 return array_map( 'sanitize_url', $urls );
244 }
245
246 /**
247 * Validates if the provided array is a list of URLs.
248 *
249 * @since 3.19.0
250 *
251 * @param array<string> $urls The array to validate.
252 * @return true|WP_Error
253 */
254 public function validate_urls( array $urls ) {
255 foreach ( $urls as $url ) {
256 if ( false === filter_var( $url, FILTER_VALIDATE_URL ) ) {
257 return new WP_Error( 'invalid_param', __( 'The parameter must be a list of URLs.', 'wp-parsely' ) );
258 }
259 }
260
261 return true;
262 }
263
264 /**
265 * Validates that the parameter has at most 5 items.
266 *
267 * @since 3.17.0
268 *
269 * @param string|array<string> $string_or_array The string or array to validate.
270 * @return true|WP_Error
271 */
272 public function validate_max_length_is_5( $string_or_array ) {
273 if ( is_string( $string_or_array ) ) {
274 $string_or_array = $this->sanitize_string_to_array( $string_or_array );
275 }
276
277 if ( count( $string_or_array ) > 5 ) {
278 return new WP_Error( 'invalid_param', __( 'The parameter must have at most 5 items.', 'wp-parsely' ) );
279 }
280
281 return true;
282 }
283
284 /**
285 * API Endpoint: GET /stats/posts
286 *
287 * Retrieves the posts with the given query parameters.
288 *
289 * @since 3.17.0
290 *
291 * @param WP_REST_Request $request The request.
292 * @return array<string, stdClass>|WP_Error|WP_REST_Response
293 */
294 public function get_posts( WP_REST_Request $request ) {
295 $params = $request->get_params();
296
297 // Setup the itm_source if it is provided.
298 $this->set_itm_source_from_request( $request );
299
300 // Determine if we should use the campaign parameters.
301 $use_campaign_params = false;
302 if ( isset( $params['campaign_id'] ) ||
303 isset( $params['campaign_medium'] ) ||
304 isset( $params['campaign_source'] ) ||
305 isset( $params['campaign_content'] ) ||
306 isset( $params['campaign_term'] ) ) {
307 $use_campaign_params = true;
308 }
309
310 // If we are using the WordPress permalink, generate a canonical URL for each URL.
311 if ( isset( $params['use_wp_permalink'] ) && $params['use_wp_permalink'] && isset( $params['urls'] ) && is_array( $params['urls'] ) ) {
312 $new_urls = array();
313
314 foreach ( $params['urls'] as $url ) {
315 // Generate a canonical URL for the WordPress permalink.
316 $new_urls[] = \Parsely\Parsely::get_canonical_url( $url );
317
318 // Also append the WordPress permalink to the new URLs as a fallback.
319 $new_urls[] = $url;
320 }
321
322 $params['urls'] = $new_urls;
323 }
324
325 // Build the request params.
326 $request_params = array(
327 'period_start' => $params['period_start'] ?? null,
328 'period_end' => $params['period_end'] ?? null,
329 'pub_date_start' => $params['pub_date_start'] ?? null,
330 'pub_date_end' => $params['pub_date_end'] ?? null,
331 'limit' => $params['limit'] ?? self::TOP_POSTS_DEFAULT_LIMIT,
332 'sort' => $params['sort'] ?? self::SORT_DEFAULT,
333 'page' => $params['page'] ?? 1,
334 'author' => $params['author'] ?? null,
335 'section' => $params['section'] ?? null,
336 'tag' => $params['tag'] ?? null,
337 'segment' => $params['segment'] ?? null,
338 'itm_source' => $params['itm_source'] ?? null,
339 'urls' => $params['urls'] ?? null,
340 );
341
342 /**
343 * The raw analytics data, received by the API.
344 *
345 * @var array<array<string, mixed>>|WP_Error $analytics_request
346 */
347 $analytics_request = $this->content_api->get_posts( $request_params );
348
349 if ( is_wp_error( $analytics_request ) ) {
350 return $analytics_request;
351 }
352
353 // If we are using campaign parameters, fetch the additional campaign data.
354 if ( $use_campaign_params ) {
355 $analytics_request = $this->fetch_campaign_data( $analytics_request, $params, $request_params );
356
357 if ( is_wp_error( $analytics_request ) ) {
358 return $analytics_request;
359 }
360 }
361
362 // Process the data.
363 $posts = array();
364
365 /**
366 * The analytics data object.
367 *
368 * @var array<string,array<mixed>> $analytics_request
369 */
370 foreach ( $analytics_request as $item ) {
371 $posts[] = $this->extract_post_data( $item );
372 }
373
374 $response_data = array(
375 'params' => $params,
376 'data' => $posts,
377 );
378
379 return new WP_REST_Response( $response_data, 200 );
380 }
381
382 /**
383 * Fetches the campaign data for the posts.
384 *
385 * @since 3.19.0
386 *
387 * @param array<int<0, max>, array<string, mixed>> $posts The posts.
388 * @param array<string, mixed> $params The parameters.
389 * @param array<string, mixed> $request_params The request parameters.
390 * @return array<int<0, max>, array<string, mixed>>|WP_Error The posts with the campaign parameters added.
391 */
392 public function fetch_campaign_data( array $posts, array $params, array $request_params = array() ) {
393 $campaign_params = array();
394
395 // Build the campaign params for the request.
396 if ( isset( $params['campaign_id'] ) ) {
397 $campaign_params['campaign_id'] = $params['campaign_id'];
398 }
399 if ( isset( $params['campaign_medium'] ) ) {
400 $campaign_params['campaign_medium'] = $params['campaign_medium'];
401 }
402 if ( isset( $params['campaign_source'] ) ) {
403 $campaign_params['campaign_source'] = $params['campaign_source'];
404 }
405 if ( isset( $params['campaign_content'] ) ) {
406 $campaign_params['campaign_content'] = $params['campaign_content'];
407 }
408 if ( isset( $params['campaign_term'] ) ) {
409 $campaign_params['campaign_term'] = $params['campaign_term'];
410 }
411
412 // Merge the campaign params with the request params.
413 /** @var array<string, array<string, mixed>> $request_params_with_campaign */
414 $request_params_with_campaign = array_merge( $campaign_params, $request_params );
415
416 $post_urls = array();
417 foreach ( $posts as $post ) {
418 if ( ! is_string( $post['link'] ) ) {
419 continue;
420 }
421
422 /**
423 * Post URL without ITM parameters.
424 *
425 * @var string $post_url
426 */
427 $post_url = \Parsely\Parsely::get_url_with_itm_source( $post['link'], null );
428 $post_urls[] = $post_url;
429 }
430
431 // Fill the URLs with the campaign params.
432 /** @var array<string, array<string, mixed>> $request_params_with_campaign */
433 $request_params_with_campaign['urls'] = $post_urls;
434
435 /**
436 * The raw analytics data, received by the API.
437 *
438 * @var array<array<string, mixed>>|WP_Error $campaign_request
439 */
440 $campaign_request = $this->content_api->get_posts( $request_params_with_campaign );
441
442 if ( is_wp_error( $campaign_request ) ) {
443 /** @var WP_Error $campaign_request */
444 return $campaign_request;
445 }
446
447 $posts_with_campaign_data = array();
448 foreach ( $posts as $post ) {
449 // Find the post by URL in the campaign request.
450 $campaign_post = array_filter(
451 $campaign_request,
452 function ( array $item ) use ( $post ) {
453 return $item['link'] === $post['link'];
454 }
455 );
456
457 if ( array() === $campaign_post ) {
458 // If there are no campaign metrics available, skip this one.
459 $posts_with_campaign_data[] = $post;
460 continue;
461 }
462
463 /** @var array<string, array<string, mixed>> $campaign_post */
464 $campaign_post = $campaign_post[0];
465
466 $post['campaign_metrics'] = array(
467 'views' => $campaign_post['metrics']['views'],
468 'visitors' => $campaign_post['metrics']['visitors'],
469 'recirculation_rate' => $campaign_post['metrics']['recirculation_rate'],
470 'avg_engaged' => $campaign_post['metrics']['avg_engaged'],
471 );
472
473 $posts_with_campaign_data[] = $post;
474 }
475
476 return $posts_with_campaign_data;
477 }
478 }
479