PluginProbe ʕ •ᴥ•ʔ
Kirki – Freeform Page Builder, Website Builder & Customizer / 6.2.3
Kirki – Freeform Page Builder, Website Builder & Customizer v6.2.3
6.2.4 6.2.3 6.2.2 6.2.1 6.2.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 2 weeks ago Apps.php 1 month ago Collection.php 2 months ago Comments.php 3 months ago DynamicContent.php 1 week ago ExportImport.php 1 week ago Form.php 1 month ago Media.php 5 days ago Page.php 2 weeks ago PageSettings.php 1 month ago RBAC.php 1 month ago Symbol.php 1 month ago Taxonomy.php 3 months ago TemplateExportImport.php 3 months ago UserData.php 1 month ago Users.php 1 week ago Walkthrough.php 2 weeks ago WordpressData.php 3 months ago WpAdmin.php 4 weeks ago
Media.php
1079 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 enshrined\svgSanitize\Sanitizer;
20 use Exception;
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 custom font files (.ttf, .otf, .woff, .woff2), single or multiple.
297 *
298 * @return void wp_send_json.
299 */
300 public static function upload_fonts() {
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 = self::get_allowed_font_extensions();
331
332 if ( in_array( $file_extension, $allowed_font_files, true ) ) {
333 self::handle_raw_font_upload( $file, $filename, $file_extension, $wp_filesystem, $uploads );
334 return;
335 }
336
337 wp_send_json(
338 array(
339 'status' => 'failure',
340 'message' => 'Only .ttf, .otf, .woff, .woff2 files are allowed',
341 )
342 );
343 }
344
345 /**
346 * Allowed font file extensions.
347 *
348 * @return array
349 */
350 private static function get_allowed_font_extensions() {
351 return array( 'ttf', 'otf', 'woff', 'woff2' );
352 }
353
354 /**
355 * Verify that a file really is a font by inspecting its binary signature.
356 *
357 * @param string $file_path Absolute path to the file on disk.
358 * @param string $file_extension Extension claimed by the uploaded file name.
359 * @return bool True when the binary signature matches the claimed extension.
360 */
361 private static function is_valid_font_file( $file_path, $file_extension ) {
362 // if file is not readable or file size is less than 12 bytes, return false.
363 if ( ! is_readable( $file_path ) || filesize( $file_path ) < 12 ) {
364 return false;
365 }
366
367 // Opening the file in binary read mode ('rb').
368 // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_fopen
369 $handle = fopen( $file_path, 'rb' );
370 if ( ! $handle ) {
371 return false;
372 }
373
374 // Reading the first 4 bytes of the file to check its signature.
375 // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_fread
376 $header = fread( $handle, 4 );
377 // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_fclose
378 fclose( $handle );
379
380 if ( false === $header || 4 !== strlen( $header ) ) {
381 return false;
382 }
383
384 // sfnt based fonts (.ttf / .otf) may use any of these version tags.
385 $sfnt_signatures = array(
386 "\x00\x01\x00\x00", // TrueType outlines.
387 'true', // Legacy Apple TrueType.
388 'ttcf', // TrueType collection.
389 'OTTO', // CFF (PostScript) outlines.
390 );
391
392 $signatures = array(
393 'ttf' => $sfnt_signatures,
394 'otf' => $sfnt_signatures,
395 'woff' => array( 'wOFF' ),
396 'woff2' => array( 'wOF2' ),
397 );
398
399 if ( ! isset( $signatures[ $file_extension ] ) ) {
400 return false;
401 }
402
403 return in_array( $header, $signatures[ $file_extension ], true );
404 }
405
406 private static function handle_raw_font_upload( $file, $filename, $file_extension, $wp_filesystem, $uploads, $family_override = null ) {
407 $allowed_font_files = self::get_allowed_font_extensions();
408 if ( ! in_array( $file_extension, $allowed_font_files, true ) ) {
409 wp_send_json(
410 array(
411 'status' => 'failure',
412 'message' => 'Unsupported font file type',
413 )
414 );
415 }
416
417 $temp_dir = trailingslashit( $uploads['basedir'] ) .'kirki-font-temp/';
418 wp_mkdir_p( $temp_dir );
419 $temp_font = $temp_dir . wp_unique_filename( $temp_dir, $filename );
420
421 global $wp_filesystem;
422 if ( ! is_object( $wp_filesystem ) ) {
423 WP_Filesystem();
424 }
425
426 if ( ! $wp_filesystem->move( $file['tmp_name'], $temp_font ) ) {
427 wp_send_json(
428 array(
429 'status' => 'failure',
430 'message' => 'Font upload failed',
431 )
432 );
433 }
434
435 $filetype = wp_check_filetype_and_ext( $temp_font, $filename );
436 $detected_ext = ! empty( $filetype['ext'] ) ? strtolower( $filetype['ext'] ) : $file_extension;
437 if ( empty( $detected_ext ) || ! in_array( $detected_ext, $allowed_font_files, true ) || ! self::is_valid_font_file( $temp_font, $detected_ext ) ) {
438 wp_delete_file( $temp_font );
439 wp_send_json(
440 array(
441 'status' => 'failure',
442 'message' => 'Invalid font file type',
443 ),
444 );
445 }
446
447 $font_meta = self::get_font_metadata_from_filename( pathinfo( $filename, PATHINFO_FILENAME ) );
448 if ( $family_override && $family_override !== $font_meta['family'] ) {
449 $font_meta['family'] = $family_override;
450 }
451 $renamed_folder_name = self::normalize_font_family_slug( $font_meta['family'] );
452 $font_folder = trailingslashit( $uploads['basedir'] ) .'kirki-fonts/' . $renamed_folder_name;
453
454 if ( is_dir( $font_folder ) ) {
455 self::delete_dir( $font_folder );
456 }
457
458 wp_mkdir_p( $font_folder );
459
460 $stored_font_name = wp_unique_filename( $font_folder, $filename );
461 $wp_filesystem->move( $temp_font, $font_folder . '/' . $stored_font_name );
462
463 $stylesheet = self::build_single_font_stylesheet( $font_meta, $stored_font_name, $detected_ext, $font_meta['family'] );
464 $wp_filesystem->put_contents( $font_folder . '/stylesheet.css', $stylesheet );
465
466 $data = array(
467 'fontUrl' => trailingslashit( $uploads['baseurl'] ) .'kirki-fonts/' . $renamed_folder_name . '/stylesheet.css',
468 'family' => $font_meta['family'],
469 'variants' => array( $font_meta['variant'] ),
470 'subsets' => array( 'latin' ),
471 'uploaded' => true,
472 'version' => 'v1',
473 );
474
475 wp_send_json(
476 array(
477 'status' => 'success',
478 'data' => $data,
479 )
480 );
481 }
482
483 private static function get_font_metadata_from_filename( $filename ) {
484 $pattern = strtolower( $filename );
485 $style = ( strpos( $pattern, 'italic' ) !== false || strpos( $pattern, 'oblique' ) !== false ) ? 'italic' : 'normal';
486 $weight_keywords = array(
487 'extrablack' => '900',
488 'black' => '900',
489 'heavy' => '900',
490 'extrabold' => '800',
491 'ultrabold' => '800',
492 'bold' => '700',
493 'semibold' => '600',
494 'demibold' => '600',
495 'medium' => '500',
496 'book' => '400',
497 'regular' => '400',
498 'normal' => '400',
499 'semilight' => '300',
500 'light' => '300',
501 'extralight' => '200',
502 'ultralight' => '200',
503 'thin' => '100',
504 );
505
506 $weight = '400';
507 foreach ( $weight_keywords as $keyword => $value ) {
508 if ( strpos( $pattern, $keyword ) !== false ) {
509 $weight = $value;
510 break;
511 }
512 }
513
514 if ( preg_match( '/(100|200|300|400|500|600|700|800|900)/', $pattern, $match ) ) {
515 $weight = $match[1];
516 }
517
518 $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 );
519 $clean_family = preg_replace( '/[-_]+/', ' ', $clean_family );
520 $clean_family = trim( $clean_family );
521 $family = $clean_family ? ucwords( $clean_family ) : 'Custom Font';
522
523 $variant = self::get_variant_from_weight_and_style( $weight, $style );
524
525 return array(
526 'family' => $family,
527 'style' => $style,
528 'weight' => $weight,
529 'variant'=> $variant,
530 );
531 }
532
533 private static function get_variant_from_weight_and_style( $weight, $style ) {
534 if ( '400' === $weight ) {
535 return ( 'italic' === $style ) ? 'italic' : 'regular';
536 }
537
538 return ( 'italic' === $style ) ? $weight . 'italic' : $weight;
539 }
540
541 private static function build_single_font_stylesheet( $font_meta, $stored_font_name, $file_extension, $family_override = null ) {
542 $formats = array(
543 'ttf' => 'truetype',
544 'otf' => 'opentype',
545 'woff' => 'woff',
546 'woff2'=> 'woff2',
547 );
548
549 $font_format = isset( $formats[ $file_extension ] ) ? $formats[ $file_extension ] : 'truetype';
550
551 $family = $family_override ? $family_override : $font_meta['family'];
552
553 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";
554 }
555
556 private static function handle_multiple_raw_font_upload( $files, $wp_filesystem, $uploads ) {
557 $allowed_font_files = self::get_allowed_font_extensions();
558 $temp_dir = trailingslashit( $uploads['basedir'] ) .'kirki-font-temp/';
559 wp_mkdir_p( $temp_dir );
560
561 $common_family = null;
562 $common_family_slug = null;
563 $font_folder = null;
564 $renamed_folder_name = null;
565 $stylesheet = '';
566 $variants = array();
567
568 foreach ( $files as $single_file ) {
569 $filename = sanitize_file_name( $single_file['name'] );
570 $ext = strtolower( pathinfo( $filename, PATHINFO_EXTENSION ) );
571
572 if ( ! in_array( $ext, $allowed_font_files, true ) || empty( $single_file['tmp_name'] ) || ! is_uploaded_file( $single_file['tmp_name'] ) ) {
573 self::cleanup_temp_files( $temp_dir );
574 wp_send_json(
575 array(
576 'status' => 'failure',
577 'message' => 'Invalid font files provided',
578 ),
579 );
580 }
581
582 $temp_font = $temp_dir . wp_unique_filename( $temp_dir, $filename );
583 global $wp_filesystem;
584 if ( empty( $wp_filesystem ) ) {
585 require_once ABSPATH . 'wp-admin/includes/file.php';
586 WP_Filesystem();
587 }
588
589 if ( ! $wp_filesystem->move( $single_file['tmp_name'], $temp_font ) ) {
590 self::cleanup_temp_files( $temp_dir );
591 wp_send_json(
592 array(
593 'status' => 'failure',
594 'message' => 'Font upload failed',
595 ),
596 );
597 }
598
599 $filetype = wp_check_filetype_and_ext( $temp_font, $filename );
600 $detected_ext = ! empty( $filetype['ext'] ) ? strtolower( $filetype['ext'] ) : $ext;
601 if ( empty( $detected_ext ) || ! in_array( $detected_ext, $allowed_font_files, true ) || ! self::is_valid_font_file( $temp_font, $detected_ext ) ) {
602 wp_delete_file( $temp_font );
603 self::cleanup_temp_files( $temp_dir );
604 wp_send_json(
605 array(
606 'status' => 'failure',
607 'message' => 'Invalid font file type',
608 ),
609 );
610 }
611
612 $basename = pathinfo( $filename, PATHINFO_FILENAME );
613 $font_meta = self::get_font_metadata_from_filename( $basename );
614 $current_slug = self::normalize_font_family_slug( $font_meta['family'] );
615 $filename_slug = self::get_family_hint_slug_from_filename( $basename );
616 $comparison_slug = $filename_slug ? $filename_slug : $current_slug;
617 if ( ! $common_family ) {
618 $common_family = $font_meta['family'];
619 $common_family_slug = $comparison_slug;
620 $renamed_folder_name = $common_family_slug;
621 $font_folder = trailingslashit( $uploads['basedir'] ) .'kirki-fonts/' . $renamed_folder_name;
622
623 if ( is_dir( $font_folder ) ) {
624 self::delete_dir( $font_folder );
625 }
626 wp_mkdir_p( $font_folder );
627 }
628
629 if ( $comparison_slug !== $common_family_slug ) {
630 wp_delete_file( $temp_font );
631 self::delete_dir( $font_folder );
632 self::cleanup_temp_files( $temp_dir );
633 wp_send_json(
634 array(
635 'status' => 'failure',
636 'message' => 'Please upload fonts from the same family in a single batch.',
637 ),
638 );
639 }
640
641 $stored_font_name = wp_unique_filename( $font_folder, $filename );
642 $wp_filesystem->move( $temp_font, $font_folder . '/' . $stored_font_name );
643
644 $stylesheet .= self::build_single_font_stylesheet( $font_meta, $stored_font_name, $detected_ext, $common_family );
645 $variants[] = $font_meta['variant'];
646 }
647
648 self::cleanup_temp_files( $temp_dir );
649
650 if ( empty( $stylesheet ) || ! $font_folder ) {
651 wp_send_json(
652 array(
653 'status' => 'failure',
654 'message' => 'Unable to process fonts',
655 ),
656 );
657 }
658
659 $wp_filesystem->put_contents( $font_folder . '/stylesheet.css', $stylesheet );
660
661 $data = array(
662 'fontUrl' => trailingslashit( $uploads['baseurl'] ) .'kirki-fonts/' . $renamed_folder_name . '/stylesheet.css',
663 'family' => $common_family,
664 'variants' => array_values( array_unique( $variants ) ),
665 'subsets' => array( 'latin' ),
666 'uploaded' => true,
667 'version' => 'v1',
668 );
669
670 wp_send_json(
671 array(
672 'status' => 'success',
673 'data' => $data,
674 ),
675 );
676 }
677
678 private static function cleanup_temp_files( $temp_dir ) {
679 if ( is_dir( $temp_dir ) ) {
680 $files = glob( trailingslashit( $temp_dir ) . '*' );
681 if ( $files ) {
682 foreach ( $files as $file ) {
683 if ( is_file( $file ) ) {
684 wp_delete_file( $file );
685 }
686 }
687 }
688 }
689 }
690
691 private static function normalize_multiple_files( $files ) {
692 if ( empty( $files ) || ! isset( $files['name'] ) || ! is_array( $files['name'] ) ) {
693 return array();
694 }
695
696 $normalized = array();
697
698 foreach ( $files['name'] as $index => $name ) {
699 if ( empty( $files['tmp_name'][ $index ] ) ) {
700 continue;
701 }
702
703 $normalized[] = array(
704 'name' => $name,
705 'type' => $files['type'][ $index ],
706 'tmp_name' => $files['tmp_name'][ $index ],
707 'error' => $files['error'][ $index ],
708 'size' => $files['size'][ $index ],
709 );
710 }
711
712 return $normalized;
713 }
714
715 private static function normalize_font_family_slug( $family ) {
716 $family = strtolower( $family );
717 $slug = preg_replace( '/[^a-z0-9]+/i', '-', $family );
718 $slug = trim( preg_replace( '/-+/', '-', $slug ), '-' );
719
720 if ( ! $slug ) {
721 $slug = sanitize_file_name( str_replace( ' ', '', $family ) );
722 }
723
724 // Never return an empty slug: the callers append it to the kirki-fonts
725 // directory and delete that path, so an empty slug would wipe the whole
726 // kirki-fonts folder instead of a single font folder.
727 if ( ! $slug ) {
728 $slug = 'font-' . substr( md5( $family . microtime() ), 0, 12 );
729 }
730
731 return $slug;
732 }
733
734 private static function get_family_hint_slug_from_filename( $filename ) {
735 if ( empty( $filename ) ) {
736 return null;
737 }
738
739 $parts = preg_split( '/[-_]+/', $filename );
740 if ( empty( $parts ) ) {
741 return null;
742 }
743
744 if ( count( $parts ) < 2 ) {
745 return null;
746 }
747
748 $base = $parts[0];
749
750 return self::normalize_font_family_slug( $base );
751 }
752
753 /**
754 * Remove custom font folder from server
755 *
756 * @return void wp_send_json.
757 *
758 * @deprecated
759 * @see \Kirki\App\Services\FontService::remove_custom_fonts_permanently_from_directory()
760 */
761 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
762 $data = HelperFunctions::sanitize_text( isset( $_POST['data'] ) ? $_POST['data'] : null );
763 if ( ! $data ) {
764 wp_send_json(
765 array(
766 'status' => 'fail',
767 'message' => 'Invalid post data',
768 )
769 );
770 die();
771 }
772 $fonts = json_decode( stripslashes( $data ), true );
773
774 (new FontService())->remove_custom_fonts_permanently( $fonts );
775
776 // foreach ( $fonts as $key => $value ) {
777 // $upload_root_dir = wp_upload_dir()['basedir'];
778 // $upload_dir = $upload_root_dir . '/' .'kirki-fonts/' . $value['family'];
779 // if ( is_dir( $upload_dir ) ) {
780 // self::delete_dir( $upload_dir );
781 // }
782
783 // // Remove font from local.
784 // $font_family_slug = sanitize_title_with_dashes( $value['family'] );
785 // $font_local_dir = WP_CONTENT_DIR . "/uploads/kirki-fonts/{$font_family_slug}";
786
787 // if ( is_dir( $font_local_dir ) ) {
788 // HelperFunctions::delete_directory( $font_local_dir );
789 // }
790 // }
791 wp_send_json(
792 array(
793 'status' => 'success',
794 'message' => 'Font folder deleted success',
795 // 'url' => $upload_dir,
796 )
797 );
798 }
799
800 /**
801 * Delete Dir
802 *
803 * @param string $dir_path directory path string.
804 * @throws InvalidArgumentException If the $dir_path is not a directory.
805 * @return void
806 * @deprecated
807 * @see \Kirki\Framework\Supports\Facades\File::delete()
808 */
809 public static function delete_dir( $dir_path ) {
810 global $wp_filesystem;
811 if ( ! is_object( $wp_filesystem ) ) {
812 WP_Filesystem();
813 }
814 if ( ! is_dir( $dir_path ) ) {
815 return;
816 }
817 $wp_filesystem->delete( $dir_path, true );
818 }
819
820 /**
821 * Get Font Family name using regex
822 *
823 * @param string $css_string css string.
824 * @return string font family name.
825 */
826 public static function get_font_family_name_using_regex( $css_string ) {
827 $pattern = '/font-family:.*?;/';
828 preg_match( $pattern, $css_string, $matches );
829 $font_family = $matches[0];
830 $font_family = str_replace( 'font-family:', '', $font_family );
831 $font_family = str_replace( ';', '', $font_family );
832 $font_family = str_replace( "'", '', $font_family );
833 $font_family = trim( $font_family );
834 return $font_family;
835 }
836
837 /**
838 * Upload base64 image
839 *
840 * @return void wp send json
841 */
842 public static function upload_base64_img() { // phpcs:ignore WordPress.Security.NonceVerification.Missing
843 $source = isset( $_POST['source'] ) ? $_POST['source'] : '';
844 $image_name = isset( $_POST['imageName'] ) ? $_POST['imageName'] : '';
845
846 $source = HelperFunctions::sanitize_text( $source );
847 $image_name = HelperFunctions::sanitize_text( $image_name );
848
849 if ( empty( $source ) ) {
850 wp_send_json(
851 array(
852 'status' => 'fail',
853 'message' => 'No image data provided',
854 )
855 );
856 }
857
858 /*
859 |--------------------------------------------------------------------------
860 | Configuration
861 |--------------------------------------------------------------------------
862 */
863 $max_size_bytes = 5 * 1024 * 1024; // 5MB decoded limit
864 $allowed_mimes = array(
865 'image/png' => 'png',
866 'image/jpeg' => 'jpg',
867 'image/webp' => 'webp',
868 );
869
870 /*
871 |--------------------------------------------------------------------------
872 | Parse base64 header safely
873 |--------------------------------------------------------------------------
874 */
875 if ( ! preg_match( '#^data:(image\/[a-zA-Z0-9.+-]+);base64,#', $source, $matches ) ) {
876 wp_send_json(
877 array(
878 'status' => 'fail',
879 'message' => 'Invalid base64 image format',
880 )
881 );
882 }
883
884 $mime = strtolower( $matches[1] );
885
886 if ( ! isset( $allowed_mimes[ $mime ] ) ) {
887 wp_send_json(
888 array(
889 'status' => 'fail',
890 'message' => 'Unsupported image type',
891 )
892 );
893 }
894
895 $base64 = substr( $source, strpos( $source, ',' ) + 1 );
896 $base64 = str_replace( ' ', '+', $base64 );
897
898 /*
899 |--------------------------------------------------------------------------
900 | Enforce decoded size quota (before decode)
901 |--------------------------------------------------------------------------
902 | Base64 expands data by ~33%, so estimate first
903 */
904 $estimated_size = (int) ( strlen( $base64 ) * 0.75 );
905 if ( $estimated_size > $max_size_bytes ) {
906 wp_send_json(
907 array(
908 'status' => 'fail',
909 'message' => 'Image exceeds maximum allowed size',
910 )
911 );
912 }
913
914 $decoded = base64_decode( $base64, true );
915
916 if ( $decoded === false ) {
917 wp_send_json(
918 array(
919 'status' => 'fail',
920 'message' => 'Invalid base64 data',
921 )
922 );
923 }
924
925 if ( strlen( $decoded ) > $max_size_bytes ) {
926 wp_send_json(
927 array(
928 'status' => 'fail',
929 'message' => 'Image exceeds maximum allowed size',
930 )
931 );
932 }
933
934 /*
935 |--------------------------------------------------------------------------
936 | Re-encode image to strip metadata & polyglots
937 |--------------------------------------------------------------------------
938 */
939 $image = @imagecreatefromstring( $decoded );
940 if ( ! $image ) {
941 wp_send_json(
942 array(
943 'status' => 'fail',
944 'message' => 'Image decoding failed',
945 )
946 );
947 }
948
949 ob_start();
950 switch ( $mime ) {
951 case 'image/png':
952 imagepng( $image, null, 9 );
953 break;
954 case 'image/jpeg':
955 imagejpeg( $image, null, 90 );
956 break;
957 case 'image/webp':
958 imagewebp( $image, null, 90 );
959 break;
960 }
961 $clean_image = ob_get_clean();
962 imagedestroy( $image );
963
964 if ( ! $clean_image ) {
965 wp_send_json(
966 array(
967 'status' => 'fail',
968 'message' => 'Failed to process image',
969 )
970 );
971 }
972
973 /*
974 |--------------------------------------------------------------------------
975 | Save using WordPress upload system
976 |--------------------------------------------------------------------------
977 */
978 $extension = $allowed_mimes[ $mime ];
979 $filename = $image_name
980 ? sanitize_file_name( $image_name ) . '.' . $extension
981 : 'base64-image-' . gmdate( 'Y-m-d-His' ) . '.' . $extension;
982
983 $upload = wp_upload_bits( $filename, null, $clean_image );
984
985 if ( ! empty( $upload['error'] ) ) {
986 wp_send_json(
987 array(
988 'status' => 'fail',
989 'message' => 'Error saving image',
990 )
991 );
992 }
993
994 $file = array(
995 'name' => basename( $upload['file'] ),
996 'type' => $mime,
997 'tmp_name' => $upload['file'],
998 'error' => 0,
999 'size' => filesize( $upload['file'] ),
1000 );
1001
1002 $attachment_id = self::upload_single_media( $file );
1003
1004 if ( ! $attachment_id ) {
1005 wp_send_json(
1006 array(
1007 'status' => 'fail',
1008 'message' => 'Failed to create media attachment',
1009 )
1010 );
1011 }
1012
1013 $img = wp_get_attachment_image_src( $attachment_id, 'full' );
1014
1015 wp_send_json(
1016 array(
1017 'status' => 'success',
1018 'src' => $img[0],
1019 'id' => $attachment_id,
1020 )
1021 );
1022 }
1023
1024 /**
1025 * Validate a svg file
1026 *
1027 * @param string $svg_file svg file path.
1028 * @return bool
1029 */
1030 private function validate_svg( $svg_file ) {
1031 // File sanity checks
1032 if ( ! file_exists( $svg_file ) || ! is_readable( $svg_file ) ) {
1033 return false;
1034 }
1035
1036 $svg = file_get_contents( $svg_file );
1037 if ( $svg === false ) {
1038 return false;
1039 }
1040
1041 // Quick check to avoid non-SVG files (existing behavior)
1042 if ( stripos( $svg, '<svg' ) === false ) {
1043 return false;
1044 }
1045
1046 // Initialize sanitizer
1047 $sanitizer = new Sanitizer();
1048
1049 // Security hardening
1050 $sanitizer->removeRemoteReferences( true ); // blocks external <use>, <image>, etc.
1051 $sanitizer->minify( true );
1052
1053 // IMPORTANT:
1054 // Do NOT call setAllowedTags() or setAllowedAttrs()
1055 // The built-in allowlist is already safe and complete.
1056
1057 $clean_svg = $sanitizer->sanitize( $svg );
1058
1059 if ( $clean_svg === false ) {
1060 return false; // Sanitization failed
1061 }
1062
1063 // Final validation using DOM
1064 $dom = new DOMDocument();
1065 libxml_use_internal_errors( true );
1066
1067 if ( ! $dom->loadXML( $clean_svg, LIBXML_NONET ) ) {
1068 return false;
1069 }
1070
1071 // Ensure root element is <svg>
1072 if ( $dom->documentElement->nodeName !== 'svg' ) {
1073 return false;
1074 }
1075
1076 return true; // SVG is sanitized and safe
1077 }
1078
1079 }