class-redux-functions-ex.php (17880B)
1 <?php 2 /** 3 * Redux Framework Private Extended Functions Container Class 4 * 5 * @class Redux_Functions_Ex 6 * @since 3.0.0 7 * @package Redux_Framework/Classes 8 */ 9 10 // Exit if accessed directly. 11 defined( 'ABSPATH' ) || exit; 12 13 // Don't duplicate me! 14 if ( ! class_exists( 'Redux_Functions_Ex', false ) ) { 15 16 /** 17 * Redux Functions Class 18 * A Class of useful functions that can/should be shared among all Redux files. 19 * 20 * @since 3.0.0 21 */ 22 class Redux_Functions_Ex { 23 24 /** 25 * What is this for? 26 * 27 * @var array 28 */ 29 public static $args; 30 31 /** 32 * Output alpha data tag for Iris alpha color picker, if enabled. 33 * 34 * @param array $data Data array. 35 * 36 * @return string 37 */ 38 public static function output_alpha_data( array $data ): string { 39 extract( $data ); // phpcs:ignore WordPress.PHP.DontExtract 40 41 $value = false; 42 43 if ( isset( $field['color_alpha'] ) && $field['color_alpha'] ) { 44 if ( is_array( $field['color_alpha'] ) ) { 45 $value = $field['color_alpha'][ $index ] ?? false; 46 } else { 47 $value = $field['color_alpha']; 48 } 49 } 50 51 return 'data-alpha-enabled="' . (bool) esc_attr( $value ) . '"'; 52 } 53 54 /** 55 * Parses the string into variables without the max_input_vars limitation. 56 * 57 * @param string $string String of data. 58 * 59 * @return array|false $result 60 * @since 3.5.7.11 61 * @author harunbasic 62 * @access private 63 */ 64 public static function parse_str( string $string ) { 65 if ( '' === $string ) { 66 return false; 67 } 68 69 $result = array(); 70 $pairs = explode( '&', $string ); 71 72 foreach ( $pairs as $key => $pair ) { 73 // use the original parse_str() on each element. 74 parse_str( $pair, $params ); 75 76 $k = key( $params ); 77 78 if ( ! isset( $result[ $k ] ) ) { 79 $result += $params; 80 } elseif ( is_array( $result[ $k ] ) && is_array( $params[ $k ] ) ) { 81 $result[ $k ] = self::array_merge_recursive_distinct( $result[ $k ], $params[ $k ] ); 82 } 83 } 84 85 return $result; 86 } 87 88 /** 89 * Merge arrays without converting values with duplicate keys to arrays as array_merge_recursive does. 90 * As seen here http://php.net/manual/en/function.array-merge-recursive.php#92195 91 * 92 * @since 3.5.7.11 93 * 94 * @param array $array1 array one. 95 * @param array $array2 array two. 96 * 97 * @return array $merged 98 * @author harunbasic 99 * @access private 100 */ 101 public static function array_merge_recursive_distinct( array $array1, array $array2 ): array { 102 $merged = $array1; 103 104 foreach ( $array2 as $key => $value ) { 105 106 if ( is_array( $value ) && isset( $merged[ $key ] ) && is_array( $merged[ $key ] ) ) { 107 $merged[ $key ] = self::array_merge_recursive_distinct( $merged[ $key ], $value ); 108 } elseif ( is_numeric( $key ) && isset( $merged[ $key ] ) ) { 109 $merged[] = $value; 110 } else { 111 $merged[ $key ] = $value; 112 } 113 } 114 115 return $merged; 116 } 117 118 /** 119 * Records calling function. 120 * 121 * @param string $opt_name Panel opt_name. 122 */ 123 public static function record_caller( string $opt_name = '' ) { 124 global $pagenow; 125 126 // phpcs:ignore WordPress.Security.NonceVerification 127 if ( ! ( 'options-general.php' === $pagenow && isset( $_GET['page'] ) && ( 'redux-framework' === $_GET['page'] || 'health-check' === $_GET['page'] ) ) ) { 128 return; 129 } 130 131 // phpcs:ignore WordPress.PHP.DevelopmentFunctions 132 $caller = debug_backtrace( DEBUG_BACKTRACE_IGNORE_ARGS, 2 )[1]['file']; 133 134 if ( ! empty( $caller ) && ! empty( $opt_name ) && class_exists( 'Redux_Core' ) ) { 135 if ( ! isset( Redux_Core::$callers[ $opt_name ] ) ) { 136 Redux_Core::$callers[ $opt_name ] = array(); 137 } 138 139 if ( strpos( $caller, 'class-redux-' ) !== false || strpos( $caller, 'redux-core/framework.php' ) ) { 140 return; 141 } 142 143 if ( ! in_array( $caller, Redux_Core::$callers[ $opt_name ], true ) ) { 144 Redux_Core::$callers[ $opt_name ][] = $caller; 145 } 146 147 if ( ! empty( self::$args[ $opt_name ]['callers'] ) && ! in_array( $caller, self::$args[ $opt_name ]['callers'], true ) ) { 148 self::$args[ $opt_name ]['callers'][] = $caller; 149 } 150 } 151 } 152 153 /** 154 * Normalize path. 155 * 156 * @param string $path Path to normalize. 157 * 158 * @return string|string[]|null 159 */ 160 public static function wp_normalize_path( string $path = '' ) { 161 if ( function_exists( 'wp_normalize_path' ) ) { 162 $path = wp_normalize_path( $path ); 163 } else { 164 // Shim for pre WP 3.9. 165 $path = str_replace( '\\', '/', $path ); 166 $path = preg_replace( '|(?<=.)/+|', '/', $path ); 167 168 if ( ':' === substr( $path, 1, 1 ) ) { 169 $path = ucfirst( $path ); 170 } 171 } 172 173 return $path; 174 } 175 176 /** 177 * Action to add generator tag to page HEAD. 178 */ 179 public static function generator() { 180 add_action( 'wp_head', array( 'Redux_Functions_Ex', 'meta_tag' ) ); 181 } 182 183 184 /** 185 * Callback for wp_head hook to add meta tag. 186 */ 187 public static function meta_tag() { 188 echo '<meta name="framework" content="Redux ' . esc_html( Redux_Core::$version ) . '" />'; 189 } 190 191 /** 192 * Run URL through a ssl check. 193 * 194 * @param string $url URL to check. 195 * 196 * @return string 197 */ 198 public static function verify_url_protocol( string $url ): string { 199 // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotValidated 200 $protocol = ! empty( $_SERVER['HTTPS'] ) && 'off' !== $_SERVER['HTTPS'] || ( ! empty( $_SERVER['SERVER_PORT'] ) && 443 === $_SERVER['SERVER_PORT'] ) ? 'https://' : 'http://'; 201 202 if ( isset( $_SERVER['HTTP_X_FORWARDED_PROTO'] ) && ! empty( $_SERVER['HTTP_X_FORWARDED_PROTO'] ) ) { // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotValidated 203 $new_protocol = sanitize_text_field( wp_unslash( $_SERVER['HTTP_X_FORWARDED_PROTO'] ) ) . '://'; // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotValidated 204 if ( 'http://' === $protocol && $new_protocol !== $protocol && false === strpos( $url, $new_protocol ) ) { 205 $url = str_replace( $protocol, $new_protocol, $url ); 206 } 207 } 208 209 return $url; 210 } 211 212 /** 213 * Check s. 214 * 215 * @access public 216 * @since 4.0.0 217 * @return bool 218 */ 219 public static function s(): bool { 220 if ( ! get_option( 'redux_p' . 'ro_lic' . 'ense_key', false ) ) { // phpcs:ignore Generic.Strings.UnnecessaryStringConcat.Found 221 $s = get_option( 'redux_p' . 'ro_l' . 'icense_status', false ); // phpcs:ignore Generic.Strings.UnnecessaryStringConcat.Found 222 223 if ( false !== $s && in_array( $s, array( 'valid', 'site_inactive' ), true ) ) { 224 return true; 225 } 226 } 227 228 return false; 229 } 230 231 /** 232 * Is file in theme. 233 * 234 * @param string $file File to check. 235 * 236 * @return bool 237 */ 238 public static function file_in_theme( string $file ): bool { 239 $file_path = self::wp_normalize_path( dirname( $file ) ); 240 241 if ( strpos( $file_path, self::wp_normalize_path( get_template_directory() ) ) !== false ) { 242 return true; 243 } elseif ( strpos( $file_path, self::wp_normalize_path( get_stylesheet_directory() ) ) !== false ) { 244 return true; 245 } 246 247 return false; 248 } 249 250 /** 251 * Is Redux embedded inside a plugin. 252 * 253 * @param string $file File to check. 254 * 255 * @return array|bool 256 */ 257 public static function is_inside_plugin( string $file ) { 258 $file = self::wp_normalize_path( $file ); 259 $plugin_basename = self::wp_normalize_path( plugin_basename( $file ) ); 260 261 if ( self::file_in_theme( $file ) ) { 262 return false; 263 } 264 265 if ( $plugin_basename !== $file ) { 266 $slug = explode( '/', $plugin_basename ); 267 $slug = $slug[0]; 268 269 return array( 270 'slug' => $slug, 271 'basename' => $plugin_basename, 272 'path' => $file, 273 'url' => self::verify_url_protocol( plugins_url( $plugin_basename ) ), 274 'real_path' => self::wp_normalize_path( dirname( realpath( $file ) ) ), 275 ); 276 } 277 278 return false; 279 } 280 281 /** 282 * Is Redux embedded in a theme. 283 * 284 * @param string $file File to check. 285 * 286 * @return array|bool 287 */ 288 public static function is_inside_theme( string $file = '' ) { 289 290 if ( ! self::file_in_theme( $file ) ) { 291 return false; 292 } 293 294 $theme_paths = array( 295 self::wp_normalize_path( get_template_directory() ) => get_template_directory_uri(), 296 // parent. 297 self::wp_normalize_path( get_stylesheet_directory() ) => get_stylesheet_directory_uri(), 298 // child. 299 ); 300 301 $theme_paths = array_unique( $theme_paths ); 302 $file_path = self::wp_normalize_path( $file ); 303 304 $filename = explode( '\\', $file ); 305 306 end( $filename ); 307 308 $filename = prev( $filename ); 309 310 foreach ( $theme_paths as $theme_path => $url ) { 311 $real_path = self::wp_normalize_path( realpath( $theme_path ) ); 312 313 if ( empty( $real_path ) ) { 314 continue; 315 } 316 317 if ( strpos( $file_path, $real_path ) !== false ) { 318 $slug = explode( '/', $theme_path ); 319 $slug = end( $slug ); 320 $relative_path = explode( $slug . '/', dirname( $file_path ) ); 321 $relative_path = $relative_path[1]; 322 $data = array( 323 'slug' => $slug, 324 'path' => trailingslashit( trailingslashit( $theme_path ) . $relative_path ) . $filename, 325 'real_path' => trailingslashit( trailingslashit( $real_path ) . $relative_path ) . $filename, 326 'url' => self::verify_url_protocol( trailingslashit( trailingslashit( $url ) . $relative_path ) . $filename ), 327 'basename' => trailingslashit( $slug ) . trailingslashit( $relative_path ) . $filename, 328 ); 329 $data['realpath'] = $data['real_path']; // Shim for old extensions. 330 331 if ( count( $theme_paths ) > 1 ) { 332 $key = array_search( $theme_path, $theme_paths, true ); 333 334 if ( false !== $key ) { 335 unset( $theme_paths[ $key ] ); 336 } 337 338 $theme_paths_end = end( $theme_paths ); 339 $parent_slug_end = explode( '/', $theme_paths_end ); 340 $parent_slug_end = end( $parent_slug_end ); 341 342 $data['parent_slug'] = $theme_paths_end; 343 $data['parent_slug'] = $parent_slug_end; 344 } 345 346 return $data; 347 } 348 } 349 350 return false; 351 } 352 353 /** 354 * Used to fix 3.x and 4 compatibility for extensions 355 * 356 * @param object $parent The extension parent object. 357 * @param string $path - Path of the file. 358 * @param string $ext_class - Extension class name. 359 * @param string $new_class_name - New dynamic class name. 360 * @param string $name extension name. 361 * 362 * @return object - Extended field class. 363 */ 364 public static function extension_compatibility( $parent, string $path, string $ext_class, string $new_class_name, string $name ) { 365 if ( empty( $new_class_name ) ) { 366 return; 367 } 368 $upload_dir = ReduxFramework::$_upload_dir . '/extension_compatibility/'; 369 if ( ! file_exists( $upload_dir . $ext_class . '.php' ) ) { 370 if ( ! is_dir( $upload_dir ) ) { 371 $parent->filesystem->mkdir( $upload_dir ); 372 $parent->filesystem->put_contents( $upload_dir . 'index.php', '<?php // Silence is golden.' ); 373 } 374 if ( ! class_exists( $ext_class ) ) { 375 require_once $path; 376 } 377 if ( ! file_exists( $upload_dir . $new_class_name . '.php' ) ) { 378 $class_file = '<?php' . PHP_EOL . PHP_EOL . 379 'class {{ext_class}} extends Redux_Extension_Abstract {' . PHP_EOL . 380 ' private $c;' . PHP_EOL . 381 ' public function __construct( $parent, $path, $ext_class ) {' . PHP_EOL . 382 ' $this->c = $parent->extensions[\'' . $name . '\'];' . PHP_EOL . 383 ' // Add all the params of the Abstract to this instance.' . PHP_EOL . 384 ' foreach( get_object_vars( $this->c ) as $key => $value ) {' . PHP_EOL . 385 ' $this->$key = $value;' . PHP_EOL . 386 ' }' . PHP_EOL . 387 ' parent::__construct( $parent, $path );' . PHP_EOL . 388 ' }' . PHP_EOL . 389 ' // fake "extends Redux_Extension_Abstract\" using magic function' . PHP_EOL . 390 ' public function __call( $method, $args ) {' . PHP_EOL . 391 ' return call_user_func_array( array( $this->c, $method ), $args );' . PHP_EOL . 392 ' }' . PHP_EOL . 393 '}' . PHP_EOL; 394 $template = str_replace( '{{ext_class}}', $new_class_name, $class_file ); 395 $parent->filesystem->put_contents( $upload_dir . $new_class_name . '.php', $template ); 396 } 397 if ( file_exists( $upload_dir . $new_class_name . '.php' ) ) { 398 if ( ! class_exists( $new_class_name ) ) { 399 require_once $upload_dir . $new_class_name . '.php'; 400 } 401 if ( class_exists( $new_class_name ) ) { 402 return new $new_class_name( $parent, $path, $ext_class ); 403 } 404 } 405 } 406 } 407 408 /** 409 * Used to merge two deep arrays. 410 * 411 * @param array $a First array to deep merge. 412 * @param array $b Second array to deep merge. 413 * 414 * @return array - Deep merge of the two arrays. 415 */ 416 public static function nested_wp_parse_args( array &$a, array $b ): array { 417 $a = $a; 418 $b = $b; 419 $result = $b; 420 421 foreach ( $a as $k => &$v ) { 422 if ( is_array( $v ) && isset( $result[ $k ] ) ) { 423 $result[ $k ] = self::nested_wp_parse_args( $v, $result[ $k ] ); 424 } else { 425 $result[ $k ] = $v; 426 } 427 } 428 429 return $result; 430 } 431 432 /** 433 * AJAX callback key 434 */ 435 public static function hash_key(): string { 436 $key = defined( 'AUTH_KEY' ) ? AUTH_KEY : get_site_url(); 437 $key .= defined( 'SECURE_AUTH_KEY' ) ? SECURE_AUTH_KEY : ''; 438 439 return $key; 440 } 441 442 /** 443 * Check if Redux is activated. 444 * 445 * @access public 446 * @since 4.0.0 447 */ 448 public static function activated(): bool { 449 if ( Redux_Core::$insights->tracking_allowed() ) { 450 return true; 451 } 452 453 return false; 454 } 455 456 /** 457 * Set Redux to activate. 458 * 459 * @access public 460 * @since 4.0.0 461 */ 462 public static function set_activated() { 463 Redux_Core::$insights->optin(); 464 } 465 466 /** 467 * Set Redux to deactivate. 468 * 469 * @access public 470 * @since 4.0.0 471 */ 472 public static function set_deactivated() { 473 Redux_Core::$insights->optout(); 474 } 475 476 /** 477 * Register a class path to be autoloaded. 478 * Registers a namespace to be autoloaded from a given path, using the 479 * WordPress/HM-style filenames (`class-{name}.php`). 480 * 481 * @link https://engineering.hmn.md/standards/style/php/#file-naming 482 * 483 * @param string $prefix Prefix to autoload from. 484 * @param string $path Path to validate. 485 */ 486 public static function register_class_path( string $prefix = '', string $path = '' ) { 487 if ( ! class_exists( 'Redux_Autoloader' ) ) { 488 require_once Redux_Path::get_path( '/inc/classes/class-redux-autoloader.php' ); 489 } 490 491 $loader = new Redux_Autoloader( $prefix, $path ); 492 493 spl_autoload_register( array( $loader, 'load' ) ); 494 } 495 496 /** 497 * Check if a string starts with a string. 498 * 499 * @param string $haystack Full string. 500 * @param string $needle String to check if it starts with. 501 * 502 * @return bool 503 */ 504 public static function string_starts_with( string $haystack, string $needle ): bool { 505 $length = strlen( $needle ); 506 return substr( $haystack, 0, $length ) === $needle; 507 } 508 509 /** 510 * Check if a string ends with a string. 511 * 512 * @param string $haystack Full string. 513 * @param string $needle String to check if it starts with. 514 * 515 * @return bool 516 */ 517 public static function string_ends_with( string $haystack, string $needle ): bool { 518 $length = strlen( $needle ); 519 520 if ( ! $length ) { 521 return true; 522 } 523 524 return substr( $haystack, -$length ) === $needle; 525 } 526 527 /** 528 * Get the url where the Admin Columns website is hosted 529 * 530 * @param string $path Path to add to url. 531 * 532 * @return string 533 */ 534 private static function get_site_url( string $path = '' ): string { 535 $url = 'https://redux.io'; 536 537 if ( ! empty( $path ) ) { 538 $url .= '/' . trim( $path, '/' ) . '/'; 539 } 540 541 return $url; 542 } 543 544 /** 545 * Url with utm tags 546 * 547 * @param string $path Path on site. 548 * @param string $utm_medium Medium var. 549 * @param string|null $utm_content Content var. 550 * @param bool $utm_campaign Campaign var. 551 * 552 * @return string 553 */ 554 public static function get_site_utm_url( string $path, string $utm_medium, string $utm_content = null, bool $utm_campaign = false ): string { 555 $url = self::get_site_url( $path ); 556 557 if ( ! $utm_campaign ) { 558 $utm_campaign = 'plugin-installation'; 559 } 560 561 $args = array( 562 // Referrer: plugin. 563 'utm_source' => 'plugin-installation', 564 565 // Specific promotions or sales. 566 'utm_campaign' => $utm_campaign, 567 568 // Marketing medium: banner, documentation or email. 569 'utm_medium' => $utm_medium, 570 571 // Used for differentiation of medium. 572 'utm_content' => $utm_content, 573 ); 574 575 $args = array_map( 'sanitize_key', array_filter( $args ) ); 576 577 return add_query_arg( $args, $url ); 578 } 579 580 /** 581 * Conversion. 582 */ 583 public static function pro_to_ext() { 584 585 // If they are a pro user, convert their key to use with Extendify. 586 $redux_pro_key = get_option( 'redux_pro_license_key' ); 587 588 if ( $redux_pro_key && ! get_user_option( 'extendifysdk_redux_key_moved' ) ) { 589 try { 590 $extendify_user_state = get_user_meta( get_current_user_id(), 'extendifysdk_user_data' ); 591 592 if ( ! isset( $extendify_user_state[0] ) ) { 593 $extendify_user_state[0] = '{}'; 594 } 595 596 $extendify_user_data = json_decode( $extendify_user_state[0], true ); 597 $extendify_user_data['state']['apiKey'] = $redux_pro_key; 598 599 update_user_meta( get_current_user_id(), 'extendifysdk_user_data', wp_json_encode( $extendify_user_data ) ); 600 } catch ( Exception $e ) { 601 // Just have it fail gracefully. 602 } 603 // Run this regardless. If the try/catch failed, better not to keep trying as something else is wrong. 604 // In that case we can expect them to come to support, and we can give them a fresh key. 605 update_user_option( get_current_user_id(), 'extendifysdk_redux_key_moved', true ); 606 } 607 } 608 } 609 }