PluginProbe ʕ •ᴥ•ʔ
Kirki – Freeform Page Builder, Website Builder & Customizer / 6.1.0
Kirki – Freeform Page Builder, Website Builder & Customizer v6.1.0
6.1.1 6.1.0 6.0.14 6.0.13 6.0.12 6.0.11 6.0.10 6.0.9 6.0.8 6.0.7 6.0.6 6.0.5 6.0.4 6.0.3 6.0.2 6.0.1 3.1.3 3.1.4 3.1.5 3.1.6 3.1.7 3.1.8 3.1.9 4.0.19 4.0.20 4.0.21 4.0.22 4.0.23 4.0.24 4.1 4.2.0 5.0.0 5.1.0 5.1.1 5.2.0 5.2.1 5.2.2 5.2.3 6.0.0 trunk 3.0.40 3.0.41 3.0.42 3.0.43 3.0.44 3.0.45 3.1.0 3.1.1 3.1.2
kirki / includes / Ajax / Media.php
kirki / includes / Ajax Last commit date
Collaboration 3 weeks ago Apps.php 3 weeks ago Collection.php 1 month ago Comments.php 2 months ago DynamicContent.php 1 month ago ExportImport.php 3 days ago Form.php 3 weeks ago Media.php 3 days ago Page.php 3 days ago PageSettings.php 3 weeks ago RBAC.php 3 weeks ago Symbol.php 3 weeks ago Taxonomy.php 2 months ago TemplateExportImport.php 2 months ago UserData.php 3 weeks ago Users.php 2 months ago Walkthrough.php 2 months ago WordpressData.php 2 months ago WpAdmin.php 3 days ago
Media.php
1299 lines
1 <?php
2
3 /**
4 * Media api controller
5 *
6 * @package kirki
7 */
8
9 namespace Kirki\Ajax;
10
11 if ( ! defined( 'ABSPATH' ) ) {
12 exit; // Exit if accessed directly.
13 }
14
15 use Kirki\HelperFunctions;
16 use DOMDocument;
17 use DOMXPath;
18 use InvalidArgumentException;
19 use ZipArchive;
20 use enshrined\svgSanitize\Sanitizer;
21 use Kirki\App\Services\FontService;
22
23 /**
24 * Media API Class
25 */
26 class Media {
27
28
29 /**
30 * Format media data
31 *
32 * @param object $post wp post.
33 * @return object formatted_data.
34 */
35 public function format_media_data( $post ) {
36 $media_categories = array(
37 'image' => KIRKI_SUPPORTED_MEDIA_TYPES['image'],
38 'video' => KIRKI_SUPPORTED_MEDIA_TYPES['video'],
39 'svg' => KIRKI_SUPPORTED_MEDIA_TYPES['svg'],
40 'audio' => KIRKI_SUPPORTED_MEDIA_TYPES['audio'],
41 'lottie' => KIRKI_SUPPORTED_MEDIA_TYPES['lottie'],
42 'pdf' => KIRKI_SUPPORTED_MEDIA_TYPES['pdf'],
43 'json' => KIRKI_SUPPORTED_MEDIA_TYPES['json'],
44 );
45
46 $formatted_data = array();
47
48 foreach ( $post as $key => $value ) {
49 if ( 'ID' === $key ) {
50 $formatted_data['id'] = $value;
51 } elseif ( 'post_mime_type' === $key ) {
52 foreach ( $media_categories as $category => $mime_types ) {
53 if ( in_array( $value, $mime_types, true ) ) {
54 $formatted_data['category'] = 'svg' === $category ? 'image' : $category;
55 $formatted_data['type'] = $value;
56 }
57 }
58 } elseif ( 'guid' === $key ) {
59 $formatted_data['url'] = $value;
60 } elseif ( 'post_name' === $key ) {
61 $formatted_data['name'] = $value;
62 $formatted_data['alt'] = $value;
63 }
64 }
65
66 // media file size converting to human readable format
67 $formatted_data['file_size'] = filesize( get_attached_file( $post->ID ) );
68 $formatted_data['file_size'] = size_format( $formatted_data['file_size'] );
69
70 // media file extension
71 $file_path = get_attached_file( $post->ID );
72 $formatted_data['file_extension'] = pathinfo( $file_path, PATHINFO_EXTENSION );
73
74 $formatted_data['trash'] = false;
75
76 $formatted_data['thumbnail'] = wp_get_attachment_image_url( $post->ID );
77
78 return $formatted_data;
79 }
80
81 /**
82 * Upload Media api
83 *
84 * @return void wp_send_json
85 */
86 public static function upload_media() {
87 $data = array();
88 //phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
89 $files = isset( $_FILES['files'] ) ? wp_unslash( $_FILES['files'] ) : null;
90 if ( ! $files ) {
91 wp_send_json(
92 array(
93 'status' => 'fail',
94 'message' => 'Invalid files',
95 )
96 );
97 die();
98 }
99 foreach ( $files['name'] as $key => $value ) {
100 if ( isset( $files['name'][ $key ] ) ) {
101 $tmp_name = wp_unslash( $files['tmp_name'][ $key ] );
102 $type = wp_unslash( $files['type'][ $key ] );
103 if ( 'image/svg+xml' === $type && ! ( new self() )->validate_svg( $tmp_name ) ) {
104 wp_send_json(
105 array(
106 'status' => 'fail',
107 'message' => 'Invalid SVG file',
108 )
109 );
110 die();
111 }
112 $file = array(
113 'name' => wp_unslash( $files['name'][ $key ] ),
114 'type' => $type,
115 'tmp_name' => $tmp_name,
116 'error' => wp_unslash( $files['error'][ $key ] ),
117 'size' => wp_unslash( $files['size'][ $key ] ),
118 );
119
120 $attachment_id = self::upload_single_media( $file );
121
122 if ( is_wp_error( $attachment_id ) ) {
123 $message = 'Some error occurred, please try again';
124 if ( isset( $attachment_id->errors['upload_error'][0] ) ) {
125 $message = $attachment_id->errors['upload_error'][0];
126 }
127 wp_send_json(
128 array(
129 'status' => 'fail',
130 'message' => $message,
131 )
132 );
133 } else {
134 $post = get_post( $attachment_id );
135 $formatted_data = ( new self() )->format_media_data( $post );
136 $data[] = $formatted_data;
137 }
138 }
139 }
140
141 wp_send_json(
142 array(
143 'status' => 'success',
144 'data' => $data,
145 )
146 );
147 }
148
149 public static function upload_single_media( $file ) {
150 $file = self::kirki_handle_upload_prefilter( $file );
151
152 require_once ABSPATH . 'wp-admin/includes/image.php';
153 require_once ABSPATH . 'wp-admin/includes/file.php';
154 require_once ABSPATH . 'wp-admin/includes/media.php';
155
156 $_FILES = array( 'upload_file' => $file );
157
158 // $attachment_id = media_handle_upload('upload_file', 0);
159 $attachment_id = media_handle_upload(
160 'upload_file',
161 0,
162 array(),
163 array(
164 'test_form' => false,
165 'action' => 'upload-attachment',
166 )
167 );
168
169 return $attachment_id;
170 }
171
172
173 /**
174 * Upload Media Pre filter.
175 * This method will call from 'wp_handle_upload_prefilter'this hook. which is imeplemented inside plugin init events file.
176 *
177 * @param array $file Original file.
178 * @return array $file Converted file. If image optimization is enabled from kirki dashboard menu. then any image related file will converted to webp and return as file.
179 */
180 public static function kirki_handle_upload_prefilter( $file ) {
181 $common_data = WpAdmin::get_common_data( true );
182 if ( ! isset( $common_data['image_optimization'] ) || ! $common_data['image_optimization'] ) {
183 return $file;
184 }
185 $filetype = wp_check_filetype( $file['name'] );
186
187 if ( 'image/jpeg' === $filetype['type'] || 'image/png' === $filetype['type'] ) {
188 if ( ! extension_loaded( 'gd' ) ) {
189 return $file;
190 }
191
192 $image = ( 'image/jpeg' === $filetype['type'] ) ? imagecreatefromjpeg( $file['tmp_name'] ) : imagecreatefrompng( $file['tmp_name'] );
193 $webp_image_path = preg_replace( '/\\.[^.\\s]{3,4}$/', '', $file['tmp_name'] ) . '.webp';
194
195 imagewebp( $image, $webp_image_path, 80 );
196 imagedestroy( $image );
197
198 // copy the webp image back to the original tmp location.
199 copy( $webp_image_path, $file['tmp_name'] );
200
201 $file['name'] = preg_replace( '/\\.[^.\\s]{3,4}$/', '', $file['name'] ) . '.webp';
202 $file['type'] = 'image/webp';
203 }
204
205 return $file;
206 }
207
208 /**
209 * Convert images generated sizes to webp format.
210 * This method will call from 'wp_generate_attachment_metadata' this filter hook. which is imeplemented inside plugin init events file.
211 *
212 * @param array $metadata uploaded files attachment meta data.
213 * @return array $metadata If image optimization is enabled from kirki dashboard menu then it will convert all images to webp otherwise return default metadata.
214 */
215 public static function kirki_convert_sizes_to_webp( $metadata ) {
216 $common_data = WpAdmin::get_common_data( true );
217 if (
218 ! isset( $common_data['image_optimization'] ) ||
219 ! $common_data['image_optimization'] ||
220 ! isset( $metadata['sizes'] ) ||
221 ! isset( $metadata['image_meta'] )
222 ) {
223 return $metadata;
224 }
225 if ( ! isset( $metadata['file'] ) ) {
226 return $metadata;
227 }
228 $upload_dir = wp_upload_dir();
229 $original_image_dir = trailingslashit( $upload_dir['basedir'] ) . dirname( $metadata['file'] );
230 $image_files = array_merge( array( $metadata['file'] ), array_column( $metadata['sizes'], 'file' ) );
231
232 foreach ( $image_files as $image_file ) {
233 $original_image_path = trailingslashit( $original_image_dir ) . $image_file;
234 $webp_image_path = preg_replace( '/\\.[^.\\s]{3,4}$/', '', $original_image_path ) . '.webp';
235
236 if ( ! file_exists( $webp_image_path ) ) {
237 if ( ! extension_loaded( 'gd' ) ) {
238 continue;
239 }
240
241 $image_info = getimagesize( $original_image_path );
242
243 switch ( $image_info[2] ) {
244 case IMAGETYPE_JPEG:
245 $image = imagecreatefromjpeg( $original_image_path );
246 break;
247 case IMAGETYPE_PNG:
248 $image = imagecreatefrompng( $original_image_path );
249 break;
250 case IMAGETYPE_GIF:
251 $image = imagecreatefromgif( $original_image_path );
252 break;
253 default:
254 return $metadata;
255 }
256
257 imagewebp( $image, $webp_image_path, 80 );
258 imagedestroy( $image );
259 }
260
261 // Delete the original image.
262 if ( file_exists( $original_image_path ) ) {
263 wp_delete_file( $original_image_path );
264 }
265
266 $metadata = self::kirki_replace_file_in_metadata( $metadata, $image_file, basename( $webp_image_path ) );
267 }
268
269 return $metadata;
270 }
271
272 /**
273 * Replace file to meta data.
274 *
275 * @param array $metadata files attachment metadata.
276 * @param string $old_file old file original path. (jpg, png, gif).
277 * @param string $new_file new file original path.
278 *
279 * @return array $metadata files updated attachment metadata.
280 */
281 private static function kirki_replace_file_in_metadata( $metadata, $old_file, $new_file ) {
282 if ( $old_file === $metadata['file'] ) {
283 $metadata['file'] = str_replace( $old_file, $new_file, $metadata['file'] );
284 }
285
286 foreach ( $metadata['sizes'] as $size => $info ) {
287 if ( $old_file === $info['file'] ) {
288 $metadata['sizes'][ $size ]['file'] = $new_file;
289 }
290 }
291
292 return $metadata;
293 }
294
295 /**
296 * Upload font zip
297 *
298 * @return void wp_send_json.
299 */
300 public static function upload_font_zip() {
301 require_once ABSPATH . 'wp-admin/includes/image.php';
302 require_once ABSPATH . 'wp-admin/includes/file.php';
303 require_once ABSPATH . 'wp-admin/includes/media.php';
304
305 global $wp_filesystem;
306 if ( ! is_object( $wp_filesystem ) ) {
307 WP_Filesystem();
308 }
309
310 $file = isset( $_FILES['file'] ) ? wp_unslash( $_FILES['file'] ) : null;
311 $uploads = wp_upload_dir();
312 $multiple_files = self::normalize_multiple_files( isset( $_FILES['files'] ) ? $_FILES['files'] : null );
313
314 if ( ! empty( $multiple_files ) ) {
315 self::handle_multiple_raw_font_upload( $multiple_files, $wp_filesystem, $uploads );
316 return;
317 }
318
319 if ( ! $file || empty( $file['tmp_name'] ) || ! is_uploaded_file( $file['tmp_name'] ) ) {
320 wp_send_json(
321 array(
322 'status' => 'failure',
323 'message' => 'Invalid file',
324 ),
325 );
326 }
327
328 $filename = sanitize_file_name( $file['name'] );
329 $file_extension = strtolower( pathinfo( $filename, PATHINFO_EXTENSION ) );
330 $allowed_font_files = array( 'ttf', 'otf', 'woff', 'woff2' );
331
332 if ( 'zip' === $file_extension ) {
333 self::handle_font_zip_upload( $file, $filename, $wp_filesystem, $uploads );
334 return;
335 }
336
337 if ( in_array( $file_extension, $allowed_font_files, true ) ) {
338 self::handle_raw_font_upload( $file, $filename, $file_extension, $wp_filesystem, $uploads );
339 return;
340 }
341
342 wp_send_json(
343 array(
344 'status' => 'failure',
345 'message' => 'Only .zip, .ttf, .otf, .woff, .woff2 files are allowed',
346 )
347 );
348 }
349
350 private static function handle_raw_font_upload( $file, $filename, $file_extension, $wp_filesystem, $uploads, $family_override = null ) {
351 $allowed_font_files = array( 'ttf', 'otf', 'woff', 'woff2' );
352 if ( ! in_array( $file_extension, $allowed_font_files, true ) ) {
353 wp_send_json(
354 array(
355 'status' => 'failure',
356 'message' => 'Unsupported font file type',
357 )
358 );
359 }
360
361 $temp_dir = trailingslashit( $uploads['basedir'] ) .'kirki-font-temp/';
362 wp_mkdir_p( $temp_dir );
363 $temp_font = $temp_dir . wp_unique_filename( $temp_dir, $filename );
364
365 global $wp_filesystem;
366 if ( ! is_object( $wp_filesystem ) ) {
367 WP_Filesystem();
368 }
369
370 if ( ! $wp_filesystem->move( $file['tmp_name'], $temp_font ) ) {
371 wp_send_json(
372 array(
373 'status' => 'failure',
374 'message' => 'Font upload failed',
375 )
376 );
377 }
378
379 $filetype = wp_check_filetype_and_ext( $temp_font, $filename );
380 $detected_ext = ! empty( $filetype['ext'] ) ? strtolower( $filetype['ext'] ) : $file_extension;
381 if ( empty( $detected_ext ) || ! in_array( $detected_ext, $allowed_font_files, true ) ) {
382 wp_delete_file( $temp_font );
383 wp_send_json(
384 array(
385 'status' => 'failure',
386 'message' => 'Invalid font file type',
387 ),
388 );
389 }
390
391 $font_meta = self::get_font_metadata_from_filename( pathinfo( $filename, PATHINFO_FILENAME ) );
392 if ( $family_override && $family_override !== $font_meta['family'] ) {
393 $font_meta['family'] = $family_override;
394 }
395 $renamed_folder_name = self::normalize_font_family_slug( $font_meta['family'] );
396 $font_folder = trailingslashit( $uploads['basedir'] ) .'kirki-fonts/' . $renamed_folder_name;
397
398 if ( is_dir( $font_folder ) ) {
399 self::delete_dir( $font_folder );
400 }
401
402 wp_mkdir_p( $font_folder );
403
404 $stored_font_name = wp_unique_filename( $font_folder, $filename );
405 $wp_filesystem->move( $temp_font, $font_folder . '/' . $stored_font_name );
406
407 $stylesheet = self::build_single_font_stylesheet( $font_meta, $stored_font_name, $detected_ext, $font_meta['family'] );
408 $wp_filesystem->put_contents( $font_folder . '/stylesheet.css', $stylesheet );
409
410 $data = array(
411 'fontUrl' => trailingslashit( $uploads['baseurl'] ) .'kirki-fonts/' . $renamed_folder_name . '/stylesheet.css',
412 'family' => $font_meta['family'],
413 'variants' => array( $font_meta['variant'] ),
414 'subsets' => array( 'latin' ),
415 'uploaded' => true,
416 'version' => 'v1',
417 );
418
419 wp_send_json(
420 array(
421 'status' => 'success',
422 'data' => $data,
423 )
424 );
425 }
426
427 private static function handle_font_zip_upload( $file, $filename, $wp_filesystem, $uploads ) {
428 $temp_dir = trailingslashit( $uploads['basedir'] ) .'kirki-font-temp/';
429 wp_mkdir_p( $temp_dir );
430
431 $temp_zip = $temp_dir . wp_unique_filename( $temp_dir, $filename );
432
433 global $wp_filesystem;
434 if ( ! is_object( $wp_filesystem ) ) {
435 WP_Filesystem();
436 }
437
438 if ( ! $wp_filesystem->move( $file['tmp_name'], $temp_zip ) ) {
439 wp_send_json(
440 array(
441 'status' => 'failure',
442 'message' => 'Zip file upload failed',
443 )
444 );
445 }
446
447 $zip = new ZipArchive();
448 if ( $zip->open( $temp_zip ) !== true ) {
449 wp_delete_file( $temp_zip );
450 wp_send_json(
451 array(
452 'status' => 'failure',
453 'message' => 'Invalid zip file',
454 )
455 );
456 }
457
458 $blocked_extensions = array( 'php', 'phtml', 'php3', 'php4', 'php5', 'php7', 'phar', 'inc' );
459
460 for ( $i = 0; $i < $zip->numFiles; $i++ ) {
461 $entry = wp_normalize_path( $zip->getNameIndex( $i ) );
462 if ( strpos( $entry, '../' ) !== false || strpos( $entry, '..\\' ) !== false ) {
463 $zip->close();
464 wp_delete_file( $temp_zip );
465 wp_send_json(
466 array(
467 'status' => 'failure',
468 'message' => 'Invalid zip contents',
469 )
470 );
471 }
472
473 $ext = strtolower( pathinfo( $entry, PATHINFO_EXTENSION ) );
474 if ( $ext && in_array( $ext, $blocked_extensions, true ) ) {
475 $zip->close();
476 wp_delete_file( $temp_zip );
477 wp_send_json(
478 array(
479 'status' => 'failure',
480 'message' => 'Executable files are not allowed',
481 )
482 );
483 }
484 }
485
486 $folder_name = sanitize_file_name( pathinfo( $filename, PATHINFO_FILENAME ) );
487 $upload_dir = trailingslashit( $uploads['basedir'] ) .'kirki-fonts/' . $folder_name;
488 wp_mkdir_p( $upload_dir );
489
490 $zip->extractTo( $upload_dir );
491 $zip->close();
492 wp_delete_file( $temp_zip );
493
494 if ( ! file_exists( $upload_dir . '/stylesheet.css' ) ) {
495 self::delete_dir( $upload_dir );
496 wp_send_json(
497 array(
498 'status' => 'failure',
499 'message' => 'Invalid data',
500 )
501 );
502 }
503
504 $style_sheet_string = $wp_filesystem->get_contents( $upload_dir . '/stylesheet.css' );
505 $font_family_name = self::get_common_family_name( $style_sheet_string );
506
507 if ( ! $font_family_name ) {
508 self::delete_dir( $upload_dir );
509 wp_send_json(
510 array(
511 'status' => 'failure',
512 'message' => 'Font family name not found',
513 )
514 );
515 }
516
517 $result = self::get_rewritten_style_and_variants( $style_sheet_string, $font_family_name );
518 $wp_filesystem->put_contents( $upload_dir . '/stylesheet.css', $result['css'] );
519
520 $renamed_folder_name = self::normalize_font_family_slug( $font_family_name );
521 $new_folder_path = trailingslashit( $uploads['basedir'] ) .'kirki-fonts/' . $renamed_folder_name;
522
523 if ( is_dir( $new_folder_path ) ) {
524 self::delete_dir( $new_folder_path );
525 }
526
527 $wp_filesystem->move( $upload_dir, $new_folder_path );
528
529 self::remove_extra_files_from_font_folder( $new_folder_path );
530
531 wp_send_json(
532 array(
533 'status' => 'success',
534 'data' => array(
535 'fontUrl' => trailingslashit( $uploads['baseurl'] ) .'kirki-fonts/' . $renamed_folder_name . '/stylesheet.css',
536 'family' => $font_family_name,
537 'variants' => $result['variants'],
538 'subsets' => array( 'latin' ),
539 'uploaded' => true,
540 'version' => 'v1',
541 ),
542 )
543 );
544 }
545
546 private static function get_font_metadata_from_filename( $filename ) {
547 $pattern = strtolower( $filename );
548 $style = ( strpos( $pattern, 'italic' ) !== false || strpos( $pattern, 'oblique' ) !== false ) ? 'italic' : 'normal';
549 $weight_keywords = array(
550 'extrablack' => '900',
551 'black' => '900',
552 'heavy' => '900',
553 'extrabold' => '800',
554 'ultrabold' => '800',
555 'bold' => '700',
556 'semibold' => '600',
557 'demibold' => '600',
558 'medium' => '500',
559 'book' => '400',
560 'regular' => '400',
561 'normal' => '400',
562 'semilight' => '300',
563 'light' => '300',
564 'extralight' => '200',
565 'ultralight' => '200',
566 'thin' => '100',
567 );
568
569 $weight = '400';
570 foreach ( $weight_keywords as $keyword => $value ) {
571 if ( strpos( $pattern, $keyword ) !== false ) {
572 $weight = $value;
573 break;
574 }
575 }
576
577 if ( preg_match( '/(100|200|300|400|500|600|700|800|900)/', $pattern, $match ) ) {
578 $weight = $match[1];
579 }
580
581 $clean_family = preg_replace( array( '/(italic|oblique)/i', '/(extrablack|black|heavy|extrabold|ultrabold|semibold|semilight|demibold|bold|medium|book|regular|normal|light|extralight|ultralight|thin)/i', '/(100|200|300|400|500|600|700|800|900)/' ), ' ', $filename );
582 $clean_family = preg_replace( '/[-_]+/', ' ', $clean_family );
583 $clean_family = trim( $clean_family );
584 $family = $clean_family ? ucwords( $clean_family ) : 'Custom Font';
585
586 $variant = self::get_variant_from_weight_and_style( $weight, $style );
587
588 return array(
589 'family' => $family,
590 'style' => $style,
591 'weight' => $weight,
592 'variant'=> $variant,
593 );
594 }
595
596 private static function get_variant_from_weight_and_style( $weight, $style ) {
597 if ( '400' === $weight ) {
598 return ( 'italic' === $style ) ? 'italic' : 'regular';
599 }
600
601 return ( 'italic' === $style ) ? $weight . 'italic' : $weight;
602 }
603
604 private static function build_single_font_stylesheet( $font_meta, $stored_font_name, $file_extension, $family_override = null ) {
605 $formats = array(
606 'ttf' => 'truetype',
607 'otf' => 'opentype',
608 'woff' => 'woff',
609 'woff2'=> 'woff2',
610 );
611
612 $font_format = isset( $formats[ $file_extension ] ) ? $formats[ $file_extension ] : 'truetype';
613
614 $family = $family_override ? $family_override : $font_meta['family'];
615
616 return "@font-face {\n\tfont-family: '{$family}';\n\tfont-style: {$font_meta['style']};\n\tfont-weight: {$font_meta['weight']};\n\tfont-display: swap;\n\tsrc: url('{$stored_font_name}') format('{$font_format}');\n}\n";
617 }
618
619 private static function handle_multiple_raw_font_upload( $files, $wp_filesystem, $uploads ) {
620 $allowed_font_files = array( 'ttf', 'otf', 'woff', 'woff2' );
621 $temp_dir = trailingslashit( $uploads['basedir'] ) .'kirki-font-temp/';
622 wp_mkdir_p( $temp_dir );
623
624 $common_family = null;
625 $common_family_slug = null;
626 $font_folder = null;
627 $renamed_folder_name = null;
628 $stylesheet = '';
629 $variants = array();
630
631 foreach ( $files as $single_file ) {
632 $filename = sanitize_file_name( $single_file['name'] );
633 $ext = strtolower( pathinfo( $filename, PATHINFO_EXTENSION ) );
634
635 if ( ! in_array( $ext, $allowed_font_files, true ) || empty( $single_file['tmp_name'] ) || ! is_uploaded_file( $single_file['tmp_name'] ) ) {
636 self::cleanup_temp_files( $temp_dir );
637 wp_send_json(
638 array(
639 'status' => 'failure',
640 'message' => 'Invalid font files provided',
641 ),
642 );
643 }
644
645 $temp_font = $temp_dir . wp_unique_filename( $temp_dir, $filename );
646 global $wp_filesystem;
647 if ( empty( $wp_filesystem ) ) {
648 require_once ABSPATH . 'wp-admin/includes/file.php';
649 WP_Filesystem();
650 }
651
652 if ( ! $wp_filesystem->move( $single_file['tmp_name'], $temp_font ) ) {
653 self::cleanup_temp_files( $temp_dir );
654 wp_send_json(
655 array(
656 'status' => 'failure',
657 'message' => 'Font upload failed',
658 ),
659 );
660 }
661
662 $filetype = wp_check_filetype_and_ext( $temp_font, $filename );
663 $detected_ext = ! empty( $filetype['ext'] ) ? strtolower( $filetype['ext'] ) : $ext;
664 if ( empty( $detected_ext ) || ! in_array( $detected_ext, $allowed_font_files, true ) ) {
665 wp_delete_file( $temp_font );
666 self::cleanup_temp_files( $temp_dir );
667 wp_send_json(
668 array(
669 'status' => 'failure',
670 'message' => 'Invalid font file type',
671 ),
672 );
673 }
674
675 $basename = pathinfo( $filename, PATHINFO_FILENAME );
676 $font_meta = self::get_font_metadata_from_filename( $basename );
677 $current_slug = self::normalize_font_family_slug( $font_meta['family'] );
678 $filename_slug = self::get_family_hint_slug_from_filename( $basename );
679 $comparison_slug = $filename_slug ? $filename_slug : $current_slug;
680 if ( ! $common_family ) {
681 $common_family = $font_meta['family'];
682 $common_family_slug = $comparison_slug;
683 $renamed_folder_name = $common_family_slug;
684 $font_folder = trailingslashit( $uploads['basedir'] ) .'kirki-fonts/' . $renamed_folder_name;
685
686 if ( is_dir( $font_folder ) ) {
687 self::delete_dir( $font_folder );
688 }
689 wp_mkdir_p( $font_folder );
690 }
691
692 if ( $comparison_slug !== $common_family_slug ) {
693 wp_delete_file( $temp_font );
694 self::delete_dir( $font_folder );
695 self::cleanup_temp_files( $temp_dir );
696 wp_send_json(
697 array(
698 'status' => 'failure',
699 'message' => 'Please upload fonts from the same family in a single batch.',
700 ),
701 );
702 }
703
704 $stored_font_name = wp_unique_filename( $font_folder, $filename );
705 $wp_filesystem->move( $temp_font, $font_folder . '/' . $stored_font_name );
706
707 $stylesheet .= self::build_single_font_stylesheet( $font_meta, $stored_font_name, $detected_ext, $common_family );
708 $variants[] = $font_meta['variant'];
709 }
710
711 self::cleanup_temp_files( $temp_dir );
712
713 if ( empty( $stylesheet ) || ! $font_folder ) {
714 wp_send_json(
715 array(
716 'status' => 'failure',
717 'message' => 'Unable to process fonts',
718 ),
719 );
720 }
721
722 $wp_filesystem->put_contents( $font_folder . '/stylesheet.css', $stylesheet );
723
724 $data = array(
725 'fontUrl' => trailingslashit( $uploads['baseurl'] ) .'kirki-fonts/' . $renamed_folder_name . '/stylesheet.css',
726 'family' => $common_family,
727 'variants' => array_values( array_unique( $variants ) ),
728 'subsets' => array( 'latin' ),
729 'uploaded' => true,
730 'version' => 'v1',
731 );
732
733 wp_send_json(
734 array(
735 'status' => 'success',
736 'data' => $data,
737 ),
738 );
739 }
740
741 private static function cleanup_temp_files( $temp_dir ) {
742 if ( is_dir( $temp_dir ) ) {
743 $files = glob( trailingslashit( $temp_dir ) . '*' );
744 if ( $files ) {
745 if ( is_file( $file ) ) {
746 wp_delete_file( $file );
747 }
748 }
749 }
750 }
751
752 private static function normalize_multiple_files( $files ) {
753 if ( empty( $files ) || ! isset( $files['name'] ) || ! is_array( $files['name'] ) ) {
754 return array();
755 }
756
757 $normalized = array();
758
759 foreach ( $files['name'] as $index => $name ) {
760 if ( empty( $files['tmp_name'][ $index ] ) ) {
761 continue;
762 }
763
764 $normalized[] = array(
765 'name' => $name,
766 'type' => $files['type'][ $index ],
767 'tmp_name' => $files['tmp_name'][ $index ],
768 'error' => $files['error'][ $index ],
769 'size' => $files['size'][ $index ],
770 );
771 }
772
773 return $normalized;
774 }
775
776 private static function normalize_font_family_slug( $family ) {
777 $family = strtolower( $family );
778 $slug = preg_replace( '/[^a-z0-9]+/i', '-', $family );
779 $slug = trim( preg_replace( '/-+/', '-', $slug ), '-' );
780
781 if ( ! $slug ) {
782 $slug = sanitize_file_name( str_replace( ' ', '', $family ) );
783 }
784
785 return $slug;
786 }
787
788 private static function get_family_hint_slug_from_filename( $filename ) {
789 if ( empty( $filename ) ) {
790 return null;
791 }
792
793 $parts = preg_split( '/[-_]+/', $filename );
794 if ( empty( $parts ) ) {
795 return null;
796 }
797
798 if ( count( $parts ) < 2 ) {
799 return null;
800 }
801
802 $base = $parts[0];
803
804 return self::normalize_font_family_slug( $base );
805 }
806
807
808 public static function get_rewritten_style_and_variants( $css_string, $common_family_name ) {
809 // Define weight keywords
810 $weight_keywords = array(
811 'extrablack' => '900',
812 'black' => '900',
813 'heavy' => '900',
814 'extrabold' => '800',
815 'ultrabold' => '800',
816 'semibold' => '600',
817 'demibold' => '600',
818 'bold' => '700',
819 'medium' => '500',
820 'extralight' => '200',
821 'ultralight' => '200',
822 'light' => '300',
823 'book' => '400',
824 'regular' => '400',
825 'normal' => '400',
826 'thin' => '100',
827 );
828
829 // Extract all @font-face blocks
830 preg_match_all( '/@font-face\s*{[^}]*}/is', $css_string, $blocks );
831
832 $rewritten_css = '';
833 $variants = array();
834
835 foreach ( $blocks[0] as $block ) {
836 $weight = null;
837 $style = 'normal';
838
839 // get src
840 preg_match( '/src:[^;]+;/i', $block, $src_match );
841 $src = isset( $src_match[0] ) ? $src_match[0] : '';
842
843 // get font-family
844 preg_match( '/font-family:\s*[\'"]?([^;\'"]+)[\'"]?;/i', $block, $family_match );
845 $family = isset( $family_match[1] ) ? $family_match[1] : '';
846
847 // Detect weight from keywords in src or font-family only
848 foreach ( $weight_keywords as $keyword => $w ) {
849 if ( stripos( $src, $keyword ) !== false || stripos( $family, $keyword ) !== false ) {
850 $weight = $w;
851 break;
852 }
853 }
854
855 // Fallback to declared CSS font-weight if not found
856 if ( $weight === null ) {
857 if ( preg_match( '/font-weight:\s*([0-9]+)/i', $block, $match ) ) {
858 $weight = $match[1];
859 } else {
860 $weight = '400';
861 }
862 }
863
864 if ( stripos( $block, 'italic' ) !== false ) {
865 $style = 'italic';
866 }
867
868 // rewritten @font-face block
869 $rewritten_css .= "@font-face {
870 font-family: '{$common_family_name}';
871 {$src}
872 font-weight: {$weight};
873 font-style: {$style};
874 }\n\n";
875
876 // if weight is 400, change -> regular, if italic 400italic -> italic, 200italic,700italic etc
877 if ( $weight === '400' ) {
878 $variant = ( $style === 'italic' ) ? 'italic' : 'regular';
879 } else {
880 $variant = ( $style === 'italic' ) ? $weight . 'italic' : $weight;
881 }
882
883 $variants[] = $variant;
884 }
885
886 // remove duplicate variants
887 $variants = array_values( array_unique( $variants ) );
888
889 return array(
890 'css' => $rewritten_css,
891 'variants' => $variants,
892 );
893 }
894
895 public static function get_common_family_name( $css_string ) {
896 // get all font family names
897 $pattern = '/font-family\s*:\s*([^;]+);/i';
898 preg_match_all( $pattern, $css_string, $matches );
899
900 if ( empty( $matches[1] ) ) {
901 return null;
902 }
903
904 $base_names = array();
905
906 foreach ( $matches[1] as $match ) {
907 $parts = explode( ',', $match );
908 foreach ( $parts as $p ) {
909 $f = trim( $p, " \t\n\r\0\x0B'\"" );
910 if ( ! $f ) {
911 continue;
912 }
913
914 // normalize font family name
915 $base = preg_replace( '/[-_\s]?(thin|extra|light|regular|medium|bold|black|italic|\d+)/i', '', $f );
916
917 if ( $base ) {
918 $base_names[] = strtolower( $base );
919 }
920 }
921 }
922
923 if ( empty( $base_names ) ) {
924 return null;
925 }
926
927 // find most common
928 $counts = array_count_values( $base_names );
929 arsort( $counts ); // most frequent first
930 $common_name = array_key_first( $counts );
931 $common_name = str_replace( array( '_', '-' ), ' ', $common_name );
932 $common_name = ucwords( $common_name );
933
934 return $common_name;
935 }
936
937 /**
938 * Remove extra files from font folder.
939 * we need only stylesheet.css, .woff and .woff2 file.
940 * others files will be removed.
941 *
942 * @param string $folder_path //raw path of uploaded folder.
943 * @return void
944 */
945 private static function remove_extra_files_from_font_folder( $folder_path ) {
946 global $wp_filesystem;
947 if ( empty( $wp_filesystem ) ) {
948 require_once ABSPATH . 'wp-admin/includes/file.php';
949 WP_Filesystem();
950 }
951
952 if ( $wp_filesystem->is_dir( $folder_path ) ) {
953 $files = $wp_filesystem->dirlist( $folder_path );
954
955 if ( ! empty( $files ) ) {
956 foreach ( $files as $file ) {
957 $file_path = $folder_path . '/' . $file['name'];
958
959 if ( 'f' === $file['type'] ) {
960 $ext = pathinfo( $file_path, PATHINFO_EXTENSION );
961 $allowed = array( 'css', 'woff', 'woff2', 'ttf', 'otf' );
962
963 if ( ! in_array( strtolower( $ext ), $allowed, true ) ) {
964 wp_delete_file( $file_path );
965 }
966 }
967 }
968 }
969 }
970 }
971
972
973 /**
974 * Remove custom font folder from server
975 *
976 * @return void wp_send_json.
977 *
978 * @deprecated
979 * @see \Kirki\App\Services\FontService::remove_custom_fonts_permanently_from_directory()
980 */
981 public static function remove_custom_font_folder_from_server() { //phpcs:ignore WordPress.Security.NonceVerification.Missing,WordPress.Security.NonceVerification.Recommended,WordPress.Security.ValidatedSanitizedInput.MissingUnslash,WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
982 $data = HelperFunctions::sanitize_text( isset( $_POST['data'] ) ? $_POST['data'] : null );
983 if ( ! $data ) {
984 wp_send_json(
985 array(
986 'status' => 'fail',
987 'message' => 'Invalid post data',
988 )
989 );
990 die();
991 }
992 $fonts = json_decode( stripslashes( $data ), true );
993
994 (new FontService())->remove_custom_fonts_permanently( $fonts );
995
996 // foreach ( $fonts as $key => $value ) {
997 // $upload_root_dir = wp_upload_dir()['basedir'];
998 // $upload_dir = $upload_root_dir . '/' .'kirki-fonts/' . $value['family'];
999 // if ( is_dir( $upload_dir ) ) {
1000 // self::delete_dir( $upload_dir );
1001 // }
1002
1003 // // Remove font from local.
1004 // $font_family_slug = sanitize_title_with_dashes( $value['family'] );
1005 // $font_local_dir = WP_CONTENT_DIR . "/uploads/kirki-fonts/{$font_family_slug}";
1006
1007 // if ( is_dir( $font_local_dir ) ) {
1008 // HelperFunctions::delete_directory( $font_local_dir );
1009 // }
1010 // }
1011 wp_send_json(
1012 array(
1013 'status' => 'success',
1014 'message' => 'Font folder deleted success',
1015 // 'url' => $upload_dir,
1016 )
1017 );
1018 }
1019
1020 /**
1021 * Delete Dir
1022 *
1023 * @param string $dir_path directory path string.
1024 * @throws InvalidArgumentException If the $dir_path is not a directory.
1025 * @return void
1026 * @deprecated
1027 * @see \Kirki\Framework\Supports\Facades\File::delete()
1028 */
1029 public static function delete_dir( $dir_path ) {
1030 global $wp_filesystem;
1031 if ( ! is_object( $wp_filesystem ) ) {
1032 WP_Filesystem();
1033 }
1034 if ( ! is_dir( $dir_path ) ) {
1035 return;
1036 }
1037 $wp_filesystem->delete( $dir_path, true );
1038 }
1039
1040 /**
1041 * Get Font Family name using regex
1042 *
1043 * @param string $css_string css string.
1044 * @return string font family name.
1045 */
1046 public static function get_font_family_name_using_regex( $css_string ) {
1047 $pattern = '/font-family:.*?;/';
1048 preg_match( $pattern, $css_string, $matches );
1049 $font_family = $matches[0];
1050 $font_family = str_replace( 'font-family:', '', $font_family );
1051 $font_family = str_replace( ';', '', $font_family );
1052 $font_family = str_replace( "'", '', $font_family );
1053 $font_family = trim( $font_family );
1054 return $font_family;
1055 }
1056
1057 /**
1058 * Upload base64 image
1059 *
1060 * @return void wp send json
1061 */
1062 public static function upload_base64_img() { // phpcs:ignore WordPress.Security.NonceVerification.Missing
1063 $source = isset( $_POST['source'] ) ? $_POST['source'] : '';
1064 $image_name = isset( $_POST['imageName'] ) ? $_POST['imageName'] : '';
1065
1066 $source = HelperFunctions::sanitize_text( $source );
1067 $image_name = HelperFunctions::sanitize_text( $image_name );
1068
1069 if ( empty( $source ) ) {
1070 wp_send_json(
1071 array(
1072 'status' => 'fail',
1073 'message' => 'No image data provided',
1074 )
1075 );
1076 }
1077
1078 /*
1079 |--------------------------------------------------------------------------
1080 | Configuration
1081 |--------------------------------------------------------------------------
1082 */
1083 $max_size_bytes = 5 * 1024 * 1024; // 5MB decoded limit
1084 $allowed_mimes = array(
1085 'image/png' => 'png',
1086 'image/jpeg' => 'jpg',
1087 'image/webp' => 'webp',
1088 );
1089
1090 /*
1091 |--------------------------------------------------------------------------
1092 | Parse base64 header safely
1093 |--------------------------------------------------------------------------
1094 */
1095 if ( ! preg_match( '#^data:(image\/[a-zA-Z0-9.+-]+);base64,#', $source, $matches ) ) {
1096 wp_send_json(
1097 array(
1098 'status' => 'fail',
1099 'message' => 'Invalid base64 image format',
1100 )
1101 );
1102 }
1103
1104 $mime = strtolower( $matches[1] );
1105
1106 if ( ! isset( $allowed_mimes[ $mime ] ) ) {
1107 wp_send_json(
1108 array(
1109 'status' => 'fail',
1110 'message' => 'Unsupported image type',
1111 )
1112 );
1113 }
1114
1115 $base64 = substr( $source, strpos( $source, ',' ) + 1 );
1116 $base64 = str_replace( ' ', '+', $base64 );
1117
1118 /*
1119 |--------------------------------------------------------------------------
1120 | Enforce decoded size quota (before decode)
1121 |--------------------------------------------------------------------------
1122 | Base64 expands data by ~33%, so estimate first
1123 */
1124 $estimated_size = (int) ( strlen( $base64 ) * 0.75 );
1125 if ( $estimated_size > $max_size_bytes ) {
1126 wp_send_json(
1127 array(
1128 'status' => 'fail',
1129 'message' => 'Image exceeds maximum allowed size',
1130 )
1131 );
1132 }
1133
1134 $decoded = base64_decode( $base64, true );
1135
1136 if ( $decoded === false ) {
1137 wp_send_json(
1138 array(
1139 'status' => 'fail',
1140 'message' => 'Invalid base64 data',
1141 )
1142 );
1143 }
1144
1145 if ( strlen( $decoded ) > $max_size_bytes ) {
1146 wp_send_json(
1147 array(
1148 'status' => 'fail',
1149 'message' => 'Image exceeds maximum allowed size',
1150 )
1151 );
1152 }
1153
1154 /*
1155 |--------------------------------------------------------------------------
1156 | Re-encode image to strip metadata & polyglots
1157 |--------------------------------------------------------------------------
1158 */
1159 $image = @imagecreatefromstring( $decoded );
1160 if ( ! $image ) {
1161 wp_send_json(
1162 array(
1163 'status' => 'fail',
1164 'message' => 'Image decoding failed',
1165 )
1166 );
1167 }
1168
1169 ob_start();
1170 switch ( $mime ) {
1171 case 'image/png':
1172 imagepng( $image, null, 9 );
1173 break;
1174 case 'image/jpeg':
1175 imagejpeg( $image, null, 90 );
1176 break;
1177 case 'image/webp':
1178 imagewebp( $image, null, 90 );
1179 break;
1180 }
1181 $clean_image = ob_get_clean();
1182 imagedestroy( $image );
1183
1184 if ( ! $clean_image ) {
1185 wp_send_json(
1186 array(
1187 'status' => 'fail',
1188 'message' => 'Failed to process image',
1189 )
1190 );
1191 }
1192
1193 /*
1194 |--------------------------------------------------------------------------
1195 | Save using WordPress upload system
1196 |--------------------------------------------------------------------------
1197 */
1198 $extension = $allowed_mimes[ $mime ];
1199 $filename = $image_name
1200 ? sanitize_file_name( $image_name ) . '.' . $extension
1201 : 'base64-image-' . gmdate( 'Y-m-d-His' ) . '.' . $extension;
1202
1203 $upload = wp_upload_bits( $filename, null, $clean_image );
1204
1205 if ( ! empty( $upload['error'] ) ) {
1206 wp_send_json(
1207 array(
1208 'status' => 'fail',
1209 'message' => 'Error saving image',
1210 )
1211 );
1212 }
1213
1214 $file = array(
1215 'name' => basename( $upload['file'] ),
1216 'type' => $mime,
1217 'tmp_name' => $upload['file'],
1218 'error' => 0,
1219 'size' => filesize( $upload['file'] ),
1220 );
1221
1222 $attachment_id = self::upload_single_media( $file );
1223
1224 if ( ! $attachment_id ) {
1225 wp_send_json(
1226 array(
1227 'status' => 'fail',
1228 'message' => 'Failed to create media attachment',
1229 )
1230 );
1231 }
1232
1233 $img = wp_get_attachment_image_src( $attachment_id, 'full' );
1234
1235 wp_send_json(
1236 array(
1237 'status' => 'success',
1238 'src' => $img[0],
1239 'id' => $attachment_id,
1240 )
1241 );
1242 }
1243
1244 /**
1245 * Validate a svg file
1246 *
1247 * @param string $svg_file svg file path.
1248 * @return bool
1249 */
1250 private function validate_svg( $svg_file ) {
1251 // File sanity checks
1252 if ( ! file_exists( $svg_file ) || ! is_readable( $svg_file ) ) {
1253 return false;
1254 }
1255
1256 $svg = file_get_contents( $svg_file );
1257 if ( $svg === false ) {
1258 return false;
1259 }
1260
1261 // Quick check to avoid non-SVG files (existing behavior)
1262 if ( stripos( $svg, '<svg' ) === false ) {
1263 return false;
1264 }
1265
1266 // Initialize sanitizer
1267 $sanitizer = new Sanitizer();
1268
1269 // Security hardening
1270 $sanitizer->removeRemoteReferences( true ); // blocks external <use>, <image>, etc.
1271 $sanitizer->minify( true );
1272
1273 // IMPORTANT:
1274 // Do NOT call setAllowedTags() or setAllowedAttrs()
1275 // The built-in allowlist is already safe and complete.
1276
1277 $clean_svg = $sanitizer->sanitize( $svg );
1278
1279 if ( $clean_svg === false ) {
1280 return false; // Sanitization failed
1281 }
1282
1283 // Final validation using DOM
1284 $dom = new DOMDocument();
1285 libxml_use_internal_errors( true );
1286
1287 if ( ! $dom->loadXML( $clean_svg, LIBXML_NONET ) ) {
1288 return false;
1289 }
1290
1291 // Ensure root element is <svg>
1292 if ( $dom->documentElement->nodeName !== 'svg' ) {
1293 return false;
1294 }
1295
1296 return true; // SVG is sanitized and safe
1297 }
1298
1299 }