PluginProbe
Elementor Website Builder – more than just a page builder / 4.3.0-beta2
Elementor Website Builder – more than just a page builder v4.3.0-beta2
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 4.0.7 All 451 releases
elementor / core / files / base.php

base.php in Elementor Website Builder – more than just a page builder 4.3.0-beta2, at core/files/base.php

626 lines 13.5 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 namespace Elementor\Core\Files;
3
4 use Elementor\Plugin;
5
6 if ( ! defined( 'ABSPATH' ) ) {
7 exit; // Exit if accessed directly.
8 }
9
10 abstract class Base {
11
12 const UPLOADS_DIR = 'elementor/';
13
14 const DEFAULT_FILES_DIR = 'css/';
15
16 const META_KEY = '';
17
18 private static $wp_uploads_dir = [];
19
20 private $files_dir;
21
22 private $file_name;
23
24 /**
25 * File path.
26 *
27 * Holds the file path.
28 *
29 * @access private
30 *
31 * @var string
32 */
33 private $path;
34
35 /**
36 * Content.
37 *
38 * Holds the file content.
39 *
40 * @access private
41 *
42 * @var string
43 */
44 private $content;
45
46 /**
47 * @since 2.1.0
48 * @access public
49 * @static
50 */
51 public static function get_base_uploads_dir() {
52 $wp_upload_dir = self::get_wp_uploads_dir();
53 $dir = $wp_upload_dir['basedir'] . '/' . self::UPLOADS_DIR;
54
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;
82 }
83
84 /**
85 * @since 2.1.0
86 * @access public
87 * @static
88 */
89 public static function get_base_uploads_url() {
90 $wp_upload_dir = self::get_wp_uploads_dir();
91 $url = $wp_upload_dir['baseurl'] . '/' . self::UPLOADS_DIR;
92
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;
121 }
122
123 /**
124 * Use a create function for PhpDoc (@return static).
125 *
126 * @return static
127 */
128 public static function create() {
129 return Plugin::$instance->files_manager->get( get_called_class(), func_get_args() );
130 }
131
132 /**
133 * @since 2.1.0
134 * @access public
135 */
136 public function __construct( $file_name ) {
137 /**
138 * Elementor File Name
139 *
140 * Filters the File name
141 *
142 * @since 2.3.0
143 *
144 * @param string $file_name
145 * @param object $this The file instance, which inherits Elementor\Core\Files
146 */
147 $file_name = apply_filters( 'elementor/files/file_name', $file_name, $this );
148
149 $this->set_file_name( $file_name );
150
151 $this->set_files_dir( static::DEFAULT_FILES_DIR );
152
153 $this->set_path();
154 }
155
156 /**
157 * @since 2.1.0
158 * @access public
159 */
160 public function set_files_dir( $files_dir ) {
161 $this->files_dir = $files_dir;
162 }
163
164 /**
165 * @since 2.1.0
166 * @access public
167 */
168 public function set_file_name( $file_name ) {
169 $this->file_name = $file_name;
170 }
171
172 /**
173 * @since 2.1.0
174 * @access public
175 */
176 public function get_file_name() {
177 return $this->file_name;
178 }
179
180 /**
181 * @since 2.1.0
182 * @access public
183 */
184 public function get_url() {
185 $url = set_url_scheme( self::get_base_uploads_url() . $this->files_dir . $this->file_name );
186
187 return add_query_arg( [ 'ver' => $this->get_meta( 'time' ) ], $url );
188 }
189
190 /**
191 * Get Path
192 *
193 * Returns the local path of the generated file.
194 *
195 * @since 3.5.0
196 * @access public
197 *
198 * @return string
199 */
200 public function get_path() {
201 return set_url_scheme( self::get_base_uploads_dir() . $this->files_dir . $this->file_name );
202 }
203
204 /**
205 * @since 2.1.0
206 * @access public
207 */
208 public function get_content() {
209 if ( ! $this->content ) {
210 $this->content = $this->parse_content();
211 }
212
213 return $this->content;
214 }
215
216 /**
217 * @since 2.1.0
218 * @access public
219 */
220 public function update() {
221 $this->update_file();
222
223 $meta = $this->get_meta();
224
225 $meta['time'] = time();
226
227 $this->update_meta( $meta );
228 }
229
230 /**
231 * @since 2.1.0
232 * @access public
233 */
234 public function update_file() {
235 $this->content = $this->parse_content();
236
237 if ( $this->content ) {
238 $this->write();
239 } else {
240 $this->delete();
241 }
242 }
243
244 /**
245 * @since 2.1.0
246 * @access public
247 */
248 public function write() {
249 return file_put_contents( $this->path, $this->content );
250 }
251
252 /**
253 * @since 2.1.0
254 * @access public
255 */
256 public function delete() {
257 if ( file_exists( $this->path ) ) {
258 unlink( $this->path );
259 }
260
261 $this->delete_meta();
262 }
263
264 /**
265 * Get meta data.
266 *
267 * Retrieve the CSS file meta data. Returns an array of all the data, or if
268 * custom property is given it will return the property value, or `null` if
269 * the property does not exist.
270 *
271 * @since 2.1.0
272 * @access public
273 *
274 * @param string $property Optional. Custom meta data property. Default is
275 * null.
276 *
277 * @return array|null An array of all the data, or if custom property is
278 * given it will return the property value, or `null` if
279 * the property does not exist.
280 */
281 public function get_meta( $property = null ) {
282 $meta = array_merge( $this->get_default_meta(), (array) $this->load_meta() );
283
284 if ( $property ) {
285 return isset( $meta[ $property ] ) ? $meta[ $property ] : null;
286 }
287
288 return $meta;
289 }
290
291 /**
292 * @since 2.1.0
293 * @access protected
294 * @abstract
295 */
296 abstract protected function parse_content();
297
298 /**
299 * Load meta.
300 *
301 * Retrieve the file meta data.
302 *
303 * @since 2.1.0
304 * @access protected
305 */
306 protected function load_meta() {
307 return get_option( static::META_KEY );
308 }
309
310 /**
311 * Update meta.
312 *
313 * Update the file meta data.
314 *
315 * @since 2.1.0
316 * @access protected
317 *
318 * @param array $meta New meta data.
319 */
320 protected function update_meta( $meta ) {
321 update_option( static::META_KEY, $meta );
322 }
323
324 /**
325 * Delete meta.
326 *
327 * Delete the file meta data.
328 *
329 * @since 2.1.0
330 * @access protected
331 */
332 protected function delete_meta() {
333 delete_option( static::META_KEY );
334 }
335
336 /**
337 * @since 2.1.0
338 * @access protected
339 */
340 protected function get_default_meta() {
341 return [
342 'time' => 0,
343 ];
344 }
345
346 /**
347 * @since 2.1.0
348 * @access private
349 * @static
350 */
351 private static function get_wp_uploads_dir() {
352 global $blog_id;
353 if ( empty( self::$wp_uploads_dir[ $blog_id ] ) ) {
354 self::$wp_uploads_dir[ $blog_id ] = wp_upload_dir( null, false );
355 }
356
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;
610 }
611
612 /**
613 * @since 2.1.0
614 * @access private
615 */
616 private function set_path() {
617 $dir_path = self::get_base_uploads_dir() . $this->files_dir;
618
619 if ( ! is_dir( $dir_path ) ) {
620 wp_mkdir_p( $dir_path );
621 }
622
623 $this->path = $dir_path . $this->file_name;
624 }
625 }
626