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 / Utils / class-utils.php

class-utils.php in Parse.ly 3.24.2, at src/Utils/class-utils.php

632 lines 15.8 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Utils Class.
4 *
5 * To enforce typing on commonly used functions.
6 *
7 * @package Parsely
8 * @since 3.7.0
9 * @since 3.17.0 Refactored to a class.
10 */
11
12 declare(strict_types=1);
13
14 namespace Parsely\Utils;
15
16 use WP_Post;
17 use WP_Error;
18
19 use const Parsely\PARSELY_CACHE_GROUP;
20 use const Parsely\PARSELY_FILE;
21
22 /**
23 * Utils Class.
24 *
25 * @since 3.17.0
26 *
27 * @phpstan-type ItmParams array{
28 * campaign: string,
29 * source?: string,
30 * medium?: string,
31 * content?: string,
32 * term?: string,
33 * }
34 */
35 class Utils {
36 const DATE_UTC_FORMAT = 'Y-m-d';
37 const WP_DATE_TIME_FORMAT = 'Y-m-d H:i:s';
38
39 /**
40 * Gets UTC Date.
41 *
42 * @since 3.7.0
43 *
44 * @param int $days Number of days before or after the current date.
45 *
46 * @return string
47 */
48 public static function get_utc_date_format( int $days = 0 ): string {
49 if ( 0 === $days ) {
50 return gmdate( self::DATE_UTC_FORMAT );
51 }
52
53 return gmdate( self::DATE_UTC_FORMAT, (int) strtotime( "{$days} days" ) );
54 }
55
56 /**
57 * Gets default category.
58 *
59 * @since 3.7.0
60 *
61 * @return int
62 */
63 public static function get_default_category(): int {
64 /**
65 * Variable.
66 *
67 * @var string
68 */
69 $default_category = get_option( 'default_category' );
70 return (int) $default_category;
71 }
72
73 /**
74 * Gets option `page_for_posts`.
75 *
76 * @since 3.7.0
77 *
78 * @param bool $default_value Default Value.
79 *
80 * @return int|WP_Post
81 */
82 public static function get_page_for_posts( bool $default_value = false ) {
83 /**
84 * Variable.
85 *
86 * @var int|WP_Post
87 */
88 return get_option( 'page_for_posts', $default_value );
89 }
90
91 /**
92 * Gets option `page_on_front`.
93 *
94 * @since 3.7.0
95 *
96 * @return bool
97 */
98 public static function get_page_on_front(): bool {
99 /**
100 * Variable.
101 *
102 * @var bool
103 */
104 return (bool) get_option( 'page_on_front' );
105 }
106
107 /**
108 * Gets 'string' query variable from WP_Query class.
109 *
110 * @since 3.7.0
111 *
112 * @param string $key Variable key to retrieve.
113 *
114 * @return string
115 */
116 public static function get_string_query_var( string $key ): string {
117 /**
118 * Variable.
119 *
120 * @var string
121 */
122 return get_query_var( $key );
123 }
124
125 /**
126 * Gets site date format.
127 *
128 * @since 3.7.0
129 *
130 * @return string
131 */
132 public static function get_date_format(): string {
133 /**
134 * Variable.
135 *
136 * @var string
137 */
138 return get_option( 'date_format' );
139 }
140
141 /**
142 * Gets site time format.
143 *
144 * @since 3.7.0
145 *
146 * @return string
147 */
148 public static function get_time_format(): string {
149 /**
150 * Variable.
151 *
152 * @var string
153 */
154 return get_option( 'time_format' );
155 }
156
157 /**
158 * Returns the current time's timestamp.
159 *
160 * @since 3.12.0
161 *
162 * @return string
163 */
164 public static function get_timestamp(): string {
165 $timestamp = round( microtime( true ) * 1000 );
166 return number_format( $timestamp, 0, '', '' );
167 }
168
169 /**
170 * Gets number in formatted form i.e. express bigger numbers in form of
171 * thousands (k), millions (M), billions (B).
172 *
173 * Note: This function is not made to process float numbers, and it is a PHP
174 * port of our formatToImpreciseNumber() TypeScript function.
175 *
176 * Example:
177 * - Represent 10000 as 10K.
178 *
179 * @since 3.7.0
180 *
181 * @param string $value The number to process. It can be formatted.
182 * @param int $fraction_digits The number of desired fraction digits.
183 * @param string $glue A string to put between the number and unit.
184 *
185 * @return string The number formatted as an imprecise number.
186 */
187 public static function get_formatted_number( string $value, int $fraction_digits = 1, string $glue = '' ): string {
188 $number = (int) preg_replace( '/\D/', '', $value );
189
190 if ( $number < 1000 ) {
191 return $value;
192 } elseif ( $number < 10000 ) {
193 $fraction_digits = 1;
194 }
195
196 $unit_names = array(
197 '1000' => 'k',
198 '1000000' => 'M',
199 '1000000000' => 'B',
200 '1000000000000' => 'T',
201 '1000000000000000' => 'Q',
202 );
203 $current_number = $number;
204 $current_number_as_string = (string) $number;
205 $unit = '';
206 $previous_number = 0;
207
208 foreach ( $unit_names as $thousands => $suffix ) {
209 $thousands_int = (int) preg_replace( '/\D/', '', (string) $thousands );
210
211 if ( $number >= $thousands_int ) {
212 $current_number = $number / $thousands_int;
213 $precision = $fraction_digits;
214
215 // For over 10 units, we reduce the precision to 1 fraction digit.
216 $modulo = (int) fmod( $current_number, 1 );
217 if ( 0 !== $previous_number && $modulo > 1 / $previous_number ) {
218 $precision = $current_number > 10 ? 1 : 2;
219 }
220
221 // Precision override, where we want to show 2 fraction digits.
222 $zeroes = floatval( number_format( $current_number, 2 ) ) === floatval( number_format( $current_number, 0 ) );
223 $precision = $zeroes ? 0 : $precision;
224 $current_number_as_string = number_format( $current_number, $precision, '.', '' );
225 $unit = $suffix;
226 }
227
228 $previous_number = $current_number;
229 }
230
231 return $current_number_as_string . $glue . $unit;
232 }
233
234 /**
235 * Gets time in formatted form.
236 *
237 * Example:
238 * - Input `1000` (seconds) and Output `16:40` which represents "16 minutes, 40 seconds”
239 *
240 * @since 3.7.0
241 *
242 * @param float $seconds Time in seconds to be formatted.
243 *
244 * @return string
245 */
246 public static function get_formatted_time( $seconds ): string {
247 $seconds = round( $seconds );
248 $hours = floor( $seconds / 3600 );
249
250 if ( $hours >= 1 ) {
251 $seconds = $seconds - ( $hours * 3600 );
252 $minutes = floor( $seconds / 60 );
253 $seconds = round( $seconds % 60 );
254
255 return esc_html(
256 sprintf(
257 /* translators: 1: Number of hours 2: Number of minutes 3: Number of seconds */
258 __( '%1$d:%2$02d:%3$02d', 'wp-parsely' ),
259 $hours,
260 $minutes,
261 $seconds
262 )
263 );
264 }
265
266 $minutes = floor( $seconds / 60 );
267 $seconds = round( $seconds % 60 );
268
269 if ( $minutes >= 1 ) {
270 return esc_html(
271 sprintf(
272 /* translators: 1: Number of minutes 2: Number of seconds */
273 __( '%1$d:%2$02d', 'wp-parsely' ),
274 $minutes,
275 $seconds
276 )
277 );
278 }
279
280 return esc_html(
281 sprintf(
282 /* translators: 1: Number of seconds */
283 __( '%1$ds', 'wp-parsely' ),
284 round( $seconds )
285 )
286 );
287 }
288
289 /**
290 * Returns the passed float as a time duration in m:ss format.
291 *
292 * Examples:
293 * - $time of 1.005 yields '1:00'.
294 * - $time of 1.5 yields '1:30'.
295 * - $time of 1.999 yields '2:00'.
296 *
297 * @since 3.6.0
298 *
299 * @param float $time The time as a float number.
300 *
301 * @return string The resulting formatted time duration.
302 */
303 public static function get_formatted_duration( float $time ): string {
304 $minutes = absint( $time );
305 $seconds = absint( round( fmod( $time, 1 ) * 60 ) );
306
307 if ( 60 === $seconds ) {
308 ++$minutes;
309 $seconds = 0;
310 }
311
312 return sprintf( '%d:%02d', $minutes, $seconds );
313 }
314
315 /**
316 * Converts to associate array.
317 *
318 * @since 3.7.0
319 *
320 * @param mixed $obj Input object.
321 *
322 * @return array<string, mixed>|WP_Error
323 */
324 public static function convert_to_associative_array( $obj ) {
325 $encoded = wp_json_encode( $obj );
326 if ( false === $encoded ) {
327 return new WP_Error( 'parsely_encoding_failed', __( 'Unable to encode API response for associative array', 'wp-parsely' ) );
328 }
329
330 /**
331 * Variable.
332 *
333 * @var array<string, mixed>
334 */
335 return json_decode( $encoded, true );
336 }
337
338 /**
339 * Converts a string to a positive integer, removing any non-numeric
340 * characters.
341 *
342 * @param string $value The string to be converted to an integer.
343 * @return int The integer resulting from the conversion.
344 */
345 public static function convert_to_positive_integer( string $value ): int {
346 return (int) preg_replace( '/\D/', '', $value );
347 }
348
349 /**
350 * Converts endpoint to filter key by replacing `/` with `_`.
351 *
352 * @param string $endpoint Route of the endpoint.
353 *
354 * @since 3.7.0
355 *
356 * @return string
357 */
358 public static function convert_endpoint_to_filter_key( string $endpoint ): string {
359 return trim( str_replace( array( '-', '/' ), '_', $endpoint ), '_' );
360 }
361
362 /**
363 * Gets content of asset file.
364 *
365 * @param string $path Path of the asset file.
366 *
367 * @since 3.8.0
368 *
369 * @return Asset_Info
370 */
371 public static function get_asset_info( string $path ) {
372 return require plugin_dir_path( PARSELY_FILE ) . $path;
373 }
374
375 /**
376 * Checks if a string starts with a specific substring.
377 *
378 * This function uses the built-in PHP function `str_starts_with` if it's available (PHP 8.0 and later).
379 * If the function is not available (PHP versions prior to 8.0), it uses the `strpos` function as a fallback.
380 *
381 * @since 3.13.0
382 *
383 * @param string $haystack The string to search in.
384 * @param string $needle The substring to search for at the start of $haystack.
385 * @return bool Returns true if $haystack starts with $needle, false otherwise.
386 */
387 public static function str_starts_with( string $haystack, string $needle ): bool {
388 if ( function_exists( '\str_starts_with' ) ) {
389 return \str_starts_with( $haystack, $needle );
390 }
391 return 0 === strpos( $haystack, $needle );
392 }
393
394 /**
395 * Checks if HTTPS is supported for the site.
396 *
397 * This function checks if the WordPress function 'wp_is_using_https' exists and uses it to determine if
398 * HTTPS is supported.
399 * If the function does not exist, it checks if the home URL scheme is HTTPS.
400 * If neither of the above conditions are met, it checks if the site URL option scheme is HTTPS.
401 *
402 * @since 3.14.1
403 *
404 * @return bool Returns true if HTTPS is supported, false otherwise.
405 */
406 public static function parsely_is_https_supported(): bool {
407 if ( function_exists( 'wp_is_using_https' ) ) {
408 return wp_is_using_https();
409 }
410
411 if ( 'https' === wp_parse_url( home_url(), PHP_URL_SCHEME ) ) {
412 return true;
413 }
414
415 // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound
416 $site_url = apply_filters( 'site_url', get_option( 'siteurl' ), '', null, null );
417 return 'https' === wp_parse_url( $site_url, PHP_URL_SCHEME );
418 }
419
420 /**
421 * Returns the post ID for the passed URL.
422 *
423 * Posts the current user can neither view publicly nor read resolve to 0.
424 *
425 * @since 3.16.0
426 * @since 3.18.0 Moved from `Models/class-smart-link.php`.
427 * @since 3.24.2 Resolves only to posts visible to the current user, and
428 * matches slugs only of this site's URLs, unambiguously.
429 *
430 * @param string $url The URL to get the post ID for.
431 * @return int The post ID of the URL, 0 if not found.
432 */
433 public static function get_post_id_by_url( string $url ): int {
434 $cache_key = sprintf( 'url-to-postid-%s', hash( 'sha256', $url ) );
435 $cache = wp_cache_get( $cache_key, PARSELY_CACHE_GROUP );
436
437 if ( false !== $cache && is_numeric( $cache ) ) {
438 return self::get_visible_post_id( (int) $cache );
439 }
440
441 if ( function_exists( 'wpcom_vip_url_to_postid' ) ) {
442 $post_id = wpcom_vip_url_to_postid( $url );
443 } else {
444 // phpcs:ignore WordPressVIPMinimum.Functions.RestrictedFunctions.url_to_postid_url_to_postid
445 $post_id = url_to_postid( $url );
446 wp_cache_set( $cache_key, $post_id, PARSELY_CACHE_GROUP, WEEK_IN_SECONDS );
447 }
448
449 // A post ID was found, return it.
450 if ( 0 !== $post_id ) {
451 return self::get_visible_post_id( $post_id );
452 }
453
454 // A slug can only identify a post in this site's URLs.
455 if ( ! self::is_site_url( $url ) ) {
456 return 0;
457 }
458
459 // No post ID was found, try to find it from the slug.
460 $clean_url = preg_replace( '/\?.*$/', '', $url ); // Remove the query string from the URL.
461 if ( null === $clean_url ) {
462 $clean_url = $url;
463 }
464 $post_slug = basename( $clean_url );
465
466 // Public posts take precedence over other posts with the same slug.
467 $post_ids = array_values(
468 array_filter(
469 self::get_top_level_post_ids_by_slug( $post_slug, array( 'publish', 'inherit' ) ),
470 'is_post_publicly_viewable'
471 )
472 );
473
474 // Visibility is checked on return, so the resolution can be cached for all users.
475 if ( 0 === count( $post_ids ) ) {
476 $post_ids = self::get_top_level_post_ids_by_slug( $post_slug, array( 'any' ) );
477 }
478
479 // A slug shared by several posts identifies none of them.
480 if ( 1 === count( $post_ids ) ) {
481 wp_cache_set( $cache_key, $post_ids[0], PARSELY_CACHE_GROUP, WEEK_IN_SECONDS );
482 return self::get_visible_post_id( $post_ids[0] );
483 }
484
485 return 0;
486 }
487
488 /**
489 * Returns up to 2 IDs of top-level posts of public post types with the
490 * passed slug, enough to tell whether the slug is ambiguous.
491 *
492 * Like get_page_by_path() given a single slug, child posts don't match.
493 *
494 * @since 3.24.2
495 *
496 * @param string $slug The post slug.
497 * @param array<string> $post_statuses The post statuses to match.
498 * @return array<int> The post IDs.
499 */
500 private static function get_top_level_post_ids_by_slug( string $slug, array $post_statuses ): array {
501 $public_post_types = get_post_types(
502 array(
503 'public' => true,
504 'show_in_rest' => true,
505 )
506 );
507
508 $query = new \WP_Query(
509 array(
510 'name' => $slug,
511 'post_type' => array_keys( $public_post_types ),
512 'post_status' => $post_statuses,
513 'post_parent' => 0,
514 'fields' => 'ids',
515 'posts_per_page' => 2,
516 'no_found_rows' => true,
517 )
518 );
519
520 $post_ids = array();
521 foreach ( $query->posts as $post_id ) {
522 if ( is_int( $post_id ) ) {
523 $post_ids[] = $post_id;
524 }
525 }
526
527 return $post_ids;
528 }
529
530 /**
531 * Returns the passed post ID, or 0 if the current user can neither view
532 * the post publicly nor read it.
533 *
534 * @since 3.24.2
535 *
536 * @param int $post_id The post ID.
537 * @return int The post ID, or 0.
538 */
539 private static function get_visible_post_id( int $post_id ): int {
540 if ( is_post_publicly_viewable( $post_id ) || current_user_can( 'read_post', $post_id ) ) {
541 return $post_id;
542 }
543
544 return 0;
545 }
546
547 /**
548 * Returns whether the passed URL is relative, or has the host of the home
549 * URL, the site URL, the Site ID or the canonical URL domain.
550 *
551 * A leading `www.` is ignored, as `url_to_postid()` does.
552 *
553 * @since 3.24.2
554 *
555 * @param string $url The URL to check.
556 * @return bool Whether the URL belongs to this site.
557 */
558 private static function is_site_url( string $url ): bool {
559 $url_host = wp_parse_url( $url, PHP_URL_HOST );
560
561 if ( ! is_string( $url_host ) ) {
562 return true;
563 }
564
565 $site_addresses = array(
566 home_url(),
567 site_url(),
568 \Parsely\get_parsely()->get_site_id(),
569 apply_filters( 'wp_parsely_canonical_url_domain', null ),
570 );
571
572 foreach ( $site_addresses as $site_address ) {
573 if ( ! is_string( $site_address ) || '' === trim( $site_address ) ) {
574 continue;
575 }
576
577 // Domain-only addresses need a scheme to be parsed.
578 $site_host = wp_parse_url(
579 'http://' . preg_replace( '#^https?://#i', '', trim( $site_address ) ),
580 PHP_URL_HOST
581 );
582
583 if ( is_string( $site_host ) && self::strip_www( $site_host ) === self::strip_www( $url_host ) ) {
584 return true;
585 }
586 }
587
588 return false;
589 }
590
591 /**
592 * Lowercases a host and removes its leading `www.`.
593 *
594 * @since 3.24.2
595 *
596 * @param string $host The host.
597 * @return string The host, without its leading `www.`.
598 */
599 private static function strip_www( string $host ): string {
600 return (string) preg_replace( '/^www\./', '', strtolower( $host ) );
601 }
602
603 /**
604 * Appends ITM parameters to a URL.
605 *
606 * @since 3.19.0
607 *
608 * @param string $url The URL to append the ITM parameters to.
609 * @param ItmParams $params The ITM parameters to append.
610 * @return string The URL with the ITM parameters appended.
611 */
612 public static function append_itm_params( string $url, $params ): string {
613 // Convert the params array to the correct format.
614 $mapping = array(
615 'campaign' => 'itm_campaign',
616 'source' => 'itm_source',
617 'medium' => 'itm_medium',
618 'content' => 'itm_content',
619 'term' => 'itm_term',
620 );
621
622 $itm_params = array();
623 foreach ( $params as $key => $value ) {
624 if ( array_key_exists( $key, $mapping ) ) {
625 $itm_params[ $mapping[ $key ] ] = $value;
626 }
627 }
628
629 return add_query_arg( $itm_params, $url );
630 }
631 }
632