//`. Every directory gets an empty `index.php` so the folders * cannot be listed; the random segment keeps file URLs unguessable. Submissions keep the file URLs. */ final class Uploads { public const DIR = 'zadex-forms'; /** * Turns the PHP `$_FILES['fields']` shape (name[] / name[][]) into ` => [file, ...]`. * * @param mixed $files `$_FILES['fields']` (or a WP_REST_Request file param) * @return array> */ public static function normalize($files): array { if (!is_array($files) || !isset($files['name']) || !is_array($files['name'])) { return []; } $out = []; foreach ($files['name'] as $raw_id => $name) { $id = Fields::sanitize_id($raw_id); if ($id === '') { continue; } $names = is_array($name) ? $name : [$name]; foreach (array_keys($names) as $i) { $pick = static function (string $prop) use ($files, $raw_id, $name, $i) { $v = $files[$prop][$raw_id] ?? null; return is_array($name) ? (is_array($v) ? ($v[$i] ?? null) : null) : $v; }; $error = (int) ($pick('error') ?? UPLOAD_ERR_NO_FILE); if ($error === UPLOAD_ERR_NO_FILE) { continue; } $out[$id][] = [ 'name' => (string) ($pick('name') ?? ''), 'type' => (string) ($pick('type') ?? ''), 'tmp_name' => (string) ($pick('tmp_name') ?? ''), 'error' => $error, 'size' => (int) ($pick('size') ?? 0), ]; } } return $out; } /** * @param array>> $files validated uploads per field id * @return array>|WP_Error field id => file URLs */ public function store(array $files) { if (!$files) { return []; } if (!function_exists('wp_handle_upload') && defined('ABSPATH') && is_file(ABSPATH . 'wp-admin/includes/file.php')) { require_once ABSPATH . 'wp-admin/includes/file.php'; } $subdir = '/' . self::DIR . '/' . gmdate('Y') . '/' . bin2hex(random_bytes(8)); $filter = static function (array $dirs) use ($subdir): array { $dirs['subdir'] = $subdir; $dirs['path'] = $dirs['basedir'] . $subdir; $dirs['url'] = $dirs['baseurl'] . $subdir; return $dirs; }; add_filter('upload_dir', $filter); $stored = []; $paths = []; $error = null; foreach ($files as $id => $list) { foreach ($list as $file) { $result = wp_handle_upload($file, ['test_form' => false, 'action' => 'zadex_form_upload']); if (!is_array($result) || isset($result['error']) || empty($result['url'])) { $error = new WP_Error('zadex_upload_failed', is_array($result) && isset($result['error']) ? (string) $result['error'] : __('The file could not be uploaded.', 'zadex'), ['field' => $id]); break 2; } $stored[$id][] = (string) $result['url']; $paths[] = (string) ($result['file'] ?? ''); } } remove_filter('upload_dir', $filter); if ($error) { foreach ($paths as $p) { if ($p !== '' && is_file($p)) { wp_delete_file($p); } } return $error; } $this->protect($subdir); return $stored; } /** Empty index.php in zadex-forms/, the year and the random folder (no directory listing). */ private function protect(string $subdir): void { if (!function_exists('wp_upload_dir')) { return; } $base = (string) (wp_upload_dir(null, false)['basedir'] ?? ''); if ($base === '') { return; } $dir = $base; foreach (array_filter(explode('/', $subdir)) as $part) { $dir .= '/' . $part; if (is_dir($dir) && !is_file($dir . '/index.php')) { \Zadex\Filesystem::put($dir . '/index.php', "