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

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