PluginProbe
Parse.ly / 3.24.2
Parse.ly v3.24.2
3.24.3 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 All 107 releases
← All changes | src/rest-api/stats/class-endpoint-posts.php +243 -42 3.18.0 → 3.24.2 View file →
@@ -21,8 +21,11 @@
21 21 * The Stats API Posts endpoint.
22 22 *
23 23 * Provides an endpoint for retrieving posts.
24 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 + *
25 28 * @since 3.17.0
26 29 */
27 30 class Endpoint_Posts extends Base_Endpoint {
28 31 use Post_Data_Trait;
@@ -28,8 +31,9 @@
28 31 use Post_Data_Trait;
29 32
30 33 public const TOP_POSTS_DEFAULT_LIMIT = 5;
31 34 public const SORT_DEFAULT = 'views';
35 + public const MAX_URLS = 500;
32 36
33 37 /**
34 38 * The metrics that can be sorted by.
35 39 *
@@ -82,9 +86,9 @@
82 86 $this->content_api = $this->parsely->get_content_api();
83 87 }
84 88
85 89 /**
86 - * Returns the endpoint name.
90 + * Returns the endpoint's name.
87 91 *
88 92 * @since 3.17.0
89 93 *
90 94 * @return string
@@ -108,35 +112,41 @@
108 112 array( 'GET' ),
109 113 array( $this, 'get_posts' ),
110 114 array_merge(
111 115 array(
112 - 'period_start' => 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(
113 123 'description' => 'The start of the period to query.',
114 124 'type' => 'string',
115 125 'required' => false,
116 126 ),
117 - 'period_end' => array(
127 + 'period_end' => array(
118 128 'description' => 'The end of the period to query.',
119 129 'type' => 'string',
120 130 'required' => false,
121 131 ),
122 - 'pub_date_start' => array(
132 + 'pub_date_start' => array(
123 133 'description' => 'The start of the publication date range to query.',
124 134 'type' => 'string',
125 135 'required' => false,
126 136 ),
127 - 'pub_date_end' => array(
137 + 'pub_date_end' => array(
128 138 'description' => 'The end of the publication date range to query.',
129 139 'type' => 'string',
130 140 'required' => false,
131 141 ),
132 - 'limit' => array(
142 + 'limit' => array(
133 143 'description' => 'The number of posts to return.',
134 144 'type' => 'integer',
135 145 'required' => false,
136 146 'default' => self::TOP_POSTS_DEFAULT_LIMIT,
137 147 ),
138 - 'sort' => array(
148 + 'sort' => array(
139 149 'description' => 'The sort order of the posts.',
140 150 'type' => 'string',
141 151 'enum' => self::SORT_METRICS,
142 152 'default' => self::SORT_DEFAULT,
@@ -141,46 +151,71 @@
141 151 'enum' => self::SORT_METRICS,
142 152 'default' => self::SORT_DEFAULT,
143 153 'required' => false,
144 154 ),
145 - 'page' => array(
155 + 'page' => array(
146 156 'description' => 'The page to fetch.',
147 157 'type' => 'integer',
148 158 'required' => false,
149 159 'default' => 1,
150 160 ),
151 - 'author' => array(
152 - 'description' => 'Comma-separated list of authors to filter by.',
153 - 'type' => 'string',
154 - 'required' => false,
155 - 'validate_callback' => array( $this, 'validate_max_length_is_5' ),
156 - 'sanitize_callback' => array( $this, 'sanitize_string_to_array' ),
161 + 'author' => array(
162 + 'description' => 'The author to filter by.',
163 + 'type' => 'string',
164 + 'required' => false,
157 165 ),
158 - 'section' => array(
159 - 'description' => 'Comma-separated list of sections to filter by.',
160 - 'type' => 'string',
161 - 'required' => false,
162 - 'validate_callback' => array( $this, 'validate_max_length_is_5' ),
163 - 'sanitize_callback' => array( $this, 'sanitize_string_to_array' ),
166 + 'section' => array(
167 + 'description' => 'The section to filter by.',
168 + 'type' => 'string',
169 + 'required' => false,
164 170 ),
165 - 'tag' => array(
166 - 'description' => 'Comma-separated list of tags to filter by.',
171 + 'tag' => array(
172 + 'description' => 'The tags to filter by (in comma-separated format).',
167 173 'type' => 'string',
168 174 'required' => false,
169 175 'validate_callback' => array( $this, 'validate_max_length_is_5' ),
170 176 'sanitize_callback' => array( $this, 'sanitize_string_to_array' ),
171 177 ),
172 - 'segment' => array(
178 + 'segment' => array(
173 179 'description' => 'The segment to filter by.',
174 180 'type' => 'string',
175 181 'required' => false,
176 182 ),
177 - 'urls' => array(
183 + 'urls' => array(
178 184 'description' => 'The URLs to fetch data for.',
179 185 'type' => 'array',
186 + 'items' => array( 'type' => 'string' ),
187 + 'maxItems' => self::MAX_URLS,
180 188 'sanitize_callback' => array( $this, 'sanitize_urls' ),
189 + 'validate_callback' => array( $this, 'validate_urls' ),
181 190 'required' => false,
182 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 + ),
183 218 ),
184 219 $this->get_itm_source_param_args()
185 220 )
186 221 );
@@ -214,8 +249,42 @@
214 249 return array_map( 'sanitize_url', $urls );
215 250 }
216 251
217 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 + /**
218 287 * Validates that the parameter has at most 5 items.
219 288 *
220 289 * @since 3.17.0
221 290 *
@@ -249,35 +318,70 @@
249 318
250 319 // Setup the itm_source if it is provided.
251 320 $this->set_itm_source_from_request( $request );
252 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 +
253 364 /**
254 365 * The raw analytics data, received by the API.
255 366 *
256 - * @var array<stdClass>|WP_Error $analytics_request
367 + * @var array<array<string, mixed>>|WP_Error $analytics_request
257 368 */
258 - $analytics_request = $this->content_api->get_posts(
259 - array(
260 - 'period_start' => $params['period_start'] ?? null,
261 - 'period_end' => $params['period_end'] ?? null,
262 - 'pub_date_start' => $params['pub_date_start'] ?? null,
263 - 'pub_date_end' => $params['pub_date_end'] ?? null,
264 - 'limit' => $params['limit'] ?? self::TOP_POSTS_DEFAULT_LIMIT,
265 - 'sort' => $params['sort'] ?? self::SORT_DEFAULT,
266 - 'page' => $params['page'] ?? 1,
267 - 'author' => $params['author'] ?? null,
268 - 'section' => $params['section'] ?? null,
269 - 'tag' => $params['tag'] ?? null,
270 - 'segment' => $params['segment'] ?? null,
271 - 'itm_source' => $params['itm_source'] ?? null,
272 - 'urls' => $params['urls'] ?? null,
273 - )
274 - );
369 + $analytics_request = $this->content_api->get_posts( $request_params );
275 370
276 371 if ( is_wp_error( $analytics_request ) ) {
277 372 return $analytics_request;
278 373 }
279 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 +
280 384 // Process the data.
281 385 $posts = array();
282 386
283 387 /**
@@ -294,6 +398,103 @@
294 398 'data' => $posts,
295 399 );
296 400
297 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;
298 499 }
299 500 }