PluginProbe
Elementor Website Builder – more than just a page builder / 3.25.4
Elementor Website Builder – more than just a page builder v3.25.4
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.25.4, at core/files/uploads-manager.php

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