$error === UPLOAD_ERR_OK && $tmp !== '' && is_uploaded_file($tmp) ? UPLOAD_ERR_OK : ($error ?: UPLOAD_ERR_CANT_WRITE), 'tmp_name' => $tmp, 'size' => isset($_FILES[$field]['size']) ? absint($_FILES[$field]['size']) : 0, 'name' => isset($_FILES[$field]['name']) ? sanitize_file_name(wp_unslash((string)$_FILES[$field]['name'])) : '', ]; } /** * Safe image upload. The file is decoded and re-encoded with GD, which strips metadata and any * embedded payload, and saved under a random name. Returns the stored file name or null if none was sent. */ function handle_image_upload(string $field, int $maxWidth = 1600): ?string { $f = uploaded_file($field); if (!$f) return null; if ($f['error'] !== UPLOAD_ERR_OK) fail('Image upload failed. Please try again.'); if ($f['size'] > 6 * 1024 * 1024) fail('Image is too large. The limit is 6 MB.'); return save_image_path($f['tmp_name'], $maxWidth); } /** Decode an image file, re-encode it with GD and store it under a random name. Used by uploads and by event imports. */ function save_image_path(string $tmp, int $maxWidth = 1600): string { $info = wp_getimagesize($tmp); $type = $info[2] ?? 0; if (!in_array($type, [IMAGETYPE_JPEG, IMAGETYPE_PNG, IMAGETYPE_WEBP], true)) fail('Please upload a JPG, PNG or WebP image.'); if ($info[0] > 8000 || $info[1] > 8000 || $info[0] * $info[1] > 40_000_000) fail('Image dimensions are too large.'); $src = match ($type) { IMAGETYPE_JPEG => imagecreatefromjpeg($tmp), IMAGETYPE_PNG => imagecreatefrompng($tmp), IMAGETYPE_WEBP => imagecreatefromwebp($tmp), }; if (!$src) fail('That image could not be read.'); $w = imagesx($src); $h = imagesy($src); if ($w > $maxWidth) { $nh = (int)round($h * $maxWidth / $w); $dst = imagecreatetruecolor($maxWidth, $nh); imagealphablending($dst, false); imagesavealpha($dst, true); imagecopyresampled($dst, $src, 0, 0, 0, 0, $maxWidth, $nh, $w, $h); imagedestroy($src); $src = $dst; } $dir = gatecraft_uploads()['dir']; $isPng = $type === IMAGETYPE_PNG; $name = bin2hex(random_bytes(16)) . ($isPng ? '.png' : '.jpg'); if ($isPng) { imagesavealpha($src, true); imagepng($src, $dir . '/' . $name, 7); } else { $bg = imagecreatetruecolor(imagesx($src), imagesy($src)); imagefill($bg, 0, 0, imagecolorallocate($bg, 255, 255, 255)); imagecopy($bg, $src, 0, 0, 0, 0, imagesx($src), imagesy($src)); imagejpeg($bg, $dir . '/' . $name, 85); imagedestroy($bg); } imagedestroy($src); return $name; } /** Full path of a stored upload, or null if the name is not one of ours or the file is missing. */ function upload_path(string $name): ?string { if (!preg_match('/^[a-f0-9]{32}\.(jpg|png|webp)$/', $name)) return null; $p = gatecraft_uploads()['dir'] . '/' . $name; return is_file($p) ? $p : null; } function delete_upload(?string $name): void { if ($name && preg_match('/^[a-f0-9]{32}\.(jpg|png|webp)$/', $name)) wp_delete_file(gatecraft_uploads()['dir'] . '/' . $name); } /** * The WordPress filesystem API for the plugin's own upload folder. The folder is in wp-content/uploads, which PHP writes * to directly (as WordPress does for media), so the direct method is used. */ function gatecraft_fs(): \WP_Filesystem_Base { static $fs = null; if ($fs) return $fs; if (!class_exists('WP_Filesystem_Direct')) { require_once ABSPATH . 'wp-admin/includes/class-wp-filesystem-base.php'; require_once ABSPATH . 'wp-admin/includes/class-wp-filesystem-direct.php'; } return $fs = new \WP_Filesystem_Direct(null); } /** * File permissions for files the plugin writes. WordPress defines FS_CHMOD_FILE only once its filesystem API has been * started, which has not happened during plugin activation or on most front-end requests, so fall back to the same * value WordPress would use. */ function gatecraft_file_mode(): int { if (defined('FS_CHMOD_FILE')) return (int)FS_CHMOD_FILE; $index = ABSPATH . 'index.php'; $perms = file_exists($index) ? fileperms($index) : false; return $perms ? (($perms & 0777) | 0644) : 0644; } /** Upload folder inside wp-content/uploads, created with rules that stop anything in it from executing. */ function gatecraft_uploads(): array { static $r = null; if ($r) return $r; $u = wp_upload_dir(null, false); $dir = $u['basedir'] . '/gatecraft'; if (!is_dir($dir)) wp_mkdir_p($dir); // Written whenever missing, so a folder created without them (by an earlier version) is completed too. if (is_dir($dir) && !file_exists($dir . '/index.php')) { gatecraft_fs()->put_contents($dir . '/index.php', "put_contents($dir . '/.htaccess', "Options -Indexes -ExecCGI\n\n Require all denied\n\n\n php_flag engine off\n\n", gatecraft_file_mode()); } return $r = ['dir' => $dir, 'url' => set_url_scheme($u['baseurl'] . '/gatecraft')]; }