| @@ -837,9 +837,9 @@ | ||
| 837 | 837 | $email = isset( $_POST['email'] ) ? strtolower( sanitize_email( wp_unslash( $_POST['email'] ) ) ) : false; |
| 838 | 838 | |
| 839 | 839 | /**Condtion user for user logout & expire token*/ |
| 840 | 840 | if ( 'Token is Expired' === $status || 'Authorization Token not found' === $status ) { |
| 841 | - delete_transient( 'wdkit_auth_' . $email ); | |
| 841 | + delete_transient( 'wdkit_auth_' . wdesignkit_cloud_session_key( $email ) ); | |
| 842 | 842 | // Clear stored license data when token expires so banner shows again |
| 843 | 843 | delete_option( 'wdkit_licence_data' ); |
| 844 | 844 | } |
| 845 | 845 | |
| @@ -851,9 +851,9 @@ | ||
| 851 | 851 | if ( ! empty( $response['credits']['wdkit_licence'] ) && is_array( $response['credits']['wdkit_licence'] ) ) { |
| 852 | 852 | $wdkit_licence = $response['credits']['wdkit_licence']; |
| 853 | 853 | // Handle serialized data |
| 854 | 854 | if ( is_string( $wdkit_licence ) && is_serialized( $wdkit_licence ) ) { |
| 855 | - $wdkit_licence = unserialize( $wdkit_licence ); | |
| 855 | + $wdkit_licence = unserialize( $wdkit_licence, array( 'allowed_classes' => false ) ); | |
| 856 | 856 | } |
| 857 | 857 | if ( ! empty( $wdkit_licence ) && is_array( $wdkit_licence ) ) { |
| 858 | 858 | update_option( 'wdkit_licence_data', $wdkit_licence ); |
| 859 | 859 | } |
| @@ -884,8 +884,16 @@ | ||
| 884 | 884 | $credits = ! empty( $response['credits']['widget_limit']['meta_value'] ) ? $response['credits']['widget_limit']['meta_value'] : 10; |
| 885 | 885 | $server_list = ! empty( $response['widgettemplate'] ) ? $response['widgettemplate'] : array(); |
| 886 | 886 | $db_builder_list = ! empty( $response['widgetbuilder'] ) ? $response['widgetbuilder'] : array(); |
| 887 | 887 | |
| 888 | + // Whether this call actually carried the server widget list that activation state is | |
| 889 | + // derived from. Captured before the loops below, which unset() matched $server_list | |
| 890 | + // entries as they go. wdkit_meta_data() calls this method with array(), and without | |
| 891 | + // this flag that call rebuilt $db_widget from local widgets only — every one of which | |
| 892 | + // is forced 'active' further down — and then wrote the empty result over | |
| 893 | + // wkit_deactivate_widgets, erasing every deactivation the user had made. | |
| 894 | + $has_server_widgets = ! empty( $server_list ); | |
| 895 | + | |
| 888 | 896 | $placeholderimg = WDKIT_URL . 'assets/images/placeholder.jpg'; |
| 889 | 897 | |
| 890 | 898 | $local_list = $this->wdkit_get_local_widgets(); |
| 891 | 899 | |
| @@ -971,13 +979,31 @@ | ||
| 971 | 979 | ); |
| 972 | 980 | } |
| 973 | 981 | } |
| 974 | 982 | |
| 975 | - $get_db_widget = get_option( 'wkit_deactivate_widgets', array() ); | |
| 976 | - if ( empty( $get_db_widget ) ) { | |
| 977 | - add_option( 'wkit_deactivate_widgets', $db_widget, '', 'yes' ); | |
| 978 | - } else { | |
| 979 | - update_option( 'wkit_deactivate_widgets', $db_widget ); | |
| 983 | + // Only persist activation state when the server list it is derived from was actually | |
| 984 | + // supplied. See $has_server_widgets above. | |
| 985 | + if ( $has_server_widgets ) { | |
| 986 | + // update_option() creates the row when it is missing, so it covers both cases. | |
| 987 | + // The previous add_option()/update_option() split was chosen on empty( $option ), | |
| 988 | + // but wdkit_db_widgetlist() creates this row as an empty array on every install — | |
| 989 | + // so the empty branch ran while the row already existed, and add_option() is a | |
| 990 | + // no-op for an existing option. Deactivating from the My Widgets screen was | |
| 991 | + // therefore silently discarded on effectively every site. Autoload stays 'yes', | |
| 992 | + // matching the original add_option() call and wdkit_db_widgetlist(). | |
| 993 | + update_option( 'wkit_deactivate_widgets', $db_widget, 'yes' ); | |
| 994 | + | |
| 995 | + // The cached widget registry bakes in wkit_deactivate_widgets membership and is | |
| 996 | + // stored as a no-expiry transient, so it never self-heals. Without this the | |
| 997 | + // loaders kept registering a widget the user had just switched off (and kept | |
| 998 | + // hiding one they had switched back on) until the transient was flushed by hand. | |
| 999 | + // The write above is not per-builder — one save can change any builder's set, and | |
| 1000 | + // a widget can move between builders — so clear all four. | |
| 1001 | + if ( function_exists( 'wdesignkit_invalidate_widget_registry' ) ) { | |
| 1002 | + foreach ( array( 'elementor', 'gutenberg', 'gutenberg_core', 'bricks' ) as $builder_slug ) { | |
| 1003 | + wdesignkit_invalidate_widget_registry( $builder_slug ); | |
| 1004 | + } | |
| 1005 | + } | |
| 980 | 1006 | } |
| 981 | 1007 | |
| 982 | 1008 | return $final; |
| 983 | 1009 | } |
| @@ -1114,8 +1140,24 @@ | ||
| 1114 | 1140 | } |
| 1115 | 1141 | |
| 1116 | 1142 | $response = WDesignKit_Data_Query::get_data( 'save_template', $args ); |
| 1117 | 1143 | |
| 1144 | + /** | |
| 1145 | + * The cloud call can come back as a WP_Error (timeout, DNS, refused) or with an | |
| 1146 | + * empty / unparsable body, which json_decode()s to null. Forwarding that as-is | |
| 1147 | + * makes admin-ajax answer with a literal `null` that the editor then reads | |
| 1148 | + * `.id` off, killing the whole app. Normalise it to the failure shape used above. | |
| 1149 | + */ | |
| 1150 | + if ( is_wp_error( $response ) || ! is_array( $response ) ) { | |
| 1151 | + $response = array( | |
| 1152 | + 'id' => 0, | |
| 1153 | + 'editpage' => '', | |
| 1154 | + 'message' => esc_html__( 'Template Not Saved !', 'wdesignkit' ), | |
| 1155 | + 'description' => is_wp_error( $response ) ? $response->get_error_message() : esc_html__( 'Could not reach the WDesignKit server. Please try again.', 'wdesignkit' ), | |
| 1156 | + 'success' => false, | |
| 1157 | + ); | |
| 1158 | + } | |
| 1159 | + | |
| 1118 | 1160 | wp_send_json( $response ); |
| 1119 | 1161 | wp_die(); |
| 1120 | 1162 | } |
| 1121 | 1163 | |
| @@ -1139,9 +1181,11 @@ | ||
| 1139 | 1181 | 'success' => false, |
| 1140 | 1182 | ); |
| 1141 | 1183 | } else { |
| 1142 | 1184 | $temp_content = str_replace( '\\', '', $temp_content ); |
| 1143 | - $temp_content = wp_remote_get( $temp_content )['body']; | |
| 1185 | + // SSRF guard (CWE-918): validate the resolved host before fetching a caller-supplied URL. | |
| 1186 | + $fetched = wdesignkit_safe_remote_get( $temp_content ); | |
| 1187 | + $temp_content = is_wp_error( $fetched ) ? '' : wp_remote_retrieve_body( $fetched ); | |
| 1144 | 1188 | $temp_content = base64_encode( $temp_content ); |
| 1145 | 1189 | |
| 1146 | 1190 | $args = array( |
| 1147 | 1191 | 'token' => $token, |
| @@ -1237,8 +1281,235 @@ | ||
| 1237 | 1281 | * Get Elementor Global color and Typography. |
| 1238 | 1282 | * |
| 1239 | 1283 | * @since 1.1.16 |
| 1240 | 1284 | */ |
| 1285 | + /** | |
| 1286 | + * Kit settings holding The Plus Addons' own globals. | |
| 1287 | + * | |
| 1288 | + * These sit in the Elementor kit's `_elementor_page_settings` alongside Elementor's | |
| 1289 | + * system_colors / system_typography, but the save flow only ever collected the four | |
| 1290 | + * Elementor keys. Widgets reference an entry in these lists by its `_id` through a | |
| 1291 | + * `tp_global_preset` setting, so a template saved without them travels with the | |
| 1292 | + * reference but not the definition - which is why imported sections come in missing | |
| 1293 | + * their button styling, radii and shadows. | |
| 1294 | + * | |
| 1295 | + * @since 2.6.4 | |
| 1296 | + * | |
| 1297 | + * @return array Kit setting keys. | |
| 1298 | + */ | |
| 1299 | + private function wdkit_tp_global_kit_keys() { | |
| 1300 | + return array( | |
| 1301 | + 'tp_global_button_style_list', | |
| 1302 | + 'tp_global_dimensions_list', | |
| 1303 | + 'tp_global_box_shadow_list', | |
| 1304 | + 'tp_global_gradient_list', | |
| 1305 | + 'tp_global_gsap_list', | |
| 1306 | + 'tp_global_scroll_animation_list', | |
| 1307 | + 'tp_text_global_gsap_list', | |
| 1308 | + 'tp_image_global_gsap_list', | |
| 1309 | + ); | |
| 1310 | + } | |
| 1311 | + | |
| 1312 | + /** | |
| 1313 | + * Merge incoming Plus globals into the active kit, keyed by `_id`. | |
| 1314 | + * | |
| 1315 | + * Entries are matched on their `_id`, never on position: an existing entry is always | |
| 1316 | + * left as it is, and only genuinely new ones are appended. That matters because | |
| 1317 | + * widgets - and the entries themselves, a button style points at dimension and shadow | |
| 1318 | + * entries - resolve by `_id`. Renumbering or overwriting would repoint references on | |
| 1319 | + * the destination site's own content. | |
| 1320 | + * | |
| 1321 | + * @since 2.6.4 | |
| 1322 | + * | |
| 1323 | + * @param array $incoming Lists captured with the template. | |
| 1324 | + * @return bool True when the kit was changed. | |
| 1325 | + */ | |
| 1326 | + /** | |
| 1327 | + * Global colour / typography ids this site already defines. | |
| 1328 | + * | |
| 1329 | + * @since 2.6.4 | |
| 1330 | + * | |
| 1331 | + * @param array $kit_meta Kit `_elementor_page_settings`. | |
| 1332 | + * @return array{color:array<string,bool>,typography:array<string,bool>} | |
| 1333 | + */ | |
| 1334 | + private function wdkit_known_global_ids( $kit_meta ) { | |
| 1335 | + $known = array( | |
| 1336 | + 'color' => array(), | |
| 1337 | + 'typography' => array(), | |
| 1338 | + ); | |
| 1339 | + | |
| 1340 | + $sources = array( | |
| 1341 | + 'color' => array( 'system_colors', 'custom_colors' ), | |
| 1342 | + 'typography' => array( 'system_typography', 'custom_typography' ), | |
| 1343 | + ); | |
| 1344 | + | |
| 1345 | + foreach ( $sources as $kind => $keys ) { | |
| 1346 | + foreach ( $keys as $key ) { | |
| 1347 | + if ( empty( $kit_meta[ $key ] ) || ! is_array( $kit_meta[ $key ] ) ) { | |
| 1348 | + continue; | |
| 1349 | + } | |
| 1350 | + | |
| 1351 | + foreach ( $kit_meta[ $key ] as $entry ) { | |
| 1352 | + if ( ! empty( $entry['_id'] ) ) { | |
| 1353 | + $known[ $kind ][ $entry['_id'] ] = true; | |
| 1354 | + } | |
| 1355 | + } | |
| 1356 | + } | |
| 1357 | + } | |
| 1358 | + | |
| 1359 | + return $known; | |
| 1360 | + } | |
| 1361 | + | |
| 1362 | + /** | |
| 1363 | + * Make one incoming Plus global's colour / font references resolvable here. | |
| 1364 | + * | |
| 1365 | + * A Plus global can point at an Elementor global: the "Primary Button" entry holds | |
| 1366 | + * `__globals__: { text_color: "globals/colors?id=72e09b4", … }`, which The Plus Addons | |
| 1367 | + * turns into `var(--e-global-color-72e09b4)`. Elementor only emits that variable for ids | |
| 1368 | + * present in the kit, so on a site without `72e09b4` the button renders with no colour. | |
| 1369 | + * | |
| 1370 | + * Two cases, and the difference is deliberate: | |
| 1371 | + * | |
| 1372 | + * - The site ALREADY defines that id — leave the reference alone. The button then picks | |
| 1373 | + * up the destination's own colour, which is the point of a global. Their palette is | |
| 1374 | + * never read from or written to beyond this check. | |
| 1375 | + * - The site does NOT define it — write the captured value straight into the entry and | |
| 1376 | + * drop the reference, so it renders as designed. | |
| 1377 | + * | |
| 1378 | + * Nothing is ever added to the user's global colours or fonts. An earlier version injected | |
| 1379 | + * the missing definitions into their palette, which made the reference resolve but grew | |
| 1380 | + * their Site Settings by every colour an imported template happened to use. | |
| 1381 | + * | |
| 1382 | + * @since 2.6.4 | |
| 1383 | + * | |
| 1384 | + * @param array $entry One repeater entry. | |
| 1385 | + * @param array $refs Definitions captured with the template. | |
| 1386 | + * @param array $known Ids this site defines, from wdkit_known_global_ids(). | |
| 1387 | + * @return array Entry, with unresolvable references replaced by their values. | |
| 1388 | + */ | |
| 1389 | + private function wdkit_resolve_entry_globals( $entry, $refs, $known ) { | |
| 1390 | + if ( empty( $entry['__globals__'] ) || ! is_array( $entry['__globals__'] ) ) { | |
| 1391 | + return $entry; | |
| 1392 | + } | |
| 1393 | + | |
| 1394 | + foreach ( $entry['__globals__'] as $control => $ref ) { | |
| 1395 | + if ( ! is_string( $ref ) || false === strpos( $ref, 'id=' ) ) { | |
| 1396 | + continue; | |
| 1397 | + } | |
| 1398 | + | |
| 1399 | + if ( false !== strpos( $ref, 'globals/colors' ) ) { | |
| 1400 | + $kind = 'color'; | |
| 1401 | + } elseif ( false !== strpos( $ref, 'globals/typography' ) ) { | |
| 1402 | + $kind = 'typography'; | |
| 1403 | + } else { | |
| 1404 | + continue; | |
| 1405 | + } | |
| 1406 | + | |
| 1407 | + $id = substr( $ref, strpos( $ref, 'id=' ) + 3 ); | |
| 1408 | + if ( '' === $id || isset( $known[ $kind ][ $id ] ) ) { | |
| 1409 | + // Defined here already — their value wins. | |
| 1410 | + continue; | |
| 1411 | + } | |
| 1412 | + | |
| 1413 | + $definition = null; | |
| 1414 | + foreach ( ( $refs[ $kind ] ?? array() ) as $candidate ) { | |
| 1415 | + if ( is_array( $candidate ) && ( $candidate['_id'] ?? '' ) === $id ) { | |
| 1416 | + $definition = $candidate; | |
| 1417 | + break; | |
| 1418 | + } | |
| 1419 | + } | |
| 1420 | + | |
| 1421 | + if ( null === $definition ) { | |
| 1422 | + // Nothing captured for it, so leave the reference rather than blank the field. | |
| 1423 | + continue; | |
| 1424 | + } | |
| 1425 | + | |
| 1426 | + if ( 'color' === $kind ) { | |
| 1427 | + if ( empty( $definition['color'] ) ) { | |
| 1428 | + continue; | |
| 1429 | + } | |
| 1430 | + | |
| 1431 | + $entry[ $control ] = $definition['color']; | |
| 1432 | + } else { | |
| 1433 | + // A typography global expands into its own set of controls: the reference is | |
| 1434 | + // held under e.g. `typography_typography`, and each definition key replaces | |
| 1435 | + // that suffix — `typography_font_family`, `typography_font_weight`, and so on. | |
| 1436 | + foreach ( $definition as $def_key => $def_value ) { | |
| 1437 | + if ( '_id' === $def_key || 'title' === $def_key ) { | |
| 1438 | + continue; | |
| 1439 | + } | |
| 1440 | + | |
| 1441 | + $entry[ str_replace( 'typography_typography', $def_key, $control ) ] = $def_value; | |
| 1442 | + } | |
| 1443 | + } | |
| 1444 | + | |
| 1445 | + unset( $entry['__globals__'][ $control ] ); | |
| 1446 | + } | |
| 1447 | + | |
| 1448 | + return $entry; | |
| 1449 | + } | |
| 1450 | + | |
| 1451 | + private function wdkit_merge_tp_globals( $incoming, $refs = array() ) { | |
| 1452 | + if ( empty( $incoming ) || ! is_array( $incoming ) ) { | |
| 1453 | + return false; | |
| 1454 | + } | |
| 1455 | + | |
| 1456 | + $kit_id = get_option( 'elementor_active_kit' ); | |
| 1457 | + if ( empty( $kit_id ) ) { | |
| 1458 | + return false; | |
| 1459 | + } | |
| 1460 | + | |
| 1461 | + $kit_meta = get_post_meta( $kit_id, '_elementor_page_settings', true ); | |
| 1462 | + if ( ! is_array( $kit_meta ) ) { | |
| 1463 | + $kit_meta = array(); | |
| 1464 | + } | |
| 1465 | + | |
| 1466 | + // Which global ids this site already defines. The Plus Addons turns a reference into | |
| 1467 | + // var(--e-global-color-<_id>), and Elementor only emits that variable for ids in the | |
| 1468 | + // kit — so a reference the destination does not define resolves to nothing at all. | |
| 1469 | + $known = $this->wdkit_known_global_ids( $kit_meta ); | |
| 1470 | + | |
| 1471 | + $changed = false; | |
| 1472 | + | |
| 1473 | + foreach ( $this->wdkit_tp_global_kit_keys() as $key ) { | |
| 1474 | + if ( empty( $incoming[ $key ] ) || ! is_array( $incoming[ $key ] ) ) { | |
| 1475 | + continue; | |
| 1476 | + } | |
| 1477 | + | |
| 1478 | + $existing = ( ! empty( $kit_meta[ $key ] ) && is_array( $kit_meta[ $key ] ) ) ? $kit_meta[ $key ] : array(); | |
| 1479 | + | |
| 1480 | + $seen = array(); | |
| 1481 | + foreach ( $existing as $entry ) { | |
| 1482 | + if ( ! empty( $entry['_id'] ) ) { | |
| 1483 | + $seen[ $entry['_id'] ] = true; | |
| 1484 | + } | |
| 1485 | + } | |
| 1486 | + | |
| 1487 | + foreach ( $incoming[ $key ] as $entry ) { | |
| 1488 | + if ( ! is_array( $entry ) || empty( $entry['_id'] ) || isset( $seen[ $entry['_id'] ] ) ) { | |
| 1489 | + continue; | |
| 1490 | + } | |
| 1491 | + | |
| 1492 | + // Only ever rewrite the entry being added — never one already in the kit. | |
| 1493 | + $existing[] = $this->wdkit_resolve_entry_globals( $entry, $refs, $known ); | |
| 1494 | + $seen[ $entry['_id'] ] = true; | |
| 1495 | + $changed = true; | |
| 1496 | + } | |
| 1497 | + | |
| 1498 | + $kit_meta[ $key ] = array_values( $existing ); | |
| 1499 | + } | |
| 1500 | + | |
| 1501 | + if ( $changed ) { | |
| 1502 | + update_post_meta( $kit_id, '_elementor_page_settings', $kit_meta ); | |
| 1503 | + | |
| 1504 | + // Writing kit meta directly does not rebuild the kit stylesheet, so the | |
| 1505 | + // merged globals would never reach the frontend. | |
| 1506 | + $this->wdkit_regenerate_elementor_kit_css(); | |
| 1507 | + } | |
| 1508 | + | |
| 1509 | + return $changed; | |
| 1510 | + } | |
| 1511 | + | |
| 1241 | 1512 | protected function wdkit_get_global_val() { |
| 1242 | 1513 | |
| 1243 | 1514 | $builder = isset( $_POST['builder'] ) ? strtolower( sanitize_text_field( $_POST['builder'] ) ) : ''; |
| 1244 | 1515 | |
| @@ -2521,34 +2792,56 @@ | ||
| 2521 | 2792 | $error_message = $response->get_error_message(); |
| 2522 | 2793 | |
| 2523 | 2794 | $result = $this->tpae_set_response( false, 'oops', 'oops', '' ); |
| 2524 | 2795 | } else { |
| 2525 | - $theme_info = unserialize( $response['body'] ); | |
| 2796 | + // api.wordpress.org's theme_information response is a serialized stdClass | |
| 2797 | + // (accessed below via ->name / ->download_link). allowed_classes => false | |
| 2798 | + // blocks stdClass too, turning it into an __PHP_Incomplete_Class whose | |
| 2799 | + // properties silently don't exist — allow only stdClass, still refusing any | |
| 2800 | + // other (potentially dangerous) class the payload might reference. | |
| 2801 | + $theme_info = unserialize( $response['body'], array( 'allowed_classes' => array( 'stdClass' ) ) ); | |
| 2526 | 2802 | $theme_name = $theme_info->name; |
| 2527 | 2803 | $theme_zip_url = $theme_info->download_link; |
| 2528 | 2804 | |
| 2529 | - global $wp_filesystem; | |
| 2530 | - // Install the theme | |
| 2531 | - $theme = wp_remote_get( $theme_zip_url, array( 'timeout' => 30 ) ); | |
| 2805 | + // SSRF guard (CWE-918): validate the resolved host before fetching the ZIP | |
| 2806 | + // referenced by the external theme_info response. | |
| 2807 | + if ( ! wdesignkit_validate_external_url( $theme_zip_url ) ) { | |
| 2808 | + return array( | |
| 2809 | + 'message' => esc_html__( 'Theme Not Activated !', 'wdesignkit' ), | |
| 2810 | + 'description' => esc_html__( 'The theme package URL is not allowed.', 'wdesignkit' ), | |
| 2811 | + 'status' => 'inactive', | |
| 2812 | + 'success' => false, | |
| 2813 | + ); | |
| 2814 | + } | |
| 2532 | 2815 | |
| 2533 | 2816 | if ( ! function_exists( 'WP_Filesystem' ) ) { |
| 2534 | 2817 | require_once wp_normalize_path( ABSPATH . '/wp-admin/includes/file.php' ); |
| 2535 | 2818 | } |
| 2536 | 2819 | |
| 2820 | + require_once wp_normalize_path( ABSPATH . '/wp-admin/includes/class-wp-upgrader.php' ); | |
| 2821 | + require_once wp_normalize_path( ABSPATH . '/wp-admin/includes/theme.php' ); | |
| 2822 | + | |
| 2537 | 2823 | WP_Filesystem(); |
| 2538 | 2824 | |
| 2539 | 2825 | $active_theme = wp_get_theme(); |
| 2540 | 2826 | $theme_name = $active_theme->get( 'Name' ); |
| 2541 | 2827 | |
| 2542 | - $wp_filesystem->put_contents( WP_CONTENT_DIR . '/themes/' . $theme_slug . '.zip', $theme['body'] ); | |
| 2543 | - $zip = new ZipArchive(); | |
| 2544 | - if ( $zip->open( WP_CONTENT_DIR . '/themes/' . $theme_slug . '.zip' ) === true ) { | |
| 2545 | - $zip->extractTo( WP_CONTENT_DIR . '/themes/' ); | |
| 2546 | - $zip->close(); | |
| 2828 | + // Install via WordPress core's Theme_Upgrader instead of manually fetching and | |
| 2829 | + // ZipArchive::extractTo()'ing the remote package: core already performs the | |
| 2830 | + // standard download -> unpack -> validate-package-structure -> move-into-place | |
| 2831 | + // flow (including cleanup on failure) used for every trusted theme install. | |
| 2832 | + $upgrader = new Theme_Upgrader( new Automatic_Upgrader_Skin() ); | |
| 2833 | + $install = $upgrader->install( $theme_zip_url ); | |
| 2834 | + | |
| 2835 | + if ( is_wp_error( $install ) || ! $install ) { | |
| 2836 | + return array( | |
| 2837 | + 'message' => esc_html__( 'Theme Not Activated !', 'wdesignkit' ), | |
| 2838 | + 'description' => is_wp_error( $install ) ? $install->get_error_message() : esc_html__( 'Theme could not be installed.', 'wdesignkit' ), | |
| 2839 | + 'status' => 'inactive', | |
| 2840 | + 'success' => false, | |
| 2841 | + ); | |
| 2547 | 2842 | } |
| 2548 | 2843 | |
| 2549 | - $wp_filesystem->delete( WP_CONTENT_DIR . '/themes/' . $theme_slug . '.zip' ); | |
| 2550 | - | |
| 2551 | 2844 | $activate_result = switch_theme( $name ); |
| 2552 | 2845 | |
| 2553 | 2846 | if ( ! is_wp_error( $activate_result ) ) { |
| 2554 | 2847 | $response = array( |
| @@ -2746,9 +3039,9 @@ | ||
| 2746 | 3039 | |
| 2747 | 3040 | if ( ! empty( $meta_data ) ) { |
| 2748 | 3041 | foreach ( $meta_data as $meta_key => $meta_val ) { |
| 2749 | 3042 | if ( ! empty( $meta_val[0] ) && is_serialized( $meta_val[0] ) ) { |
| 2750 | - $meta_val[0] = maybe_unserialize( $meta_val[0] ); | |
| 3043 | + $meta_val[0] = unserialize( $meta_val[0], array( 'allowed_classes' => false ) ); | |
| 2751 | 3044 | } |
| 2752 | 3045 | |
| 2753 | 3046 | if ( get_post_meta( get_the_ID(), $meta_key, true ) === '' ) { |
| 2754 | 3047 | add_post_meta( get_the_ID(), $meta_key, $meta_val[0] ); |
| @@ -2759,8 +3052,34 @@ | ||
| 2759 | 3052 | } |
| 2760 | 3053 | } |
| 2761 | 3054 | } |
| 2762 | 3055 | |
| 3056 | + /** | |
| 3057 | + * Fires after a template has been imported from the cloud. | |
| 3058 | + * | |
| 3059 | + * WDesignKit's templates live in the cloud, so nothing local records that an import | |
| 3060 | + * happened — there is no post type, no option, nothing to count after the fact. This is the | |
| 3061 | + * only moment the information exists. | |
| 3062 | + * | |
| 3063 | + * @since 2.6.4 | |
| 3064 | + * | |
| 3065 | + * @param string $kind 'single' or 'kit'. | |
| 3066 | + * @param string $builder Builder the template was imported for, e.g. 'elementor'. | |
| 3067 | + * @param int $count How many templates this import brought in. | |
| 3068 | + */ | |
| 3069 | + // Only a completed import counts. The cloud's failure shape for this endpoint family sets | |
| 3070 | + // content => 'error' (see the sibling check in wdkit_import_kit_template() above) — that is | |
| 3071 | + // non-empty, so the previous `||` fired the counter on failed imports too. Require success | |
| 3072 | + // AND an absent/non-'error' content instead. | |
| 3073 | + if ( ! empty( $response['success'] ) && ( ! isset( $response['content'] ) || 'error' !== $response['content'] ) ) { | |
| 3074 | + do_action( | |
| 3075 | + 'wdkit_template_imported', | |
| 3076 | + 'import_kit_template' === $api_type ? 'kit' : 'single', | |
| 3077 | + isset( $_POST['builder'] ) ? sanitize_key( wp_unslash( $_POST['builder'] ) ) : '', | |
| 3078 | + 1 | |
| 3079 | + ); | |
| 3080 | + } | |
| 3081 | + | |
| 2763 | 3082 | wp_send_json( $response ); |
| 2764 | 3083 | wp_die(); |
| 2765 | 3084 | } |
| 2766 | 3085 | |
| @@ -2920,11 +3239,17 @@ | ||
| 2920 | 3239 | return 0; |
| 2921 | 3240 | } |
| 2922 | 3241 | |
| 2923 | 3242 | $fixed = 0; |
| 3243 | + $ids = array_unique( array_map( 'intval', $page_ids ) ); | |
| 2924 | 3244 | |
| 2925 | - foreach ( array_unique( array_map( 'intval', $page_ids ) ) as $post_id ) { | |
| 3245 | + // Primes the meta cache for the whole batch in one query, so the | |
| 3246 | + // get_post_meta() call below hits the cache instead of issuing one query | |
| 3247 | + // per imported page. | |
| 3248 | + update_meta_cache( 'post', $ids ); | |
| 2926 | 3249 | |
| 3250 | + foreach ( $ids as $post_id ) { | |
| 3251 | + | |
| 2927 | 3252 | if ( ! $post_id ) { |
| 2928 | 3253 | continue; |
| 2929 | 3254 | } |
| 2930 | 3255 | |
| @@ -3581,8 +3906,28 @@ | ||
| 3581 | 3906 | $output['description'] = $response['description']; |
| 3582 | 3907 | $output['data'] = $result; |
| 3583 | 3908 | $output['success'] = $response['success']; |
| 3584 | 3909 | |
| 3910 | + // Counts the IMPORT ACTION, not what it brought in. A kit import always counts as 1 kit, | |
| 3911 | + // no matter how many blocks/pages that kit contains — confirmed live: a single gutenberg | |
| 3912 | + // kit import recorded total=680, kinds.kit=680, because $template_ids for that call was a | |
| 3913 | + // 680-element array of the kit's own blocks and count( $template_ids ) counted every one of | |
| 3914 | + // them. A page-kit's *size* is not tracking's concern; "was a kit imported" is. | |
| 3915 | + // | |
| 3916 | + // The 'single' branch keeps a defensive fallback for the one shape this endpoint's own | |
| 3917 | + // $template_ids reliably takes when it is not a kit — a single {id, name, slug, thumb...} | |
| 3918 | + // object — where count() would likewise count JSON keys instead of "1 template imported". | |
| 3919 | + if ( ! empty( $output['success'] ) ) { | |
| 3920 | + $is_kit = ( '' !== $website_kit ); | |
| 3921 | + $import_count = $is_kit ? 1 : ( isset( $template_ids['id'] ) ? 1 : ( is_array( $template_ids ) ? count( $template_ids ) : 1 ) ); | |
| 3922 | + do_action( | |
| 3923 | + 'wdkit_template_imported', | |
| 3924 | + $is_kit ? 'kit' : 'single', | |
| 3925 | + sanitize_key( $builder ), | |
| 3926 | + $import_count | |
| 3927 | + ); | |
| 3928 | + } | |
| 3929 | + | |
| 3585 | 3930 | wp_send_json( $output ); |
| 3586 | 3931 | wp_die(); |
| 3587 | 3932 | } |
| 3588 | 3933 | |
| @@ -3934,8 +4279,21 @@ | ||
| 3934 | 4279 | } |
| 3935 | 4280 | |
| 3936 | 4281 | if ( ! empty( $data ) && ! empty( $template_id ) && ! empty( $post_type ) && current_user_can( 'manage_options' ) ) { |
| 3937 | 4282 | $post_content = $data; |
| 4283 | + // Restore The Plus Addons' globals before the page is built, so the widgets' | |
| 4284 | + // tp_global_preset references resolve as soon as it renders. Done here rather | |
| 4285 | + // than in the save-template UI's confirmation dialog so that every import path | |
| 4286 | + // - the library, the abilities, the theme builder - gets it. | |
| 4287 | + if ( isset( $post_content->tp_globals ) && ! empty( $post_content->tp_globals ) ) { | |
| 4288 | + $this->wdkit_merge_tp_globals( | |
| 4289 | + json_decode( wp_json_encode( $post_content->tp_globals ), true ), | |
| 4290 | + isset( $post_content->tp_global_refs ) | |
| 4291 | + ? json_decode( wp_json_encode( $post_content->tp_global_refs ), true ) | |
| 4292 | + : array() | |
| 4293 | + ); | |
| 4294 | + } | |
| 4295 | + | |
| 3938 | 4296 | $post_title = isset( $post_content->title ) ? sanitize_text_field( $post_content->title ) : ''; |
| 3939 | 4297 | $post_slug = isset( $post_content->slug ) ? sanitize_text_field( $post_content->slug ) : ''; |
| 3940 | 4298 | $file_type = isset( $post_content->file_type ) ? sanitize_text_field( $post_content->file_type ) : ''; |
| 3941 | 4299 | $content = isset( $post_content->content ) ? wp_slash( $post_content->content ) : ''; |
| @@ -3982,9 +4340,9 @@ | ||
| 3982 | 4340 | ); |
| 3983 | 4341 | wp_die(); |
| 3984 | 4342 | } |
| 3985 | 4343 | |
| 3986 | - if ( ! empty( $thumb_image ) ) { | |
| 4344 | + if ( ! empty( $thumb_image ) && wdesignkit_validate_external_url( $thumb_image ) ) { | |
| 3987 | 4345 | // $featured_image_url = esc_url_raw( $thumb_image ); |
| 3988 | 4346 | $tmp = download_url( $thumb_image ); |
| 3989 | 4347 | if ( is_wp_error( $tmp ) ) { |
| 3990 | 4348 | error_log( 'Image download failed: ' . esc_html( $tmp->get_error_message() ) ); |
| @@ -4020,9 +4378,9 @@ | ||
| 4020 | 4378 | $custom_meta = isset( $post_content->custom_meta ) ? json_decode( wp_json_encode( $post_content->custom_meta ), true ) : ''; |
| 4021 | 4379 | if ( ! empty( $custom_meta ) ) { |
| 4022 | 4380 | foreach ( $custom_meta as $meta_key => $meta_val ) { |
| 4023 | 4381 | if ( isset( $meta_val[0] ) && ! empty( $meta_val[0] ) && is_serialized( $meta_val[0] ) ) { |
| 4024 | - $meta_val[0] = maybe_unserialize( $meta_val[0] ); | |
| 4382 | + $meta_val[0] = unserialize( $meta_val[0], array( 'allowed_classes' => false ) ); | |
| 4025 | 4383 | } |
| 4026 | 4384 | |
| 4027 | 4385 | if ( '' === get_post_meta( $inserted_post, $meta_key, true ) && isset( $meta_val[0] ) ) { |
| 4028 | 4386 | add_post_meta( $inserted_post, $meta_key, $meta_val[0] ); |
| @@ -4054,8 +4412,15 @@ | ||
| 4054 | 4412 | Tpgb_Library()->remove_backend_dir_files(); |
| 4055 | 4413 | } |
| 4056 | 4414 | |
| 4057 | 4415 | clean_post_cache( $inserted_post ); |
| 4416 | + | |
| 4417 | + // This whole method imports exactly one section per call — unlike | |
| 4418 | + // wdkit_import_template()/wdkit_import_kit_template(), it never fired this hook | |
| 4419 | + // at all, so single-section imports (Header/Footer/CTA/etc., a primary import | |
| 4420 | + // path per the Template Type sidebar) were invisible to tracking entirely. | |
| 4421 | + do_action( 'wdkit_template_imported', 'single', sanitize_key( $editor ), 1 ); | |
| 4422 | + | |
| 4058 | 4423 | wp_send_json( |
| 4059 | 4424 | array( |
| 4060 | 4425 | $temp_id => $temp_detail, |
| 4061 | 4426 | 'description' => 'Yay! Your Section has been Successfully Imported.', |
| @@ -4111,9 +4476,9 @@ | ||
| 4111 | 4476 | } |
| 4112 | 4477 | |
| 4113 | 4478 | $inserted_id = $new_document->get_main_id(); |
| 4114 | 4479 | |
| 4115 | - if ( ! empty( $thumb_image ) ) { | |
| 4480 | + if ( ! empty( $thumb_image ) && wdesignkit_validate_external_url( $thumb_image ) ) { | |
| 4116 | 4481 | // $featured_image_url = esc_url_raw( $thumb_image ); |
| 4117 | 4482 | $tmp = download_url( $thumb_image ); |
| 4118 | 4483 | if ( is_wp_error( $tmp ) ) { |
| 4119 | 4484 | error_log( 'Image download failed: ' . esc_html( $tmp->get_error_message() ) ); |
| @@ -4167,9 +4532,9 @@ | ||
| 4167 | 4532 | $custom_meta = isset( $post_content->custom_meta ) ? json_decode( wp_json_encode( $post_content->custom_meta ), true ) : ''; |
| 4168 | 4533 | if ( ! empty( $custom_meta ) ) { |
| 4169 | 4534 | foreach ( $custom_meta as $meta_key => $meta_val ) { |
| 4170 | 4535 | if ( ! empty( $meta_val[0] ) && is_serialized( $meta_val[0] ) ) { |
| 4171 | - $meta_val[0] = maybe_unserialize( $meta_val[0] ); | |
| 4536 | + $meta_val[0] = unserialize( $meta_val[0], array( 'allowed_classes' => false ) ); | |
| 4172 | 4537 | } |
| 4173 | 4538 | if ( '' === get_post_meta( $inserted_id, $meta_key, true ) ) { |
| 4174 | 4539 | add_post_meta( $inserted_id, $meta_key, $meta_val[0] ); |
| 4175 | 4540 | } |
| @@ -4193,8 +4558,12 @@ | ||
| 4193 | 4558 | } |
| 4194 | 4559 | |
| 4195 | 4560 | \Elementor\Plugin::$instance->files_manager->clear_cache(); |
| 4196 | 4561 | |
| 4562 | + // See the matching note in the Gutenberg branch above — this method never | |
| 4563 | + // fired the tracking hook for either editor. | |
| 4564 | + do_action( 'wdkit_template_imported', 'single', 'elementor', 1 ); | |
| 4565 | + | |
| 4197 | 4566 | wp_send_json( |
| 4198 | 4567 | array( |
| 4199 | 4568 | $temp_id => $temp_detail, |
| 4200 | 4569 | 'content' => $temp_con, |
| @@ -4330,8 +4699,12 @@ | ||
| 4330 | 4699 | } |
| 4331 | 4700 | |
| 4332 | 4701 | $image_url = esc_url_raw( $_POST['image_url'] ); |
| 4333 | 4702 | |
| 4703 | + if ( ! wdesignkit_validate_external_url( $image_url ) ) { | |
| 4704 | + wp_send_json_error( 'Image could not be downloaded.' ); | |
| 4705 | + } | |
| 4706 | + | |
| 4334 | 4707 | $tmp_file = download_url( $image_url ); |
| 4335 | 4708 | if ( is_wp_error( $tmp_file ) ) { |
| 4336 | 4709 | wp_send_json_error( 'Image could not be downloaded.' ); |
| 4337 | 4710 | } |
| @@ -4367,9 +4740,21 @@ | ||
| 4367 | 4740 | |
| 4368 | 4741 | $upload_dir = wp_upload_dir(); |
| 4369 | 4742 | $result_urls = array(); |
| 4370 | 4743 | |
| 4744 | + $colour_index = 0; | |
| 4371 | 4745 | foreach ( $img_colors as $name => $rgb ) { |
| 4746 | + // $name is a key from the posted colours payload and went straight into the output | |
| 4747 | + // filename, so traversal sequences in it steered imagepng() outside the upload | |
| 4748 | + // directory (CWE-22, ClickUp 86d41ced6). sanitize_file_name() flattens it to one | |
| 4749 | + // path segment; a key made only of dots/separators sanitizes to empty, so fall back | |
| 4750 | + // to a positional index rather than writing to a bare "colored--<time>.png". | |
| 4751 | + ++$colour_index; | |
| 4752 | + $safe_name = sanitize_file_name( (string) $name ); | |
| 4753 | + if ( '' === $safe_name ) { | |
| 4754 | + $safe_name = 'colour-' . $colour_index; | |
| 4755 | + } | |
| 4756 | + | |
| 4372 | 4757 | $new = imagecreatetruecolor( $width, $height ); |
| 4373 | 4758 | imagesavealpha( $new, true ); |
| 4374 | 4759 | imagealphablending( $new, false ); |
| 4375 | 4760 | |
| @@ -4395,9 +4780,9 @@ | ||
| 4395 | 4780 | imagesetpixel( $new, $x, $y, $color ); |
| 4396 | 4781 | } |
| 4397 | 4782 | } |
| 4398 | 4783 | |
| 4399 | - $filename = 'colored-' . $name . '-' . time() . '.png'; | |
| 4784 | + $filename = 'colored-' . $safe_name . '-' . time() . '.png'; | |
| 4400 | 4785 | $filepath = $upload_dir['path'] . '/' . $filename; |
| 4401 | 4786 | |
| 4402 | 4787 | imagepng( $new, $filepath ); |
| 4403 | 4788 | imagedestroy( $new ); |
| @@ -4712,12 +5097,73 @@ | ||
| 4712 | 5097 | } |
| 4713 | 5098 | |
| 4714 | 5099 | $response = json_decode( wp_json_encode( $response['data'] ), true ); |
| 4715 | 5100 | |
| 5101 | + $this->wdkit_cache_cloud_usage( $response ); | |
| 5102 | + | |
| 4716 | 5103 | wp_send_json( $response ); |
| 4717 | 5104 | wp_die(); |
| 4718 | 5105 | } |
| 4719 | 5106 | |
| 5107 | + /** | |
| 5108 | + * Caches the storage / credit figures this response carried. | |
| 5109 | + * | |
| 5110 | + * This handler is the ONLY place those numbers ever exist on the site: the cloud endpoint | |
| 5111 | + * authenticates with a user token that only a logged-in dashboard request carries, so the | |
| 5112 | + * analytics heartbeat — which runs on cron with no user at all — can never fetch them itself. | |
| 5113 | + * Caching them here is what lets Posimyth_Tracker_WDK report them, and it reports the cache's | |
| 5114 | + * age alongside so a stale reading is recognisable as one. | |
| 5115 | + * | |
| 5116 | + * Field names are probed rather than assumed: the cloud has renamed these before, and the | |
| 5117 | + * licence ability already carries six spellings of its own key field for the same reason. An | |
| 5118 | + * unrecognised shape simply caches nothing rather than storing a wrong number. | |
| 5119 | + * | |
| 5120 | + * Only the figures are kept. No token, no account id, no email — the analytics consent copy | |
| 5121 | + * promises non-sensitive data only, and this is read by the payload builder. | |
| 5122 | + * | |
| 5123 | + * @since 2.6.4 | |
| 5124 | + * | |
| 5125 | + * @param mixed $data Decoded `data` object from the credits endpoint. | |
| 5126 | + * @return void | |
| 5127 | + */ | |
| 5128 | + private function wdkit_cache_cloud_usage( $data ) { | |
| 5129 | + if ( ! is_array( $data ) ) { | |
| 5130 | + return; | |
| 5131 | + } | |
| 5132 | + | |
| 5133 | + $pick = static function ( $source, array $fields ) { | |
| 5134 | + foreach ( $fields as $field ) { | |
| 5135 | + if ( isset( $source[ $field ] ) && is_numeric( $source[ $field ] ) ) { | |
| 5136 | + return (float) $source[ $field ]; | |
| 5137 | + } | |
| 5138 | + } | |
| 5139 | + return null; | |
| 5140 | + }; | |
| 5141 | + | |
| 5142 | + $usage = array( | |
| 5143 | + 'storage_used' => $pick( $data, array( 'used_storage', 'storage_used', 'used_space' ) ), | |
| 5144 | + 'storage_total' => $pick( $data, array( 'total_storage', 'storage_total', 'storage', 'total_space' ) ), | |
| 5145 | + 'credit_used' => $pick( $data, array( 'used_credit', 'credit_used', 'used_credits' ) ), | |
| 5146 | + 'credit_total' => $pick( $data, array( 'total_credit', 'credit_total', 'credits', 'real_credit' ) ), | |
| 5147 | + ); | |
| 5148 | + | |
| 5149 | + $usage = array_filter( | |
| 5150 | + $usage, | |
| 5151 | + static function ( $value ) { | |
| 5152 | + return null !== $value; | |
| 5153 | + } | |
| 5154 | + ); | |
| 5155 | + | |
| 5156 | + if ( empty( $usage ) ) { | |
| 5157 | + return; | |
| 5158 | + } | |
| 5159 | + | |
| 5160 | + $usage['cached_at'] = gmdate( 'Y-m-d H:i:s' ); | |
| 5161 | + | |
| 5162 | + // Not autoloaded: read once a week by the heartbeat, never on a front-end request. | |
| 5163 | + update_option( 'wdkit_cloud_usage', $usage, false ); | |
| 5164 | + } | |
| 5165 | + | |
| 4720 | 5166 | public function wdkit_nxt_thembuilder_reset() { |
| 4721 | 5167 | $post_id = isset( $_POST['post_id'] ) ? sanitize_text_field( $_POST['post_id'] ) : ''; |
| 4722 | 5168 | $sections_layout = get_post_meta( $post_id, 'nxt-hooks-layout-sections', true ); |
| 4723 | 5169 | |
| @@ -5050,10 +5496,35 @@ | ||
| 5050 | 5496 | 'description' => esc_html__( 'widget JSON file not found.', 'wdesignkit' ), |
| 5051 | 5497 | ); |
| 5052 | 5498 | } |
| 5053 | 5499 | |
| 5054 | - $json_path = WDKIT_BUILDER_PATH . "/{$widget_type}/{$folder_name}/{$file_name}"; | |
| 5500 | + // Read-side twin of the write and delete traversals fixed in 86d41cckh / 86d41ccz2: all | |
| 5501 | + // three segments arrive from $_POST with only wp_unslash() applied — which strips | |
| 5502 | + // nothing path-relevant — so "../" in any of them walked out of the builder directory | |
| 5503 | + // and this handler returned the decoded contents of any .json file the web server user | |
| 5504 | + // could read (CWE-22, ClickUp 86d41zaun). | |
| 5505 | + $safe_path = wdesignkit_widget_path_guard( $widget_type, $folder_name, $file_name ); | |
| 5055 | 5506 | |
| 5507 | + if ( false === $safe_path || '' === $safe_path['folder'] || '' === $safe_path['file'] ) { | |
| 5508 | + return array( | |
| 5509 | + 'success' => false, | |
| 5510 | + 'message' => esc_html__( 'Widget JSON not found', 'wdesignkit' ), | |
| 5511 | + 'description' => esc_html__( 'Invalid widget path.', 'wdesignkit' ), | |
| 5512 | + ); | |
| 5513 | + } | |
| 5514 | + | |
| 5515 | + $json_path = $safe_path['base']; | |
| 5516 | + | |
| 5517 | + // Re-check the resolved file: the component guard above cannot see a symlink. Returns | |
| 5518 | + // false for a path that does not exist, which is the same answer we want anyway. | |
| 5519 | + if ( ! wdesignkit_path_inside_builder_dir( "$json_path.json" ) ) { | |
| 5520 | + return array( | |
| 5521 | + 'success' => false, | |
| 5522 | + 'message' => esc_html__( 'Widget JSON not found', 'wdesignkit' ), | |
| 5523 | + 'description' => esc_html__( 'widget JSON file not found.', 'wdesignkit' ), | |
| 5524 | + ); | |
| 5525 | + } | |
| 5526 | + | |
| 5056 | 5527 | $json_data = wp_json_file_decode( "$json_path.json" ); |
| 5057 | 5528 | if ( ! empty( $json_data ) ) { |
| 5058 | 5529 | $result = (object) array( |
| 5059 | 5530 | 'success' => true, |
| @@ -5079,9 +5550,9 @@ | ||
| 5079 | 5550 | * |
| 5080 | 5551 | * @since 1.0.0 |
| 5081 | 5552 | */ |
| 5082 | 5553 | protected function wdkit_download_widget() { |
| 5083 | - $data = ! empty( $_POST['widget_info'] ) ? $this->wdkit_sanitizer_bypass( $_POST, 'widget_info', 'none' ) : ''; | |
| 5554 | + $data = ! empty( $_POST['widget_info'] ) ? $this->wdkit_extract_post_field( $_POST, 'widget_info', 'none' ) : ''; | |
| 5084 | 5555 | $data = json_decode( stripslashes( $data ) ); |
| 5085 | 5556 | |
| 5086 | 5557 | $array_data = array( |
| 5087 | 5558 | 'token' => isset( $data->token ) ? sanitize_text_field( $data->token ) : '', |
| @@ -5149,14 +5620,34 @@ | ||
| 5149 | 5620 | if ( ! is_array( $json_data ) ) { |
| 5150 | 5621 | $json_data = json_decode( $json_data, true ); |
| 5151 | 5622 | } |
| 5152 | 5623 | |
| 5153 | - $title = ! empty( $json_data['widget_data']['widgetdata']['name'] ) ? sanitize_text_field( $json_data['widget_data']['widgetdata']['name'] ) : ''; | |
| 5154 | - $builder = ! empty( $json_data['widget_data']['widgetdata']['type'] ) ? sanitize_text_field( $json_data['widget_data']['widgetdata']['type'] ) : ''; | |
| 5155 | - $w_uniq = ! empty( $json_data['widget_data']['widgetdata']['widget_id'] ) ? sanitize_text_field( $json_data['widget_data']['widgetdata']['widget_id'] ) : ''; | |
| 5624 | + // Sanitize as filenames before use in the widget path (CWE-22): sanitize_file_name() | |
| 5625 | + // on name/id and sanitize_key() + allowlist on the builder strip path separators and | |
| 5626 | + // dots so a crafted cloud response cannot escape WDKIT_BUILDER_PATH. | |
| 5627 | + $title = ! empty( $json_data['widget_data']['widgetdata']['name'] ) ? sanitize_file_name( $json_data['widget_data']['widgetdata']['name'] ) : ''; | |
| 5628 | + $builder = ! empty( $json_data['widget_data']['widgetdata']['type'] ) ? sanitize_key( $json_data['widget_data']['widgetdata']['type'] ) : ''; | |
| 5629 | + $w_uniq = ! empty( $json_data['widget_data']['widgetdata']['widget_id'] ) ? sanitize_file_name( $json_data['widget_data']['widgetdata']['widget_id'] ) : ''; | |
| 5156 | 5630 | |
| 5157 | - $folder_name = str_replace( ' ', '-', $title ) . '_' . $w_uniq; | |
| 5158 | - $file_name = str_replace( ' ', '_', $title ) . '_' . $w_uniq; | |
| 5631 | + $allowed_builders = array( 'elementor', 'gutenberg', 'gutenberg_core', 'bricks' ); | |
| 5632 | + if ( '' === $title || '' === $w_uniq || ! in_array( $builder, $allowed_builders, true ) ) { | |
| 5633 | + $responce = (object) array( | |
| 5634 | + 'success' => false, | |
| 5635 | + 'message' => esc_html__( 'Operation Failed!', 'wdesignkit' ), | |
| 5636 | + 'description' => esc_html__( 'Invalid widget path.', 'wdesignkit' ), | |
| 5637 | + ); | |
| 5638 | + | |
| 5639 | + wp_send_json( $responce ); | |
| 5640 | + wp_die(); | |
| 5641 | + } | |
| 5642 | + | |
| 5643 | + // Canonical helpers replace spaces BEFORE sanitize_file_name(). $title above is | |
| 5644 | + // already sanitized, which collapsed spaces to hyphens and left the underscore pass | |
| 5645 | + // with nothing to do — a multi-word title wrote "My-Widget_id.json" next to the | |
| 5646 | + // "My_Widget_id.php" the builder's save path writes. The loader pairs the two by | |
| 5647 | + // swapping .php for .json, so the widget was silently dropped (ClickUp 86d41cck5). | |
| 5648 | + $folder_name = wdesignkit_widget_folder_name( $title, $w_uniq ); | |
| 5649 | + $file_name = wdesignkit_widget_file_name( $title, $w_uniq ); | |
| 5159 | 5650 | $builder_type_path = WDKIT_BUILDER_PATH . "/{$builder}/"; |
| 5160 | 5651 | |
| 5161 | 5652 | if ( ! is_dir( $builder_type_path ) ) { |
| 5162 | 5653 | wp_mkdir_p( $builder_type_path ); |
| @@ -5166,15 +5657,27 @@ | ||
| 5166 | 5657 | wp_mkdir_p( $builder_type_path . $folder_name ); |
| 5167 | 5658 | } |
| 5168 | 5659 | |
| 5169 | 5660 | if ( ! empty( $img_url ) ) { |
| 5170 | - $img_body = wp_remote_get( $img_url ); | |
| 5171 | - $img_ext = pathinfo( $img_url )['extension']; | |
| 5661 | + // SSRF guard (CWE-918): validate the resolved host before fetching. | |
| 5662 | + $img_body = wdesignkit_safe_remote_get( $img_url ); | |
| 5663 | + if ( ! is_wp_error( $img_body ) ) { | |
| 5664 | + // The remote extension was written verbatim here, so a cloud response naming a | |
| 5665 | + // ".php" image put executable PHP in the builder directory (CWE-434, | |
| 5666 | + // ClickUp 86d41cczd). An empty return means the bytes are not an image. | |
| 5667 | + $img_ext = wdesignkit_safe_image_extension( $img_url, $img_body['body'] ); | |
| 5172 | 5668 | |
| 5173 | - $wp_filesystem->put_contents( WDKIT_BUILDER_PATH . "/$builder/$folder_name/$file_name.$img_ext", $img_body['body'] ); | |
| 5174 | - $json_data['widget_data']['widgetdata']['w_image'] = WDKIT_SERVER_PATH . "/$builder/$folder_name/$file_name.$img_ext"; | |
| 5669 | + if ( '' !== $img_ext ) { | |
| 5670 | + $wp_filesystem->put_contents( WDKIT_BUILDER_PATH . "/$builder/$folder_name/$file_name.$img_ext", $img_body['body'] ); | |
| 5671 | + $json_data['widget_data']['widgetdata']['w_image'] = WDKIT_SERVER_PATH . "/$builder/$folder_name/$file_name.$img_ext"; | |
| 5672 | + } | |
| 5673 | + } | |
| 5175 | 5674 | } |
| 5176 | 5675 | |
| 5676 | + if ( function_exists( 'wdesignkit_invalidate_widget_registry' ) ) { | |
| 5677 | + wdesignkit_invalidate_widget_registry( $builder ); | |
| 5678 | + } | |
| 5679 | + | |
| 5177 | 5680 | // Bug E fix (part 2): success was hardcoded false on the successful download path — always reported failure. |
| 5178 | 5681 | $result = (object) array( |
| 5179 | 5682 | 'success' => true, |
| 5180 | 5683 | 'message' => ! empty( $response['message'] ) ? $response['message'] : esc_html__( 'no message', 'wdesignkit' ), |
| @@ -5192,9 +5695,9 @@ | ||
| 5192 | 5695 | * |
| 5193 | 5696 | * @since 1.0.0 |
| 5194 | 5697 | */ |
| 5195 | 5698 | protected function wdkit_add_widget() { |
| 5196 | - $data = ! empty( $_POST['widget_info'] ) ? $this->wdkit_sanitizer_bypass( $_POST, 'widget_info', 'none' ) : ''; | |
| 5699 | + $data = ! empty( $_POST['widget_info'] ) ? $this->wdkit_extract_post_field( $_POST, 'widget_info', 'none' ) : ''; | |
| 5197 | 5700 | $data = base64_decode( $data ); |
| 5198 | 5701 | $data = json_decode( $data ); |
| 5199 | 5702 | |
| 5200 | 5703 | $title = isset( $data->title ) ? sanitize_text_field( $data->title ) : ''; |
| @@ -5203,9 +5706,11 @@ | ||
| 5203 | 5706 | $w_image = isset( $data->w_image ) ? esc_url_raw( $data->w_image ) : ''; |
| 5204 | 5707 | |
| 5205 | 5708 | if ( ! empty( $w_image ) ) { |
| 5206 | 5709 | $w_image = str_replace( '\\', '', $w_image ); |
| 5207 | - $w_image = wp_remote_get( $w_image )['body']; | |
| 5710 | + // SSRF guard (CWE-918): validate the resolved host before fetching. | |
| 5711 | + $fetched = wdesignkit_safe_remote_get( $w_image ); | |
| 5712 | + $w_image = is_wp_error( $fetched ) ? '' : wp_remote_retrieve_body( $fetched ); | |
| 5208 | 5713 | } |
| 5209 | 5714 | |
| 5210 | 5715 | $array_data = array( |
| 5211 | 5716 | 'token' => isset( $data->token ) ? sanitize_text_field( $data->token ) : '', |
| @@ -5245,22 +5750,51 @@ | ||
| 5245 | 5750 | $img_url = ! empty( $response['data']['imgurl'] ) ? $response['data']['imgurl'] : ''; |
| 5246 | 5751 | |
| 5247 | 5752 | if ( ! empty( $img_url ) && 'error' !== $res ) { |
| 5248 | 5753 | |
| 5249 | - $img_body = wp_remote_get( $img_url ); | |
| 5250 | - $img_ext = pathinfo( $img_url )['extension']; | |
| 5251 | - include_once ABSPATH . 'wp-admin/includes/file.php'; | |
| 5252 | - \WP_Filesystem(); | |
| 5253 | - global $wp_filesystem; | |
| 5254 | - $folder_name = str_replace( ' ', '-', $title ) . '_' . $w_uniq; | |
| 5255 | - $file_name = str_replace( ' ', '_', $title ) . '_' . $w_uniq; | |
| 5256 | - $file_path = WDKIT_BUILDER_PATH . "/$builder/$folder_name/$file_name"; | |
| 5754 | + // SSRF guard (CWE-918): validate the resolved host before fetching. | |
| 5755 | + $img_body = wdesignkit_safe_remote_get( $img_url ); | |
| 5756 | + if ( ! is_wp_error( $img_body ) ) { | |
| 5757 | + // Verified against the payload rather than trusted from the URL (CWE-434, | |
| 5758 | + // ClickUp 86d41cczd); '' means the bytes are not an image we accept. | |
| 5759 | + $img_ext = wdesignkit_safe_image_extension( $img_url, $img_body['body'] ); | |
| 5760 | + include_once ABSPATH . 'wp-admin/includes/file.php'; | |
| 5761 | + \WP_Filesystem(); | |
| 5762 | + global $wp_filesystem; | |
| 5763 | + // Canonical helpers, so the JSON read and the image write here address the same | |
| 5764 | + // base name every other writer uses (ClickUp 86d41cck5). They also apply | |
| 5765 | + // sanitize_file_name(), which $title and $w_uniq had not been through. | |
| 5766 | + $folder_name = wdesignkit_widget_folder_name( $title, $w_uniq ); | |
| 5767 | + $file_name = wdesignkit_widget_file_name( $title, $w_uniq ); | |
| 5257 | 5768 | |
| 5258 | - $u_r_l = wp_json_file_decode( "$file_path.json" ); | |
| 5259 | - $u_r_l->widget_data->widgetdata->w_image = WDKIT_SERVER_PATH . "/$builder/$folder_name/$file_name.$img_ext"; | |
| 5769 | + // $builder reaches here with only sanitize_text_field() applied and no | |
| 5770 | + // allowlist, so it was a live traversal segment in this path (CWE-22, | |
| 5771 | + // ClickUp 86d41cckh). Unlike the download handler earlier in this file, this | |
| 5772 | + // one had neither the builder allowlist nor a containment check. | |
| 5773 | + $safe_path = wdesignkit_widget_path_guard( $builder, $folder_name, $file_name ); | |
| 5774 | + if ( false === $safe_path || ! wdesignkit_path_inside_builder_dir( $safe_path['dir'] ) ) { | |
| 5775 | + wp_send_json( | |
| 5776 | + (object) array( | |
| 5777 | + 'success' => false, | |
| 5778 | + 'message' => esc_html__( 'Operation Failed!', 'wdesignkit' ), | |
| 5779 | + 'description' => esc_html__( 'Invalid widget path.', 'wdesignkit' ), | |
| 5780 | + ) | |
| 5781 | + ); | |
| 5782 | + wp_die(); | |
| 5783 | + } | |
| 5260 | 5784 | |
| 5261 | - $wp_filesystem->put_contents( "$file_path.json", wp_json_encode( $u_r_l ) ); | |
| 5262 | - $wp_filesystem->put_contents( "$file_path.$img_ext", $img_body['body'] ); | |
| 5785 | + $builder = $safe_path['builder']; | |
| 5786 | + $file_path = $safe_path['base']; | |
| 5787 | + | |
| 5788 | + $u_r_l = wp_json_file_decode( "$file_path.json" ); | |
| 5789 | + | |
| 5790 | + if ( '' !== $img_ext ) { | |
| 5791 | + $u_r_l->widget_data->widgetdata->w_image = WDKIT_SERVER_PATH . "/$builder/$folder_name/$file_name.$img_ext"; | |
| 5792 | + $wp_filesystem->put_contents( "$file_path.$img_ext", $img_body['body'] ); | |
| 5793 | + } | |
| 5794 | + | |
| 5795 | + $wp_filesystem->put_contents( "$file_path.json", wp_json_encode( $u_r_l ) ); | |
| 5796 | + } | |
| 5263 | 5797 | } |
| 5264 | 5798 | |
| 5265 | 5799 | wp_send_json( $response ); |
| 5266 | 5800 | wp_die(); |
| @@ -5360,9 +5894,9 @@ | ||
| 5360 | 5894 | 'template' => isset( $get_setting['template'] ) ? $get_setting['template'] : true, |
| 5361 | 5895 | 'gutenberg_builder' => isset( $get_setting['gutenberg_builder'] ) ? $get_setting['gutenberg_builder'] : true, |
| 5362 | 5896 | 'gutenberg_core_builder' => isset( $get_setting['gutenberg_core_builder'] ) ? $get_setting['gutenberg_core_builder'] : false, |
| 5363 | 5897 | 'elementor_builder' => isset( $get_setting['elementor_builder'] ) ? $get_setting['elementor_builder'] : true, |
| 5364 | - 'bricks_builder' => isset( $get_setting['bricks_builder'] ) ? $get_setting['bricks_builder'] : false, | |
| 5898 | + 'bricks_builder' => isset( $get_setting['bricks_builder'] ) ? $get_setting['bricks_builder'] : true, | |
| 5365 | 5899 | 'gutenberg_template' => isset( $get_setting['gutenberg_template'] ) ? $get_setting['gutenberg_template'] : true, |
| 5366 | 5900 | 'elementor_template' => isset( $get_setting['elementor_template'] ) ? $get_setting['elementor_template'] : true, |
| 5367 | 5901 | 'code_snippet' => isset( $get_setting['code_snippet'] ) ? $get_setting['code_snippet'] : true, |
| 5368 | 5902 | 'cross_copy_paste' => isset( $get_setting['cross_copy_paste'] ) ? $get_setting['cross_copy_paste'] : false, |
| @@ -5472,21 +6006,21 @@ | ||
| 5472 | 6006 | if ( ! empty( $response['data'] ) ) { |
| 5473 | 6007 | $response = json_decode( wp_json_encode( $response['data'] ), true ); |
| 5474 | 6008 | |
| 5475 | 6009 | if ( ! empty( $response['data']['tpae_licence'] ) && is_serialized( $response['data']['tpae_licence'] ) ) { |
| 5476 | - $response['data']['tpae_licence'] = unserialize( $response['data']['tpae_licence'] ); | |
| 6010 | + $response['data']['tpae_licence'] = unserialize( $response['data']['tpae_licence'], array( 'allowed_classes' => false ) ); | |
| 5477 | 6011 | } |
| 5478 | 6012 | |
| 5479 | 6013 | if ( ! empty( $response['data']['tpag_licence'] ) && is_serialized( $response['data']['tpag_licence'] ) ) { |
| 5480 | - $response['data']['tpag_licence'] = unserialize( $response['data']['tpag_licence'] ); | |
| 6014 | + $response['data']['tpag_licence'] = unserialize( $response['data']['tpag_licence'], array( 'allowed_classes' => false ) ); | |
| 5481 | 6015 | } |
| 5482 | 6016 | |
| 5483 | 6017 | if ( ! empty( $response['data']['uichemy_licence'] ) && is_serialized( $response['data']['uichemy_licence'] ) ) { |
| 5484 | - $response['data']['uichemy_licence'] = unserialize( $response['data']['uichemy_licence'] ); | |
| 6018 | + $response['data']['uichemy_licence'] = unserialize( $response['data']['uichemy_licence'], array( 'allowed_classes' => false ) ); | |
| 5485 | 6019 | } |
| 5486 | 6020 | |
| 5487 | 6021 | if ( ! empty( $response['data']['wdkit_licence'] ) && is_serialized( $response['data']['wdkit_licence'] ) ) { |
| 5488 | - $response['data']['wdkit_licence'] = unserialize( $response['data']['wdkit_licence'] ); | |
| 6022 | + $response['data']['wdkit_licence'] = unserialize( $response['data']['wdkit_licence'], array( 'allowed_classes' => false ) ); | |
| 5489 | 6023 | |
| 5490 | 6024 | // Store WDesignKit license status locally for quick access |
| 5491 | 6025 | if ( ! empty( $response['data']['wdkit_licence'] ) && is_array( $response['data']['wdkit_licence'] ) ) { |
| 5492 | 6026 | update_option( 'wdkit_licence_data', $response['data']['wdkit_licence'] ); |
| @@ -5493,9 +6027,9 @@ | ||
| 5493 | 6027 | } |
| 5494 | 6028 | } |
| 5495 | 6029 | |
| 5496 | 6030 | if ( ! empty( $response['data']['wdkit_licence_extra'] ) && is_serialized( $response['data']['wdkit_licence_extra'] ) ) { |
| 5497 | - $response['data']['wdkit_licence_extra'] = unserialize( $response['data']['wdkit_licence_extra'] ); | |
| 6031 | + $response['data']['wdkit_licence_extra'] = unserialize( $response['data']['wdkit_licence_extra'], array( 'allowed_classes' => false ) ); | |
| 5498 | 6032 | } |
| 5499 | 6033 | } |
| 5500 | 6034 | |
| 5501 | 6035 | wp_send_json( $response ); |
| @@ -5680,9 +6214,9 @@ | ||
| 5680 | 6214 | $token = $this->wdkit_login_user_token( $email ); |
| 5681 | 6215 | $args = array( 'token' => $token ); |
| 5682 | 6216 | |
| 5683 | 6217 | if ( 'session' !== $logout_type ) { |
| 5684 | - delete_transient( 'wdkit_auth_' . $email ); | |
| 6218 | + delete_transient( 'wdkit_auth_' . wdesignkit_cloud_session_key( $email ) ); | |
| 5685 | 6219 | // Clear stored license data on logout so banner shows again |
| 5686 | 6220 | delete_option( 'wdkit_licence_data' ); |
| 5687 | 6221 | $response = WDesignKit_Data_Query::get_data( 'logout', $args ); |
| 5688 | 6222 | } |
| @@ -5702,9 +6236,9 @@ | ||
| 5702 | 6236 | */ |
| 5703 | 6237 | protected function wdkit_login_user_token( $email = '' ) { |
| 5704 | 6238 | |
| 5705 | 6239 | if ( ! empty( $email ) ) { |
| 5706 | - $user_key = strstr( $email, '@', true ); | |
| 6240 | + $user_key = wdesignkit_cloud_session_key( $email ); | |
| 5707 | 6241 | $get_login = get_transient( 'wdkit_auth_' . $user_key ); |
| 5708 | 6242 | |
| 5709 | 6243 | if ( ! empty( $get_login ) && ! empty( $get_login['token'] ) ) { |
| 5710 | 6244 | return $get_login['token']; |
| @@ -5722,9 +6256,9 @@ | ||
| 5722 | 6256 | * @param string $data send all post data. |
| 5723 | 6257 | * @param string $type store text data. |
| 5724 | 6258 | * @param string $condition store text data. |
| 5725 | 6259 | */ |
| 5726 | - protected function wdkit_sanitizer_bypass( $data, $type, $condition = 'none' ) { | |
| 6260 | + protected function wdkit_extract_post_field( $data, $type, $condition = 'none' ) { | |
| 5727 | 6261 | |
| 5728 | 6262 | if ( 'none' === $condition ) { |
| 5729 | 6263 | return $data[ $type ]; |
| 5730 | 6264 | } elseif ( 'cr_widget' === $condition ) { |
| @@ -5729,8 +6263,10 @@ | ||
| 5729 | 6263 | return $data[ $type ]; |
| 5730 | 6264 | } elseif ( 'cr_widget' === $condition ) { |
| 5731 | 6265 | return $data[ $type ]; |
| 5732 | 6266 | } |
| 6267 | + | |
| 6268 | + return null; | |
| 5733 | 6269 | } |
| 5734 | 6270 | |
| 5735 | 6271 | |
| 5736 | 6272 | /** |