PluginProbe
Elementor Website Builder – more than just a page builder / 3.19.3
Elementor Website Builder – more than just a page builder v3.19.3
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 / uploads-manager.php

uploads-manager.php in Elementor Website Builder – more than just a page builder 3.19.3, at core/files/uploads-manager.php

738 lines 19.8 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\Core\Base\Base_Object;
5 use Elementor\Core\Common\Modules\Ajax\Module as Ajax;
6 use Elementor\Core\Files\File_Types\Base as File_Type_Base;
7 use Elementor\Core\Files\File_Types\Json;
8 use Elementor\Core\Files\File_Types\Svg;
9 use Elementor\Core\Files\File_Types\Zip;
10 use Elementor\Core\Utils\Exceptions;
11 use Elementor\User;
12
13 if ( ! defined( 'ABSPATH' ) ) {
14 exit; // Exit if accessed directly.
15 }
16
17 /**
18 * Elementor uploads manager.
19 *
20 * Elementor uploads manager handler class is responsible for handling file uploads that are not done with WP Media.
21 *
22 * @since 3.3.0
23 */
24 class Uploads_Manager extends Base_Object {
25
26 const UNFILTERED_FILE_UPLOADS_KEY = 'elementor_unfiltered_files_upload';
27 const INVALID_FILE_CONTENT = 'Invalid Content In File';
28
29 /**
30 * @var File_Type_Base[]
31 */
32 private $file_type_handlers = [];
33
34 private $allowed_file_extensions;
35
36 /**
37 * @var bool
38 */
39 private $is_elementor_upload = false;
40
41 /**
42 * @var string
43 */
44 private $temp_dir;
45
46 /**
47 * @var array - Array of temp directories that were created during the upload process.
48 */
49 private $temp_unique_dirs = [];
50
51 /**
52 * Register File Types
53 *
54 * To Add a new file type to Elementor, with its own handling logic, you need to add it to the $file_types array here.
55 *
56 * @since 3.3.0
57 * @access public
58 */
59 public function register_file_types() {
60 // All file types that have handlers should be included here.
61 $file_types = [
62 'json' => new Json(),
63 'zip' => new Zip(),
64 'svg' => new Svg(),
65 ];
66
67 foreach ( $file_types as $file_type => $file_handler ) {
68 $this->file_type_handlers[ $file_type ] = $file_handler;
69 }
70 }
71
72 /**
73 * Extract and Validate Zip
74 *
75 * This method accepts a $file array (which minimally should include a 'tmp_name')
76 *
77 * @since 3.3.0
78 * @access public
79 *
80 * @param string $file_path
81 * @param array $allowed_file_types
82 * @return array|\WP_Error
83 */
84 public function extract_and_validate_zip( $file_path, $allowed_file_types = null ) {
85 $result = [];
86
87 /** @var Zip $zip_handler - File Type */
88 $zip_handler = $this->file_type_handlers['zip'];
89
90 // Returns an array of file paths.
91 $extracted = $zip_handler->extract( $file_path, $allowed_file_types );
92
93 if ( is_wp_error( $extracted ) ) {
94 return $extracted;
95 }
96
97 $this->temp_unique_dirs[] = realpath( $extracted['extraction_directory'] );
98
99 // If there are no extracted file names, no files passed the extraction validation.
100 if ( empty( $extracted['files'] ) ) {
101 // TODO: Decide what to do if no files passed the extraction validation
102 return new \WP_Error( 'file_error', self::INVALID_FILE_CONTENT );
103 }
104
105 $result['extraction_directory'] = $extracted['extraction_directory'];
106
107 foreach ( $extracted['files'] as $extracted_file_path ) {
108 // Each file is an array with a 'name' (file path) property.
109 if ( ! is_wp_error( $this->validate_file( [ 'tmp_name' => $extracted_file_path ] ) ) ) {
110 $result['files'][] = $extracted_file_path;
111 }
112 }
113
114 return $result;
115 }
116
117 /**
118 * Handle Elementor Upload
119 *
120 * This method receives a $file array. If the received file is a Base64 string, the $file array should include a
121 * 'fileData' property containing the string, which is decoded and has its contents stored in a temporary file.
122 * If the $file parameter passed is a standard $file array, the 'name' and 'tmp_name' properties are used for
123 * validation.
124 *
125 * The file goes through validation; if it passes validation, the file is returned. Otherwise, an error is returned.
126 *
127 * @since 3.3.0
128 * @access public
129 *
130 * @param array $data {
131 * @type string 'fileName'
132 * @type string 'fileData'
133 * }
134 * @param array $allowed_file_extensions Optional. Array of file types, allowed to pass validation for each upload.
135 *
136 * @return array|\WP_Error
137 */
138 public function handle_elementor_upload( array $data, $allowed_file_extensions = null ) {
139 $normalized_data = [
140 'fileName' => basename( $data['fileName'] ?? '' ),
141 'fileData' => $data['fileData'] ?? null,
142 ];
143
144 // If $file['fileData'] is set, it signals that the passed file is a Base64 string that needs to be decoded and
145 // saved to a temporary file.
146 if ( isset( $normalized_data['fileData'] ) ) {
147 $normalized_data = $this->save_base64_to_tmp_file( $normalized_data, $allowed_file_extensions );
148 }
149
150 $validation_result = $this->validate_file( $normalized_data, $allowed_file_extensions );
151
152 if ( is_wp_error( $validation_result ) ) {
153 return $validation_result;
154 }
155
156 return $normalized_data;
157 }
158
159 /**
160 * are Unfiltered Uploads Enabled
161 *
162 * @since 3.5.0
163 * @access public
164 *
165 * @return bool
166 */
167 final public static function are_unfiltered_uploads_enabled() {
168 $enabled = ! ! get_option( self::UNFILTERED_FILE_UPLOADS_KEY )
169 && Svg::file_sanitizer_can_run()
170 && User::is_current_user_can_upload_json();
171
172 /**
173 * Allow Unfiltered Files Upload.
174 *
175 * Determines whether to enable unfiltered file uploads.
176 *
177 * @since 3.0.0
178 *
179 * @param bool $enabled Whether upload is enabled or not.
180 */
181 $enabled = apply_filters( 'elementor/files/allow_unfiltered_upload', $enabled );
182
183 return $enabled;
184 }
185
186 /**
187 * Handle Elementor WP Media Upload
188 *
189 * Runs on the 'wp_handle_upload_prefilter' filter.
190 *
191 * @since 3.2.0
192 * @access public
193 *
194 * @param $file
195 * @return mixed
196 */
197 public function handle_elementor_wp_media_upload( $file ) {
198 // If it isn't a file uploaded by Elementor, we do not intervene.
199 if ( ! $this->is_elementor_wp_media_upload() ) {
200 return $file;
201 }
202
203 $result = $this->validate_file( $file );
204
205 if ( is_wp_error( $result ) ) {
206 $file['error'] = $result->get_error_message();
207 }
208
209 return $file;
210 }
211
212 /**
213 * Get File Type Handler
214 *
215 * Initialize the proper file type handler according to the file extension
216 * and assign it to the file type handlers array.
217 *
218 * @since 3.3.0
219 * @access public
220 *
221 * @param string|null $file_extension - file extension
222 * @return File_Type_Base[]|File_Type_Base
223 */
224 public function get_file_type_handlers( $file_extension = null ) {
225 return self::get_items( $this->file_type_handlers, $file_extension );
226 }
227
228 /**
229 * Check filetype and ext
230 *
231 * A workaround for upload validation which relies on a PHP extension (fileinfo)
232 * with inconsistent reporting behaviour.
233 * ref: https://core.trac.wordpress.org/ticket/39550
234 * ref: https://core.trac.wordpress.org/ticket/40175
235 *
236 * @since 3.5.0
237 * @access public
238 *
239 * @param $data
240 * @param $file
241 * @param $filename
242 * @param $mimes
243 *
244 * @return mixed
245 */
246 public function check_filetype_and_ext( $data, $file, $filename, $mimes ) {
247 if ( ! empty( $data['ext'] ) && ! empty( $data['type'] ) ) {
248 return $data;
249 }
250
251 $wp_file_type = wp_check_filetype( $filename, $mimes );
252
253 $file_type_handlers = $this->get_file_type_handlers();
254
255 if ( isset( $file_type_handlers[ $wp_file_type['ext'] ] ) ) {
256 $file_type_handler = $file_type_handlers[ $wp_file_type['ext'] ];
257
258 $data['ext'] = $file_type_handler->get_file_extension();
259 $data['type'] = $file_type_handler->get_mime_type();
260 }
261
262 return $data;
263 }
264
265 /**
266 * Remove File Or Directory
267 *
268 * Directory is deleted recursively with all of its contents (subdirectories and files).
269 *
270 * @since 3.3.0
271 * @access public
272 *
273 * @param string $path
274 */
275 public function remove_file_or_dir( $path ) {
276 if ( is_dir( $path ) ) {
277 $this->remove_directory_with_files( $path );
278 } elseif ( is_file( $path ) ) {
279 unlink( $path );
280 }
281 }
282
283 /**
284 * Safely removes a file or directory if it resides within the designated temporary folder.
285 *
286 * This method validates that the provided file path is located within the temporary directory
287 * before proceeding with the removal. If the path is outside the temporary directory,
288 * no action is taken to prevent unintended deletions.
289 *
290 * @since 3.19.0
291 * @access public
292 *
293 * @param string $path
294 *
295 */
296 public function remove_temp_file_or_dir( $path ) {
297 $realpath = realpath( $path );
298 if ( false === $realpath ) {
299 return;
300 }
301
302 if ( is_uploaded_file( $path ) ) {
303 $this->remove_file_or_dir( $path );
304 return;
305 }
306
307 foreach ( $this->temp_unique_dirs as $temp_dir ) {
308 if ( strpos( $realpath, $temp_dir ) === 0 ) {
309 $this->remove_file_or_dir( $path );
310 break;
311 }
312 }
313 }
314
315 /**
316 * Create Temp File
317 *
318 * Create a random temporary file.
319 *
320 * @since 3.3.0
321 * @access public
322 *
323 * @param string $file_content
324 * @param string $file_name
325 * @return string|\WP_Error
326 */
327 public function create_temp_file( $file_content, $file_name ) {
328 $file_name = str_replace( ' ', '', sanitize_file_name( $file_name ) );
329
330 if ( empty( $file_name ) ) {
331 return new \WP_Error( 'invalid_file_name', esc_html__( 'Invalid file name.', 'elementor' ) );
332 }
333
334 $temp_filename = $this->create_unique_dir() . $file_name;
335
336 /**
337 * Temp File Path
338 *
339 * Allows modifying the full path of the temporary file.
340 *
341 * @since 3.7.0
342 *
343 * @param string full path to file
344 */
345 $temp_filename = apply_filters( 'elementor/files/temp-file-path', $temp_filename );
346
347 file_put_contents( $temp_filename, $file_content ); // phpcs:ignore
348
349 return $temp_filename;
350 }
351
352 /**
353 * Get Temp Directory
354 *
355 * Get the temporary files directory path. If the directory does not exist, this method creates it.
356 *
357 * @since 3.3.0
358 * @access public
359 *
360 * @return string $temp_dir
361 */
362 public function get_temp_dir() {
363 if ( ! $this->temp_dir ) {
364 $wp_upload_dir = wp_upload_dir();
365
366 $temp_dir = implode( DIRECTORY_SEPARATOR, [ $wp_upload_dir['basedir'], 'elementor', 'tmp' ] ) . DIRECTORY_SEPARATOR;
367
368 /**
369 * Temp File Path
370 *
371 * Allows modifying the full path of the temporary file.
372 *
373 * @since 3.7.0
374 *
375 * @param string temporary directory
376 */
377 $this->temp_dir = apply_filters( 'elementor/files/temp-dir', $temp_dir );
378
379 if ( ! is_dir( $this->temp_dir ) ) {
380 wp_mkdir_p( $this->temp_dir );
381 }
382 }
383
384 return $this->temp_dir;
385 }
386
387 /**
388 * Create Unique Temp Dir
389 *
390 * Create a unique temporary directory
391 *
392 * @since 3.3.0
393 * @access public
394 *
395 * @return string the new directory path
396 */
397 public function create_unique_dir() {
398 $unique_dir_path = $this->get_temp_dir() . uniqid() . DIRECTORY_SEPARATOR;
399
400 wp_mkdir_p( $unique_dir_path );
401
402 // Store uniqid and unique_dir_path pair
403 $this->temp_unique_dirs[] = realpath( $unique_dir_path );
404
405 return $unique_dir_path;
406 }
407
408 /**
409 * Register Ajax Actions
410 *
411 * Runs on the 'elementor/ajax/register_actions' hook. Receives the AJAX module as a parameter and registers
412 * callbacks for specified action IDs.
413 *
414 * @since 3.5.0
415 * @access public
416 *
417 * @param Ajax $ajax
418 */
419 public function register_ajax_actions( Ajax $ajax ) {
420 $ajax->register_ajax_action( 'enable_unfiltered_files_upload', [ $this, 'enable_unfiltered_files_upload' ] );
421 }
422
423 /**
424 * Set Unfiltered Files Upload
425 *
426 * @since 3.5.0
427 * @access public
428 */
429 public function enable_unfiltered_files_upload() {
430 if ( ! current_user_can( 'manage_options' ) ) {
431 return;
432 }
433
434 update_option( self::UNFILTERED_FILE_UPLOADS_KEY, 1 );
435 }
436
437 /**
438 * Support Unfiltered File Uploads
439 *
440 * When uploading a file within Elementor, this method adds the registered
441 * file types to WordPress' allowed mimes list. This will only happen if the user allowed unfiltered file uploads
442 * in Elementor's settings in the admin dashboard.
443 *
444 * @since 3.5.0
445 * @access public
446 *
447 * @param array $allowed_mimes
448 * @return array allowed mime types
449 */
450 final public function support_unfiltered_elementor_file_uploads( $allowed_mimes ) {
451 if ( $this->is_elementor_upload() && $this->are_unfiltered_uploads_enabled() ) {
452 foreach ( $this->file_type_handlers as $file_type_handler ) {
453 $allowed_mimes[ $file_type_handler->get_file_extension() ] = $file_type_handler->get_mime_type();
454 }
455 }
456
457 return $allowed_mimes;
458 }
459
460 /**
461 * Set Elementor Upload State
462 *
463 * @since 3.5.0
464 * @access public
465 *
466 * @param $state
467 */
468 public function set_elementor_upload_state( $state ) {
469 $this->is_elementor_upload = $state;
470 }
471
472 /**
473 * Is Elementor Upload
474 *
475 * This method checks if the current session includes a request to upload files made via Elementor.
476 *
477 * @since 3.5.0
478 * @access private
479 *
480 * @return bool
481 */
482 private function is_elementor_upload() {
483 return $this->is_elementor_upload || $this->is_elementor_media_upload() || $this->is_elementor_wp_media_upload();
484 }
485
486 /**
487 * Is Elementor Media Upload
488 *
489 * Checks whether the current request includes uploading files via Elementor which are not destined for the Media
490 * Library.
491 *
492 * @since 3.5.0
493 * @access public
494 *
495 * @return bool
496 */
497 public function is_elementor_media_upload() {
498 // Sometimes `uploadTypeCaller` passed as a GET parameter when using the WP Media Library REST API, where the
499 // whole request body is occupied by the uploaded file.
500 return isset( $_REQUEST['uploadTypeCaller'] ) && 'elementor-media-upload' === $_REQUEST['uploadTypeCaller']; // phpcs:ignore
501 }
502
503 /**
504 * Is Elementor WP Media Upload
505 *
506 * Checks whether the current request is a request to upload files into the WP Media Library via Elementor.
507 *
508 * @since 3.3.0
509 * @access private
510 *
511 * @return bool
512 */
513 private function is_elementor_wp_media_upload() {
514 return isset( $_REQUEST['uploadTypeCaller'] ) && 'elementor-wp-media-upload' === $_REQUEST['uploadTypeCaller']; // phpcs:ignore
515 }
516
517 /**
518 * Add File Extension To Allowed Extensions List
519 *
520 * @since 3.3.0
521 * @access private
522 *
523 * @param string $file_type
524 */
525 private function add_file_extension_to_allowed_extensions_list( $file_type ) {
526 $file_handler = $this->file_type_handlers[ $file_type ];
527
528 $file_extension = $file_handler->get_file_extension();
529
530 // Only add the file extension to the list if it doesn't already exist in it.
531 if ( ! in_array( $file_extension, $this->allowed_file_extensions, true ) ) {
532 $this->allowed_file_extensions[] = $file_extension;
533 }
534 }
535
536 /**
537 * Save Base64 as File
538 *
539 * Saves a Base64 string as a .tmp file in Elementor's temporary files directory.
540 *
541 * @since 3.3.0
542 * @access private
543 *
544 * @param $file
545 * @param array|null $allowed_file_extensions
546 *
547 * @return array|\WP_Error
548 */
549 private function save_base64_to_tmp_file( $file, $allowed_file_extensions = null ) {
550 $file_extension = pathinfo( $file['fileName'], PATHINFO_EXTENSION );
551 $is_file_type_allowed = $this->is_file_type_allowed( $file_extension, $allowed_file_extensions );
552
553 if ( is_wp_error( $is_file_type_allowed ) ) {
554 return $is_file_type_allowed;
555 }
556
557 $file_content = base64_decode( $file['fileData'] ); // phpcs:ignore
558
559 // If the decode fails
560 if ( ! $file_content ) {
561 return new \WP_Error( 'file_error', self::INVALID_FILE_CONTENT );
562 }
563
564 $temp_filename = $this->create_temp_file( $file_content, $file['fileName'] );
565
566 if ( is_wp_error( $temp_filename ) ) {
567 return $temp_filename;
568 }
569
570 return [
571 // the original uploaded file name
572 'name' => $file['fileName'],
573 // The path to the temporary file
574 'tmp_name' => $temp_filename,
575 ];
576 }
577
578 /**
579 * Validate File
580 *
581 * @since 3.3.0
582 * @access private
583 *
584 * @param array $file
585 * @param array $file_extensions Optional
586 * @return bool|\WP_Error
587 */
588 private function validate_file( array $file, $file_extensions = [] ) {
589 $is_name_valid = empty( $file['name'] ) || basename( $file['name'] ) === $file['name'];
590 $is_tmp_name_valid = empty( $file['tmp_name'] ) || realpath( $file['tmp_name'] ) !== false;
591
592 if ( ( empty( $file['name'] ) && empty( $file['tmp_name'] ) ) || ! $is_name_valid || ! $is_tmp_name_valid ) {
593 return new \WP_Error(
594 Exceptions::FORBIDDEN,
595 esc_html__( 'This file is not allowed for security reasons.', 'elementor' )
596 );
597 }
598
599 $uploaded_file_name = isset( $file['name'] ) ? $file['name'] : $file['tmp_name'];
600
601 $file_extension = pathinfo( $uploaded_file_name, PATHINFO_EXTENSION );
602
603 if ( ! $this->is_elementor_wp_media_upload() ) {
604 $is_file_type_allowed = $this->is_file_type_allowed( $file_extension, $file_extensions );
605
606 if ( is_wp_error( $is_file_type_allowed ) ) {
607 return $is_file_type_allowed;
608 }
609 }
610
611 $file_type_handler = $this->get_file_type_handlers( $file_extension );
612
613 // If Elementor does not have a handler for this file type, don't block it.
614 if ( ! $file_type_handler ) {
615 return true;
616 }
617
618 // If there is a File Type Handler for the uploaded file, it means it is a non-standard file type. In this case,
619 // we check if unfiltered file uploads are enabled or not before allowing it.
620 if ( ! self::are_unfiltered_uploads_enabled() ) {
621 $error = 'json' === $file_extension
622 ? esc_html__( 'You don\'t have permission to upload JSON files. Contact the administrator.', 'elementor' )
623 : esc_html__( 'This file is not allowed for security reasons.', 'elementor' );
624 return new \WP_Error( Exceptions::FORBIDDEN, $error );
625 }
626
627 // Here is each file type handler's chance to run its own specific validations
628 return $file_type_handler->validate_file( $file );
629 }
630
631 /**
632 * Is File Type Allowed
633 *
634 * Checks whether the passed file extension is allowed for upload.
635 *
636 * @since 3.5.0
637 * @access private
638 *
639 * @param $file_extension
640 * @param $filtered_file_extensions
641 * @return bool|\WP_Error
642 */
643 private function is_file_type_allowed( $file_extension, $filtered_file_extensions ) {
644 $allowed_file_extensions = $this->get_allowed_file_extensions();
645
646 if ( $filtered_file_extensions ) {
647 $allowed_file_extensions = array_intersect( $allowed_file_extensions, $filtered_file_extensions );
648 }
649
650 $is_allowed = false;
651
652 // Check if the file type (extension) is in the allowed extensions list. If it is a non-standard file type (not
653 // enabled by default in WordPress) and unfiltered file uploads are not enabled, it will not be in the allowed
654 // file extensions list.
655 foreach ( $allowed_file_extensions as $allowed_extension ) {
656 if ( preg_match( '/' . $allowed_extension . '/', $file_extension ) ) {
657 $is_allowed = true;
658
659 break;
660 }
661 }
662
663 if ( ! $is_allowed ) {
664 $is_allowed = new \WP_Error( Exceptions::FORBIDDEN, 'Uploading this file type is not allowed.' );
665 }
666
667 /**
668 * Elementor File Type Allowed
669 *
670 * Allows setting file types
671 *
672 * @since 3.5.0
673 *
674 * @param bool|\WP_Error $is_allowed
675 */
676 return apply_filters( 'elementor/files/allow-file-type/' . $file_extension, $is_allowed );
677 }
678
679 /**
680 * Remove Directory with Files
681 *
682 * @since 3.3.0
683 * @access private
684 *
685 * @param string $dir
686 * @return bool
687 */
688 private function remove_directory_with_files( $dir ) {
689 $dir_iterator = new \RecursiveDirectoryIterator( $dir, \RecursiveDirectoryIterator::SKIP_DOTS );
690
691 foreach ( new \RecursiveIteratorIterator( $dir_iterator, \RecursiveIteratorIterator::CHILD_FIRST ) as $name => $item ) {
692 if ( is_dir( $name ) ) {
693 rmdir( $name );
694 } elseif ( is_file( $name ) ) {
695 unlink( $name );
696 }
697 }
698
699 return rmdir( $dir );
700 }
701
702 /**
703 * Get Allowed File Extensions
704 *
705 * Retrieve an array containing the list of file extensions allowed for upload.
706 *
707 * @since 3.3.0
708 * @access private
709 *
710 * @return array file extension/s
711 */
712 private function get_allowed_file_extensions() {
713 if ( ! $this->allowed_file_extensions ) {
714 $this->allowed_file_extensions = array_keys( get_allowed_mime_types() );
715
716 foreach ( $this->get_file_type_handlers() as $file_type => $handler ) {
717 if ( $handler->is_upload_allowed() ) {
718 // Add the file extension to the allowed extensions list only if unfiltered files upload is enabled.
719 $this->add_file_extension_to_allowed_extensions_list( $file_type );
720 }
721 }
722 }
723
724 return $this->allowed_file_extensions;
725 }
726
727 public function __construct() {
728 $this->register_file_types();
729
730 add_filter( 'upload_mimes', [ $this, 'support_unfiltered_elementor_file_uploads' ] );
731 add_filter( 'wp_handle_upload_prefilter', [ $this, 'handle_elementor_wp_media_upload' ] );
732 add_filter( 'wp_check_filetype_and_ext', [ $this, 'check_filetype_and_ext' ], 10, 4 );
733
734 // Ajax.
735 add_action( 'elementor/ajax/register_actions', [ $this, 'register_ajax_actions' ] );
736 }
737 }
738