$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')];
}