application/json` for `upload_files` users * - `wp_check_filetype_and_ext`: PHP's fileinfo reports JSON as text/plain; a valid Lottie file is accepted as * application/json, anything else with a .json name keeps WordPress' "not allowed" result * - `wp_handle_upload_prefilter`: a clear error message for a .json file that is not a Lottie animation */ final class LottieUploads { public const MAX_BYTES = 5242880; public function register(): void { add_filter('upload_mimes', [self::class, 'mimes']); add_filter('wp_check_filetype_and_ext', [self::class, 'check_filetype'], 10, 4); add_filter('wp_handle_upload_prefilter', [self::class, 'prefilter']); } /** @param array $mimes */ public static function mimes($mimes): array { $mimes = is_array($mimes) ? $mimes : []; if (function_exists('current_user_can') && current_user_can('upload_files')) { $mimes['json'] = 'application/json'; } return $mimes; } /** * @param array{ext: string|false, type: string|false, proper_filename: string|false} $data */ public static function check_filetype($data, $file, $filename, $mimes): array { $data = is_array($data) ? $data : ['ext' => false, 'type' => false, 'proper_filename' => false]; if (strtolower((string) pathinfo((string) $filename, PATHINFO_EXTENSION)) !== 'json') { return $data; } if (is_string($file) && self::is_lottie_file($file) && function_exists('current_user_can') && current_user_can('upload_files')) { return ['ext' => 'json', 'type' => 'application/json', 'proper_filename' => false]; } return ['ext' => false, 'type' => false, 'proper_filename' => false]; } /** @param array{name?: string, tmp_name?: string, error?: string} $file */ public static function prefilter($file): array { $file = is_array($file) ? $file : []; $name = (string) ($file['name'] ?? ''); if (strtolower((string) pathinfo($name, PATHINFO_EXTENSION)) !== 'json') { return $file; } if (!self::is_lottie_file((string) ($file['tmp_name'] ?? ''))) { $file['error'] = __('Only Lottie animation JSON files can be uploaded.', 'zadex'); } return $file; } public static function is_lottie_file(string $path): bool { if ($path === '' || !is_file($path) || !is_readable($path)) { return false; } $max = (int) apply_filters('zadex/lottie/max_upload', self::MAX_BYTES); $size = filesize($path); if ($size === false || $size <= 0 || $size > $max) { return false; } $data = wp_json_file_decode($path, ['associative' => true]); return is_array($data) && self::is_lottie_data($data); } /** The minimal shape of a Lottie (bodymovin) animation. */ public static function is_lottie_json(string $raw): bool { $data = json_decode($raw, true); return is_array($data) && self::is_lottie_data($data); } /** The minimal shape of a decoded Lottie (bodymovin) animation. */ public static function is_lottie_data(array $data): bool { if (array_is_list($data)) { return false; } foreach (['fr', 'ip', 'op', 'w', 'h'] as $key) { if (!isset($data[$key]) || !is_numeric($data[$key])) { return false; } } return isset($data['v']) && is_string($data['v']) && isset($data['layers']) && is_array($data['layers']); } }