PluginProbe
Elementor Website Builder – more than just a page builder / 4.3.0-beta3
Elementor Website Builder – more than just a page builder v4.3.0-beta3
4.3.0-beta3 4.3.0-beta2 4.3.0-beta1 4.2.4 4.2.3 4.2.2 4.2.1 4.2.0 4.1.5 4.2.0-beta2 4.2.0-dev2 4.2.0-beta1 4.1.4 4.1.3 4.1.2 4.1.1 4.1.0 4.1.0-beta3 4.1.0-dev3 4.0.9 4.1.0-beta2 4.1.0-dev2 4.0.8 4.1.0-beta1 4.1.0-dev1 All 452 releases
← All changes | core/files/base.php +309 -2 4.1.0-dev24.3.0-beta3 View file →
@@ -49,10 +49,37 @@
49 49 * @static
50 50 */
51 51 public static function get_base_uploads_dir() {
52 52 $wp_upload_dir = self::get_wp_uploads_dir();
53 + $dir = $wp_upload_dir['basedir'] . '/' . self::UPLOADS_DIR;
53 54
54 - return $wp_upload_dir['basedir'] . '/' . self::UPLOADS_DIR;
55 + if ( ! self::is_optimized_css_files_active() ) {
56 + return $dir;
57 + }
58 +
59 + /**
60 + * Elementor files base directory.
61 + *
62 + * Filters the absolute filesystem path of the Elementor files base directory.
63 + * Applies to all `Elementor\Core\Files\Base` consumers (Post CSS, Global CSS,
64 + * Frontend CSS, Google Fonts, atomic CSS, etc.) — not only atomic CSS.
65 + *
66 + * Can be filtered independently of `elementor/files/base_url`:
67 + * - Filter both to relocate Elementor files (e.g. WP VIP where `uploads/` is read-only).
68 + * - Filter only `elementor/files/base_url` (leaving this untouched) to serve files
69 + * from a CDN or an alternate URL (e.g. WPML sub-folder) while keeping local writes.
70 + *
71 + * Only available when the `e_optimized_css_files` experiment is active.
72 + *
73 + * @since 4.4.0
74 + *
75 + * @param string $dir Absolute filesystem path to the Elementor files base directory.
76 + */
77 + $filtered_dir = apply_filters( 'elementor/files/base_dir', $dir );
78 +
79 + $validated_dir = self::validate_base_dir( $filtered_dir );
80 +
81 + return null !== $validated_dir ? $validated_dir : $dir;
55 82 }
56 83
57 84 /**
58 85 * @since 2.1.0
@@ -60,10 +87,38 @@
60 87 * @static
61 88 */
62 89 public static function get_base_uploads_url() {
63 90 $wp_upload_dir = self::get_wp_uploads_dir();
91 + $url = $wp_upload_dir['baseurl'] . '/' . self::UPLOADS_DIR;
64 92
65 - return $wp_upload_dir['baseurl'] . '/' . self::UPLOADS_DIR;
93 + if ( ! self::is_optimized_css_files_active() ) {
94 + return $url;
95 + }
96 +
97 + /**
98 + * Elementor files base URL.
99 + *
100 + * Filters the public URL of the Elementor files base directory.
101 + * Applies to all `Elementor\Core\Files\Base` consumers (Post CSS, Global CSS,
102 + * Frontend CSS, Google Fonts, atomic CSS, etc.) — not only atomic CSS.
103 + *
104 + * Accepts absolute (`https://cdn.example.com/…`) and protocol-relative
105 + * (`//cdn.example.com/…`) URLs. Can be filtered independently of
106 + * `elementor/files/base_dir` — filter only this hook to serve files from a
107 + * CDN or an alternate URL (e.g. WPML sub-folder) while keeping local writes
108 + * at the default location.
109 + *
110 + * Only available when the `e_optimized_css_files` experiment is active.
111 + *
112 + * @since 4.4.0
113 + *
114 + * @param string $url Public URL for the Elementor files base directory.
115 + */
116 + $filtered_url = apply_filters( 'elementor/files/base_url', $url );
117 +
118 + $validated_url = self::validate_base_url( $filtered_url );
119 +
120 + return null !== $validated_url ? $validated_url : $url;
66 121 }
67 122
68 123 /**
69 124 * Use a create function for PhpDoc (@return static).
@@ -299,8 +354,260 @@
299 354 self::$wp_uploads_dir[ $blog_id ] = wp_upload_dir( null, false );
300 355 }
301 356
302 357 return self::$wp_uploads_dir[ $blog_id ];
358 + }
359 +
360 + /**
361 + * Whether the "Optimized CSS Files" experiment is active.
362 + *
363 + * @since 4.4.0
364 + * @access private
365 + * @static
366 + *
367 + * @return bool
368 + */
369 + private static function is_optimized_css_files_active() {
370 + return Plugin::$instance->experiments->is_feature_active( 'e_optimized_css_files' );
371 + }
372 +
373 + /**
374 + * Validate a filtered base directory path.
375 + *
376 + * @since 4.4.0
377 + * @access private
378 + * @static
379 + *
380 + * @param mixed $dir Candidate base directory path.
381 + *
382 + * @return string|null Normalized path on success, or null to fall back to the default.
383 + */
384 + private static function validate_base_dir( $dir ) {
385 + if ( ! is_string( $dir ) || '' === $dir ) {
386 + _doing_it_wrong(
387 + __METHOD__,
388 + 'The `elementor/files/base_dir` filter must return a non-empty string.',
389 + '4.4.0'
390 + );
391 +
392 + return null;
393 + }
394 +
395 + $dir = trailingslashit( wp_normalize_path( $dir ) );
396 +
397 + if ( ! self::is_absolute_path( $dir ) ) {
398 + _doing_it_wrong(
399 + __METHOD__,
400 + 'The `elementor/files/base_dir` filter must return an absolute filesystem path.',
401 + '4.4.0'
402 + );
403 +
404 + return null;
405 + }
406 +
407 + if ( self::path_contains_traversal( $dir ) ) {
408 + _doing_it_wrong(
409 + __METHOD__,
410 + 'The `elementor/files/base_dir` filter must not contain path traversal segments.',
411 + '4.4.0'
412 + );
413 +
414 + return null;
415 + }
416 +
417 + if ( ! self::is_path_within_allowed_roots( $dir ) ) {
418 + _doing_it_wrong(
419 + __METHOD__,
420 + 'The `elementor/files/base_dir` filter must resolve inside `WP_CONTENT_DIR` or the uploads basedir.',
421 + '4.4.0'
422 + );
423 +
424 + return null;
425 + }
426 +
427 + return $dir;
428 + }
429 +
430 + /**
431 + * Validate a filtered base URL.
432 + *
433 + * @since 4.4.0
434 + * @access private
435 + * @static
436 + *
437 + * @param mixed $url Candidate base URL.
438 + *
439 + * @return string|null Normalized URL on success, or null to fall back to the default.
440 + */
441 + private static function validate_base_url( $url ) {
442 + if ( ! is_string( $url ) || '' === $url ) {
443 + _doing_it_wrong(
444 + __METHOD__,
445 + 'The `elementor/files/base_url` filter must return a non-empty string.',
446 + '4.4.0'
447 + );
448 +
449 + return null;
450 + }
451 +
452 + $url = trailingslashit( $url );
453 +
454 + if ( ! self::is_valid_base_url( $url ) ) {
455 + _doing_it_wrong(
456 + __METHOD__,
457 + 'The `elementor/files/base_url` filter must return an absolute or protocol-relative http(s) URL.',
458 + '4.4.0'
459 + );
460 +
461 + return null;
462 + }
463 +
464 + return $url;
465 + }
466 +
467 + /**
468 + * Lightweight structural check for the base URL filter.
469 + *
470 + * Intentionally avoids `wp_http_validate_url()` which performs DNS lookups and
471 + * rejects protocol-relative and RFC1918 URLs — none of which we want when the
472 + * URL is only being prepended to CSS asset paths (CDN endpoints, WPML sub-folder
473 + * rewrites, etc.). We only enforce that the value looks like an absolute or
474 + * protocol-relative http/https URL with a host.
475 + *
476 + * @since 4.4.0
477 + * @access private
478 + * @static
479 + *
480 + * @param string $url Candidate URL (already normalized to a trailing slash).
481 + *
482 + * @return bool
483 + */
484 + private static function is_valid_base_url( $url ) {
485 + $is_protocol_relative = 0 === strpos( $url, '//' );
486 + $parsable = $is_protocol_relative ? 'https:' . $url : $url;
487 +
488 + $parts = wp_parse_url( $parsable );
489 +
490 + if ( ! is_array( $parts ) || empty( $parts['host'] ) ) {
491 + return false;
492 + }
493 +
494 + if ( ! $is_protocol_relative ) {
495 + $scheme = isset( $parts['scheme'] ) ? strtolower( $parts['scheme'] ) : '';
496 +
497 + if ( ! in_array( $scheme, [ 'http', 'https' ], true ) ) {
498 + return false;
499 + }
500 + }
501 +
502 + return true;
503 + }
504 +
505 + /**
506 + * @since 4.4.0
507 + * @access private
508 + * @static
509 + *
510 + * @param string $path Filesystem path.
511 + *
512 + * @return bool
513 + */
514 + private static function is_absolute_path( $path ) {
515 + if ( wp_is_stream( $path ) ) {
516 + return true;
517 + }
518 +
519 + $path = wp_normalize_path( $path );
520 +
521 + return isset( $path[0] ) && ( '/' === $path[0] || preg_match( '#^[a-zA-Z]:/#', $path ) );
522 + }
523 +
524 + /**
525 + * @since 4.4.0
526 + * @access private
527 + * @static
528 + *
529 + * @param string $path Filesystem path.
530 + *
531 + * @return bool
532 + */
533 + private static function path_contains_traversal( $path ) {
534 + $parts = explode( '/', wp_normalize_path( untrailingslashit( $path ) ) );
535 +
536 + return in_array( '..', $parts, true );
537 + }
538 +
539 + /**
540 + * @since 4.4.0
541 + * @access private
542 + * @static
543 + *
544 + * @param string $path Filesystem path.
545 + *
546 + * @return bool
547 + */
548 + private static function is_path_within_allowed_roots( $path ) {
549 + if ( defined( 'ELEMENTOR_FILES_ALLOW_EXTERNAL_BASE_DIR' ) && ELEMENTOR_FILES_ALLOW_EXTERNAL_BASE_DIR ) {
550 + return true;
551 + }
552 +
553 + $resolved_path = self::resolve_deepest_existing( $path );
554 +
555 + if ( false === $resolved_path ) {
556 + return false;
557 + }
558 +
559 + $uploads_basedir = self::get_wp_uploads_dir()['basedir'];
560 + $allowed_roots = array_filter( [
561 + self::resolve_deepest_existing( WP_CONTENT_DIR ),
562 + self::resolve_deepest_existing( $uploads_basedir ),
563 + ] );
564 +
565 + foreach ( $allowed_roots as $root ) {
566 + if ( $resolved_path === $root || 0 === strpos( $resolved_path, $root . '/' ) ) {
567 + return true;
568 + }
569 + }
570 +
571 + return false;
572 + }
573 +
574 + /**
575 + * Resolve the realpath of the deepest existing ancestor of `$path`.
576 + *
577 + * Non-existent targets are common when a host sets the filter to a directory
578 + * that Elementor will create on first write. Walking up to the deepest existing
579 + * ancestor lets us still resolve symlinks and reject paths whose real location
580 + * escapes the allowed roots.
581 + *
582 + * @since 4.4.0
583 + * @access private
584 + * @static
585 + *
586 + * @param string $path Filesystem path.
587 + *
588 + * @return string|false Normalized realpath, or false if none could be resolved.
589 + */
590 + private static function resolve_deepest_existing( $path ) {
591 + $current = wp_normalize_path( untrailingslashit( (string) $path ) );
592 +
593 + while ( '' !== $current && ! file_exists( $current ) ) {
594 + $parent = wp_normalize_path( dirname( $current ) );
595 +
596 + if ( $parent === $current ) {
597 + return false;
598 + }
599 +
600 + $current = $parent;
601 + }
602 +
603 + if ( '' === $current ) {
604 + return false;
605 + }
606 +
607 + $real = realpath( $current );
608 +
609 + return $real ? untrailingslashit( wp_normalize_path( $real ) ) : false;
303 610 }
304 611
305 612 /**
306 613 * @since 2.1.0