PluginProbe
Templately – Elementor & Gutenberg Template Library: 6500+ Free & Pro Ready Templates And Cloud! / 3.6.7
Templately – Elementor & Gutenberg Template Library: 6500+ Free & Pro Ready Templates And Cloud! v3.6.7
3.7.5 3.7.4 3.7.3 3.7.2 1-final 3.7.1 3.7.0 3.6.8 3.6.7 3.6.6 3.6.5 3.6.4 3.6.3 3.6.2 3.6.1 3.0.3 3.0.4 3.0.5 3.0.6 3.0.7 3.0.8 3.0.9 3.1.0 3.1.1 3.1.10 All 111 releases
templately / includes / Utils / Helper.php

Helper.php in Templately – Elementor & Gutenberg Template Library: 6500+ Free & Pro Ready Templates And Cloud! 3.6.7, at includes/Utils/Helper.php

754 lines 22.2 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 namespace Templately\Utils;
4
5 use Elementor\Plugin;
6 use Templately\Core\Importer\Utils\Utils;
7 use WP_Error;
8 use WP_REST_Response;
9 use function get_plugins;
10 use function is_plugin_active;
11
12 /**
13 * Utility Helper for Templately
14 *
15 * This class contains some helper functions for easy access.
16 *
17 * @since 1.0.0
18 */
19 class Helper extends Base {
20 /**
21 * Check if development API should be used
22 *
23 * @return bool True if development API should be used
24 */
25 public static function is_dev_api(){
26 // Only check TEMPLATELY_DEV_API constant - no fallback mechanisms
27 return defined( 'TEMPLATELY_DEV_API' ) && constant( 'TEMPLATELY_DEV_API' );
28 }
29
30 /**
31 * Get installed WordPress Plugin List
32 * @return array
33 */
34 public static function get_plugins() {
35 if (! function_exists('get_plugins')) {
36 require_once ABSPATH . 'wp-admin/includes/plugin.php';
37 }
38 return get_plugins();
39 }
40 public static function is_plugins_installed($plugin_file) {
41 $_plugins = self::get_plugins();
42 $is_installed = isset($_plugins[$plugin_file]);
43 return $is_installed;
44 }
45
46 /**
47 * Get installed WordPress Plugin List
48 * @return boolean
49 */
50 public static function is_plugin_active($plugin) {
51 if (! function_exists('is_plugin_active')) {
52 require_once ABSPATH . 'wp-admin/includes/plugin.php';
53 }
54 return is_plugin_active($plugin);
55 }
56
57 /**
58 * Collect IP from request.
59 *
60 * Prefers REMOTE_ADDR since it cannot be spoofed by the client. When it is
61 * a private/reserved address (reverse proxy, Docker bridge gateway like
62 * 192.168.65.1, local dev), the forwarded headers are scanned for the first
63 * public IP. If nothing public is found, the request is local: 127.0.0.1.
64 *
65 * @return string
66 */
67 public static function get_ip() {
68 $remote_addr = ! empty($_SERVER['REMOTE_ADDR']) ? sanitize_text_field($_SERVER['REMOTE_ADDR']) : '';
69
70 if (self::is_public_ip($remote_addr)) {
71 return $remote_addr;
72 }
73
74 foreach (['HTTP_X_FORWARDED_FOR', 'HTTP_CLIENT_IP'] as $header) {
75 if (empty($_SERVER[$header])) {
76 continue;
77 }
78 $candidates = explode(',', sanitize_text_field($_SERVER[$header]));
79 foreach ($candidates as $candidate) {
80 $candidate = trim($candidate);
81 if (self::is_public_ip($candidate)) {
82 return $candidate;
83 }
84 }
85 }
86
87 return '127.0.0.1';
88 }
89
90 /**
91 * Check whether a string is a valid public (non-private, non-reserved) IP.
92 *
93 * @param string $ip
94 * @return bool
95 */
96 private static function is_public_ip($ip): bool {
97 return (bool) filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE);
98 }
99
100 /**
101 * Get views for front-end display
102 *
103 * @param string $name it will be file name only from the view's folder.
104 * @param array $data
105 * @return void
106 */
107 public static function views($name, $data = []) {
108 extract($data);
109 $helper = self::class;
110 $file = TEMPLATELY_PATH . 'views/' . $name . '.php';
111
112 if (is_readable($file)) {
113 include_once $file;
114 }
115 }
116
117 /**
118 * Get API URL for Templately endpoints
119 *
120 * @param string $endpoint API endpoint path (e.g., 'v2/import/pack/123')
121 * @return string Complete API URL
122 */
123 public static function get_api_url($endpoint): string {
124 $base_url = self::is_dev_api() ? 'https://app.templately.dev' : 'https://app.templately.com';
125
126 /**
127 * Filter the base URL for development API
128 *
129 * @since 3.5.0
130 * @param string $base_url The default base URL
131 */
132 $base_url = apply_filters('templately_dev_api_base_url', $base_url);
133
134 return "{$base_url}/api/{$endpoint}";
135 }
136
137 /**
138 * Make a unified API request to Templately API
139 *
140 * @param string $method HTTP method (GET or POST)
141 * @param string $api_url Complete API URL
142 * @param array $body Request body data (for POST requests)
143 * @param array $extra_headers Additional headers beyond the standard ones
144 * @param int $timeout Request timeout in seconds (default: 30)
145 * @return array|WP_Error Response array or WP_Error on failure
146 */
147 private static function make_api_request($method, $api_url, $body = [], $extra_headers = [], $timeout = 30) {
148 $api_key = Options::get_instance()->get('api_key');
149
150 $headers = [
151 'Authorization' => 'Bearer ' . $api_key,
152 'x-templately-ip' => self::get_ip(),
153 'x-templately-url' => home_url('/'),
154 'x-templately-version' => defined( 'TEMPLATELY_VERSION' ) ? constant( 'TEMPLATELY_VERSION' ) : '1.0.0',
155 // Force JSON responses so the cloud returns JSON errors instead of an HTML
156 // error page (which json_decode() cannot parse). Binary/XML downloads
157 // (zip pack, attachment WXR) use their own wp_remote_* calls and bypass
158 // this helper, so they are unaffected. Callers can override via $extra_headers.
159 'Accept' => 'application/json',
160 ];
161
162 // Add Content-Type for POST requests
163 if (strtoupper($method) === 'POST') {
164 $headers['Content-Type'] = 'application/json';
165 }
166
167 // Resolve requested platform: $_REQUEST wins (frontend-supplied), then caller's extra_headers, then default.
168 if ( isset( $_REQUEST['requested_platform'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended
169 $extra_headers['x-templately-requested-platform'] = sanitize_text_field( wp_unslash( $_REQUEST['requested_platform'] ) );
170 } elseif ( ! isset( $extra_headers['x-templately-requested-platform'] ) ) {
171 $extra_headers['x-templately-requested-platform'] = 'templately';
172 }
173
174 // Merge additional headers
175 $headers = array_merge($headers, $extra_headers);
176
177 $args = [
178 'timeout' => $timeout,
179 'headers' => $headers,
180 ];
181
182 // Apply filter to allow network admin or other functionality to modify request args
183 $args = apply_filters( 'templately_api_request_params', $args, $method, $api_url );
184
185 // Add body for POST requests
186 if (strtoupper($method) === 'POST') {
187 $args['body'] = is_array($body) ? json_encode($body) : $body;
188 }
189
190 // Make the appropriate request
191 if (strtoupper($method) === 'POST') {
192 $response = wp_remote_post($api_url, $args);
193 } else {
194 $response = wp_remote_get($api_url, $args);
195 }
196
197 // Check for verification header in the response
198 self::check_verification_header($response);
199
200
201 // Check for site disconnection in response body
202 self::check_site_disconnection($response);
203
204 return $response;
205 }
206
207 /**
208 * Make a GET request to Templately API
209 *
210 * @param string $endpoint API endpoint path (e.g., 'v2/import/pack/123')
211 * @param array $query_params Query parameters as key-value pairs
212 * @param array $extra_headers Additional headers beyond the standard ones
213 * @param int $timeout Request timeout in seconds (default: 30)
214 * @return array|WP_Error Response array or WP_Error on failure
215 */
216 public static function make_api_get_request($endpoint, $query_params = [], $extra_headers = [], $timeout = 30) {
217 $api_url = self::get_api_url($endpoint);
218
219 // Add query parameters if provided
220 if (!empty($query_params)) {
221 $api_url = add_query_arg($query_params, $api_url);
222 }
223
224 return self::make_api_request('GET', $api_url, [], $extra_headers, $timeout);
225 }
226
227 /**
228 * Make a POST request to Templately API
229 *
230 * @param string $endpoint API endpoint path (e.g., 'v2/feedback/store')
231 * @param array $body Request body data
232 * @param array $extra_headers Additional headers beyond the standard ones
233 * @param int $timeout Request timeout in seconds (default: 30)
234 * @return array|WP_Error Response array or WP_Error on failure
235 */
236 public static function make_api_post_request($endpoint, $body = [], $extra_headers = [], $timeout = 30) {
237 $api_url = self::get_api_url($endpoint);
238 return self::make_api_request('POST', $api_url, $body, $extra_headers, $timeout);
239 }
240
241 /**
242 * Sanitize Helper
243 *
244 * @param mixed $value
245 * @param string $type
246 *
247 * @return bool|string
248 */
249 public static function sanitize($value, $type = 'text') {
250 switch ($type) {
251 case 'boolean':
252 $sanitized_value = rest_sanitize_boolean($value);
253 break;
254 default:
255 $sanitized_value = sanitize_text_field($value);
256 break;
257 }
258
259 return $sanitized_value;
260 }
261
262 /**
263 * Escape a string for safe embedding inside a GraphQL or JSON string literal.
264 *
265 * GraphQL string escaping rules are identical to JSON string escaping (per the
266 * GraphQL spec), so wp_json_encode() is the authoritative escaper. We strip the
267 * outer quotes it adds and return only the escaped inner content, ready to be
268 * wrapped in your own quote pair.
269 *
270 * Handles pre-encoded JSON: when the caller has already run json_encode() +
271 * wp_slash() on a value (e.g. categories, dependencies in Items.php), the
272 * quotes are already escaped as \" and the string is ready to embed. Calling
273 * wp_json_encode() again would double-escape those backslashes. We detect this
274 * case by checking whether wp_unslash() produces valid JSON, and if so, return
275 * the value directly without further encoding.
276 *
277 * @param string $value Raw string or wp_slash(json_encode()) output.
278 * @return string Escaped string, safe to place between double quotes in GraphQL/JSON.
279 */
280 public static function esc_json_string( $value ) {
281 $value = (string) $value;
282
283 // If wp_slash() was applied to a JSON string upstream, the quotes are
284 // already escaped (e.g. {\"key\":\"val\"}). Detect this by unslashing and
285 // checking for valid JSON — if it matches, the value is already suitable
286 // for embedding in a string literal; return it as-is to avoid doubling backslashes.
287 $unslashed = wp_unslash( $value );
288 if ( $unslashed !== $value ) {
289 $decoded = json_decode( $unslashed, true );
290 if ( json_last_error() === JSON_ERROR_NONE && null !== $decoded ) {
291 return $value;
292 }
293 }
294
295 $encoded = wp_json_encode( $value );
296 // wp_json_encode wraps the value in "...", strip those outer quotes.
297 return substr( $encoded, 1, -1 );
298 }
299
300 /**
301 * Check for X-Templately-Verified header and update user verification status
302 *
303 * @param array|WP_Error $response The HTTP response array from wp_remote_get/wp_remote_post
304 * @return void
305 */
306 public static function check_verification_header($response) {
307 // Only process if response is not a WP_Error and contains headers
308 if (is_wp_error($response)) {
309 return;
310 }
311
312 // Retrieve the X-Templately-Verified header
313 $verification_header = wp_remote_retrieve_header($response, 'X-Templately-Verified');
314
315 // Check if header exists and has a truthy value
316 if (!empty($verification_header) && filter_var($verification_header, FILTER_VALIDATE_BOOLEAN)) {
317 try {
318 // Get current user data
319 $options = Options::get_instance();
320 $user = $options->get('user');
321
322 // Only update if user data exists and is not already verified
323 if (!empty($user) && is_array($user) && empty($user['is_verified'])) {
324 // Set verification flag
325 $user['is_verified'] = true;
326
327 // Save updated user data
328 $options->set('user', $user);
329
330 }
331
332 if (!empty($user['is_verified'])){
333 if(!headers_sent()){
334 header( 'X-Templately-Verified: true' );
335 if (defined('TEMPLATELY_DEBUG_LOG') && constant('TEMPLATELY_DEBUG_LOG')) {
336 self::log('User verification status already updated via X-Templately-Verified header');
337 }
338 }
339
340 return true;
341 }
342 } catch (\Exception $e) {
343 // Log error if debug logging is enabled
344 if (defined('TEMPLATELY_DEBUG_LOG') && constant('TEMPLATELY_DEBUG_LOG')) {
345 self::log('Error updating user verification status: ' . $e->getMessage());
346 }
347 }
348 }
349
350 return false;
351 }
352
353 /**
354 * Check for site disconnection status in API response body
355 *
356 * Detects SiteNotConnected errors and updates user disconnection status.
357 * Sends X-Templately-Disconnected header for frontend detection.
358 *
359 *
360 * @param array|WP_Error|mixed $response The response object or body array
361 * @return bool True if site is disconnected, false otherwise
362 */
363 public static function check_site_disconnection($response) {
364 if (is_wp_error($response)) {
365 return false;
366 }
367
368 $response_body = $response;
369
370 // If it's a raw WP response array with body, decode it
371 if (is_array($response) && isset($response['body']) && is_string($response['body'])) {
372 $response_body = json_decode(wp_remote_retrieve_body($response), true);
373 }
374
375 // Check if response body indicates site disconnection
376 if (!is_array($response_body)) {
377 return false;
378 }
379
380 $status = $response_body['status'] ?? null;
381 $status_text = $response_body['statusText'] ?? null;
382
383 // Check for SiteNotConnected error
384 if ($status === 'error' && $status_text === 'SiteNotConnected') {
385 try {
386 // Get current user data
387 $options = Options::get_instance();
388 $user = $options->get('user');
389
390 // Only update if user data exists
391 if (!empty($user) && is_array($user)) {
392 // Set disconnection flag
393 $user['is_disconnected'] = true;
394
395 // Save updated user data
396 $options->set('user', $user);
397
398 if (defined('TEMPLATELY_DEBUG_LOG') && constant('TEMPLATELY_DEBUG_LOG')) {
399 self::log('Site disconnection detected: SiteNotConnected status');
400 }
401 }
402
403 // Send header for frontend detection
404 if (!headers_sent()) {
405 header('X-Templately-Disconnected: true');
406 }
407
408 return true;
409 } catch (\Exception $e) {
410 // Log error if debug logging is enabled
411 if (defined('TEMPLATELY_DEBUG_LOG') && constant('TEMPLATELY_DEBUG_LOG')) {
412 self::log('Error updating site disconnection status: ' . $e->getMessage());
413 }
414 }
415 }
416
417 return false;
418 }
419
420 /**
421 * Clear site disconnection status
422 *
423 * Called after successful site migration to reset the disconnection flag.
424 *
425 * @return void
426 */
427 public static function clear_site_disconnection() {
428 try {
429 $options = Options::get_instance();
430 $user = $options->get('user');
431
432 if (!empty($user) && is_array($user)) {
433 $user['site_url'] = base64_encode( home_url('/') );
434 $user['is_disconnected'] = false;
435 $options->set('user', $user);
436
437 if (defined('TEMPLATELY_DEBUG_LOG') && constant('TEMPLATELY_DEBUG_LOG')) {
438 self::log('Site disconnection status cleared and URL updated.');
439 }
440 }
441 } catch (\Exception $e) {
442 if (defined('TEMPLATELY_DEBUG_LOG') && constant('TEMPLATELY_DEBUG_LOG')) {
443 self::log('Error clearing site disconnection status: ' . $e->getMessage());
444 }
445 }
446 }
447
448 /**
449 * API Error Formatter
450 *
451 * @param int $error_code
452 * @param mixed $error_message
453 * @param string $endpoint
454 * @param integer $status
455 * @param array $additional_data
456 * @return WP_Error
457 */
458 public static function error($error_code, $error_message, $endpoint = '', $status = 500, $additional_data = []) {
459 $additional_data['status'] = $status;
460 if (! empty($endpoint)) {
461 $additional_data['endpoint'] = $endpoint;
462 }
463 // Add browser padding to avoid browsers not serving small JSON responses
464 $padding_length = 512;
465 $additional_data['browser_padding'] = str_repeat(' ', $padding_length);
466
467 return new WP_Error($error_code, $error_message, $additional_data);
468 }
469
470 /**
471 * API Response Formatter
472 *
473 * @param mixed $data
474 * @return WP_REST_Response
475 */
476 public static function success($data) {
477 return new WP_REST_Response($data, 200);
478 }
479
480 /**
481 * Normalize Favourites Data
482 *
483 * @param array $favourites
484 * @param array $_favourites
485 * @param boolean $undo
486 *
487 * @return array
488 */
489 public function normalizeFavourites($favourites, $_favourites = [], $undo = false) {
490 if ($undo) {
491 $_favourites[$favourites['type']] = array_values(array_filter($_favourites[$favourites['type']], function ($item) use ($favourites) {
492 return $item != $favourites['id'];
493 }));
494 return $_favourites;
495 }
496
497 array_map(function ($item) use (&$_favourites) {
498 if (! is_null($item)) {
499 $item = (array) $item;
500 if (isset($_favourites[$item['type']])) {
501 $_favourites[$item['type']][] = $item['id'];
502 } else {
503 $_favourites[$item['type']] = [$item['id']];
504 }
505 }
506 return $_favourites;
507 }, $favourites);
508
509 return $_favourites;
510 }
511
512 public function normalizeReviews($favourites, $_favourites = [], $undo = false) {
513 array_map(function ($item) use (&$_favourites) {
514 if (! is_null($item)) {
515 $item = (array) $item;
516 if (!isset($_favourites[$item['type']])) {
517 $_favourites[$item['type']] = [];
518 }
519 $_favourites[$item['type']][$item['type_id']] = $item['rating'];
520 }
521 return $_favourites;
522 }, $favourites);
523
524 return $_favourites;
525 }
526
527 /**
528 * Trigger Error
529 *
530 * @param object $triggered_by
531 * @return void
532 */
533 public static function trigger_error($triggered_by, $method = 'get_instance') {
534 $class = get_class($triggered_by);
535 $trace = debug_backtrace(); // phpcs:ignore PHPCompatibility.FunctionUse.ArgumentFunctionsReportCurrentValue.NeedsInspection
536 $file = $trace[0]['file'];
537 $line = $trace[0]['line'];
538 trigger_error("Call to undefined method $class::$method() in $file on line $line", E_USER_ERROR);
539 }
540
541 /**
542 * Printing Error Logs in debug.log file.
543 *
544 * @param mixed $log The data to log
545 * @param string $context Optional context for categorizing log entries
546 * @param string $level Optional log level (debug, info, warning, error)
547 * @return void
548 */
549 public static function log($log, $context = '', $level = 'info') {
550 // Allow complete override of logging behavior
551 $override_result = apply_filters('templately_log_override', null, $log, $context, $level);
552 if ($override_result !== null) {
553 return;
554 }
555
556 // Only log if WP_DEBUG_LOG is enabled
557 if (!defined('WP_DEBUG_LOG') || !WP_DEBUG_LOG) {
558 return;
559 }
560
561 // Format the log message
562 $formatted_message = self::format_log_message($log, $context, $level);
563
564 // Write to error log
565 error_log($formatted_message);
566 }
567
568 /**
569 * Format log message with context and level
570 *
571 * @param mixed $log The data to log
572 * @param string $context Context for categorizing log entries
573 * @param string $level Log level
574 * @return string Formatted log message
575 */
576 private static function format_log_message($log, $context = '', $level = 'info') {
577 // Convert arrays and objects to readable format
578 if (is_array($log) || is_object($log)) {
579 $log_content = print_r($log, true);
580 } else {
581 $log_content = (string) ($log ?: '');
582 }
583
584 // Build the formatted message
585 $timestamp = current_time('Y-m-d H:i:s');
586 $level_upper = strtoupper($level);
587
588 if (!empty($context)) {
589 return "[{$timestamp}] [{$level_upper}] [{$context}] {$log_content}";
590 } else {
591 return "[{$timestamp}] [{$level_upper}] {$log_content}";
592 }
593 }
594
595 public static function should_flush() {
596 if (isset($_REQUEST['is_lightspeed']) && $_REQUEST['is_lightspeed'] === 'true') {
597 return false;
598 }
599 return (!defined('TEMPLATELY_IGNORE_FLUSH_ALL') || !TEMPLATELY_IGNORE_FLUSH_ALL) && strpos($_SERVER['SERVER_SOFTWARE'], 'LiteSpeed') === false;
600 }
601
602 public static function get_block_by_name($blocks, $search) {
603 $queue = $blocks;
604
605 while (!empty($queue)) {
606 $current_block = array_shift($queue);
607
608 if ($search === $current_block['blockName']) {
609 return $current_block;
610 }
611
612 if (isset($current_block['innerBlocks'])) {
613 // Add nested blocks to the end of the queue for processing
614 $queue = array_merge($queue, $current_block['innerBlocks']);
615 }
616 }
617
618 return false;
619 }
620
621 /**
622 * Only checks if user can install/activate plugins
623 *
624 * @param [type] $cap
625 * @param [type] ...$args
626 * @return void
627 */
628 public static function current_user_can($cap, ...$args) {
629 $user = wp_get_current_user();
630
631 // Multisite super admin has all caps by definition, Unless specifically denied.
632 if (is_multisite() && is_super_admin($user->ID)) {
633 return true;
634 }
635
636 $caps = map_meta_cap($cap, $user->ID, ...$args);
637
638 switch ($cap) {
639 case 'install_plugins':
640 case 'upload_plugins':
641 $caps = ['install_plugins'];
642 break;
643 case 'install_themes':
644 case 'upload_themes':
645 $caps = ['install_themes'];
646 break;
647 case 'activate_plugins':
648 case 'deactivate_plugins':
649 case 'activate_plugin':
650 case 'deactivate_plugin':
651 $caps = ['activate_plugins'];
652 break;
653 default:
654 break;
655 }
656
657 // Maintain BC for the argument passed to the "user_has_cap" filter.
658 $args = array_merge(array($cap, $user->ID), $args);
659
660 /**
661 * See WP_User::has_cap() for description.
662 */
663 $capabilities = apply_filters('user_has_cap', $user->allcaps, $caps, $args, $user);
664
665 // Everyone is allowed to exist.
666 $capabilities['exist'] = true;
667
668 // Nobody is allowed to do things they are not allowed to do.
669 unset($capabilities['do_not_allow']);
670
671 // Must have ALL requested caps.
672 foreach ((array) $caps as $cap) {
673 if (empty($capabilities[$cap])) {
674 return false;
675 }
676 }
677
678 return true;
679 }
680
681 /**
682 * Calculates the elapsed time and checks if it is close to the maximum execution time.
683 * Returns true if the script should exit to avoid exceeding the limit.
684 *
685 * @return bool True if the script should exit, false otherwise.
686 */
687 public static function fsi_should_exit() {
688 if (defined('TEMPLATELY_START_TIME') && ini_get('max_execution_time')) {
689 $max_time = ini_get('max_execution_time');
690 $elapsed = microtime(true) - TEMPLATELY_START_TIME;
691 $delay = max(5, $max_time * 20 / 100);
692
693 // Check if elapsed time is close to max execution time
694 if ($max_time - $elapsed <= $delay) {
695 return ['max_time' => $max_time, 'elapsed' => $elapsed, 'delay' => $delay];
696 }
697 }
698 return false;
699 }
700
701 /**
702 * Enable Elementor Container
703 * This function will enable the Elementor Container feature.
704 * Without this feature, some of the templates may not work properly.
705 *
706 * @return boolean
707 */
708 public static function enable_elementor_container() {
709 if (class_exists('Elementor\Plugin')) {
710 $control_name = Plugin::instance()->experiments->get_feature_option_key('container');
711 if (get_option($control_name) !== 'active') {
712 update_option($control_name, 'active');
713 return true;
714 }
715 }
716 return false;
717 }
718
719 /**
720 * Undocumented function
721 *
722 * @param [type] $args
723 * @param [type] $defaults
724 * @return array
725 */
726 public static function recursive_wp_parse_args($args, $defaults) {
727 $args = (array) $args;
728 $defaults = (array) $defaults;
729 $r = $defaults;
730 foreach ($args as $key => $value) {
731 if (is_array($value) && isset($r[$key])) {
732 // also handle numeric array. if both $value and $r[ $key ] are numeric array. wp_is_numeric_array()
733 if (wp_is_numeric_array($value) && wp_is_numeric_array($r[$key])) {
734 foreach ($value as $k => $v) {
735 if (!in_array($v, $r[$key])) {
736 if (!isset($r[$key][$k])) {
737 $r[$key][$k] = $v;
738 } else {
739 $r[$key][] = $v;
740 }
741 }
742 }
743 } else {
744 $r[$key] = self::recursive_wp_parse_args($value, $r[$key]);
745 }
746 } else {
747 $r[$key] = $value;
748 }
749 }
750 return $r;
751 }
752
753 }
754