| Server IP : 10.200.247.200 / Your IP : 216.73.217.19 Web Server : Apache System : Linux synergy-usa-sites 6.8.0-138-generic #138-Ubuntu SMP PREEMPT_DYNAMIC Fri Jul 31 22:41:49 UTC 2026 x86_64 User : jeremy ( 1001) PHP Version : 8.4.25 Disable Function : NONE MySQL : OFF | cURL : ON | WGET : ON | Perl : ON | Python : OFF | Sudo : ON | Pkexec : OFF Directory : /var/www/development/callings.lvsaints.com/ |
Upload File : |
<?php
// ---------------------------------------------------------------------------
// Filesystem-based version control for uploaded sites.
//
// Each upload is a ZIP that we extract into its own folder under data/releases/.
// The "live" version is whichever folder the data/current symlink points at, so
// switching versions (or rolling back) is just re-pointing that one link.
//
// data/
// releases/
// v1__20260703-153000/ <- extracted site (index.html lives here)
// v2__20260703-160000/
// current -> releases/v2__20260703-160000 (the active version)
// meta/ v1__....json ... (note / who / when, kept out of the served dir)
// .htaccess (deny direct web access; site.php serves instead)
//
// Nothing here touches the database — the folders *are* the version history.
// ---------------------------------------------------------------------------
declare(strict_types=1);
function data_dir(): string { return __DIR__ . '/data'; }
function releases_dir(): string { return data_dir() . '/releases'; }
function meta_dir(): string { return data_dir() . '/meta'; }
function current_link(): string { return data_dir() . '/current'; }
// Create the storage layout on first use and lock the folder to PHP-only access.
function ensure_dirs(): void
{
foreach ([data_dir(), releases_dir(), meta_dir()] as $d) {
if (!is_dir($d) && !@mkdir($d, 0775, true) && !is_dir($d)) {
throw new RuntimeException("Cannot create storage folder: $d (check web-server write permissions).");
}
}
$htaccess = data_dir() . '/.htaccess';
if (!file_exists($htaccess)) {
@file_put_contents($htaccess,
"# Uploaded sites are served through site.php (after login), never directly.\n" .
"<IfModule mod_authz_core.c>\n Require all denied\n</IfModule>\n" .
"<IfModule !mod_authz_core.c>\n Order allow,deny\n Deny from all\n</IfModule>\n"
);
}
}
// Name for the next release: sequential number + timestamp, e.g. v3__20260703-160000.
function next_version_name(): string
{
$max = 0;
foreach (glob(releases_dir() . '/v*__*', GLOB_ONLYDIR) ?: [] as $d) {
if (preg_match('#/v(\d+)__#', $d, $m)) {
$max = max($max, (int) $m[1]);
}
}
return sprintf('v%d__%s', $max + 1, date('Ymd-His'));
}
// The folder name the "current" symlink points at, or null if nothing is live.
function active_version(): ?string
{
$link = current_link();
if (is_link($link)) {
$t = readlink($link);
return $t === false ? null : basename($t);
}
return null;
}
// Absolute path of the live release's document root, or null.
function active_dir(): ?string
{
$name = active_version();
if ($name === null) {
return null;
}
$dir = releases_dir() . '/' . $name;
return is_dir($dir) ? $dir : null;
}
function meta_path(string $name): string
{
return meta_dir() . '/' . $name . '.json';
}
function write_meta(string $name, array $data): void
{
ensure_dirs();
@file_put_contents(meta_path($name), json_encode($data, JSON_PRETTY_PRINT));
}
function read_meta(string $name): array
{
$f = meta_path($name);
if (is_file($f)) {
$d = json_decode((string) file_get_contents($f), true);
if (is_array($d)) {
return $d;
}
}
return [];
}
// All versions, newest first, with their metadata and live flag.
function list_versions(): array
{
$active = active_version();
$out = [];
foreach (glob(releases_dir() . '/*', GLOB_ONLYDIR) ?: [] as $d) {
$name = basename($d);
$meta = read_meta($name);
$out[] = [
'name' => $name,
'note' => $meta['note'] ?? '',
'uploaded_by' => $meta['uploaded_by'] ?? '',
'uploaded_at' => $meta['uploaded_at'] ?? date('Y-m-d H:i:s', (int) filemtime($d)),
'orig' => $meta['orig'] ?? '',
'is_live' => ($name === $active),
'has_index' => is_file($d . '/index.html'),
];
}
usort($out, fn($a, $b) => strcmp($b['uploaded_at'], $a['uploaded_at']));
return $out;
}
// Point "current" at $name. Uses a temp link + atomic rename so a viewer never
// sees a half-updated symlink.
function set_active(string $name): void
{
ensure_dirs();
if (!is_dir(releases_dir() . '/' . $name)) {
throw new RuntimeException("Version “$name” does not exist.");
}
$tmp = data_dir() . '/.current-' . uniqid('', true);
// Relative target so the link stays valid if the project is moved.
if (!@symlink('releases/' . $name, $tmp)) {
@unlink($tmp);
throw new RuntimeException('Could not create the symlink. Does the server allow symlinks and have write access to the data/ folder?');
}
if (!@rename($tmp, current_link())) {
@unlink($tmp);
throw new RuntimeException('Could not update the “current” link.');
}
}
// Extract an uploaded zip into a new release folder. Guards against zip-slip,
// strips a single wrapping folder, and requires an index.html to exist.
// Uses PHP's zip extension when available, else the `unzip` binary.
function extract_zip(string $tmpFile, string $name): void
{
ensure_dirs();
$target = releases_dir() . '/' . $name;
if (!@mkdir($target, 0775, true) && !is_dir($target)) {
throw new RuntimeException('Could not create the version folder.');
}
try {
$entries = zip_list_entries($tmpFile);
// Reject anything that would escape the target folder before writing a byte.
foreach ($entries as $entry) {
$n = str_replace('\\', '/', $entry);
if (str_starts_with($n, '/') || preg_match('#(^|/)\.\.(/|$)#', $n)) {
throw new RuntimeException('The zip contains an unsafe path and was rejected.');
}
}
zip_extract_to($tmpFile, $target);
} catch (Throwable $e) {
rrmdir($target);
throw $e;
}
// Junk that macOS adds when zipping from Finder.
rrmdir($target . '/__MACOSX');
@unlink($target . '/.DS_Store');
// Symlinks could point outside the folder once served — never keep them.
if (has_symlink($target)) {
rrmdir($target);
throw new RuntimeException('The zip contains a symbolic link and was rejected.');
}
flatten_single_wrapper($target);
if (!is_file($target . '/index.html')) {
rrmdir($target);
throw new RuntimeException('No index.html was found in the zip (it must be at the top level, or inside a single folder).');
}
}
// Locate the `unzip` binary, or null if it isn't installed.
function unzip_bin(): ?string
{
foreach (['/usr/bin/unzip', '/bin/unzip', '/usr/local/bin/unzip'] as $p) {
if (is_executable($p)) {
return $p;
}
}
return null;
}
// List the entry names in a zip, using whichever backend is available.
function zip_list_entries(string $file): array
{
if (class_exists('ZipArchive')) {
$zip = new ZipArchive();
if ($zip->open($file) !== true) {
throw new RuntimeException('That file is not a valid .zip archive.');
}
$names = [];
for ($i = 0; $i < $zip->numFiles; $i++) {
$n = $zip->getNameIndex($i);
if ($n !== false) {
$names[] = $n;
}
}
$zip->close();
return $names;
}
if ($bin = unzip_bin()) {
$out = [];
$code = 0;
exec(escapeshellarg($bin) . ' -Z1 -- ' . escapeshellarg($file) . ' 2>/dev/null', $out, $code);
if ($code !== 0) {
throw new RuntimeException('That file is not a valid .zip archive.');
}
return array_map('rtrim', $out);
}
throw new RuntimeException('The server cannot read zip files. Enable the PHP “zip” extension (php-zip) or install the “unzip” program.');
}
// Extract a zip into $target using whichever backend is available.
function zip_extract_to(string $file, string $target): void
{
if (class_exists('ZipArchive')) {
$zip = new ZipArchive();
if ($zip->open($file) !== true || !$zip->extractTo($target)) {
throw new RuntimeException('Could not extract the zip.');
}
$zip->close();
return;
}
if ($bin = unzip_bin()) {
$code = 0;
$out = [];
exec(escapeshellarg($bin) . ' -o -qq -- ' . escapeshellarg($file)
. ' -d ' . escapeshellarg($target) . ' 2>/dev/null', $out, $code);
if ($code !== 0 && $code !== 1) { // 1 = warnings only (e.g. skipped junk)
throw new RuntimeException('Could not extract the zip.');
}
return;
}
throw new RuntimeException('The server cannot read zip files. Enable the PHP “zip” extension (php-zip) or install the “unzip” program.');
}
// True if any symlink exists anywhere under $dir.
function has_symlink(string $dir): bool
{
foreach (scandir($dir) ?: [] as $item) {
if ($item === '.' || $item === '..') {
continue;
}
$path = $dir . '/' . $item;
if (is_link($path)) {
return true;
}
if (is_dir($path) && has_symlink($path)) {
return true;
}
}
return false;
}
// If everything sits inside one wrapping folder (common when you zip a folder
// rather than its contents), lift that folder's contents up to the root.
function flatten_single_wrapper(string $target): void
{
if (is_file($target . '/index.html')) {
return;
}
$entries = array_values(array_filter(
scandir($target) ?: [],
fn($e) => $e !== '.' && $e !== '..' && $e !== '__MACOSX'
));
if (count($entries) !== 1) {
return;
}
$inner = $target . '/' . $entries[0];
if (!is_dir($inner)) {
return;
}
foreach (scandir($inner) ?: [] as $child) {
if ($child === '.' || $child === '..') {
continue;
}
@rename($inner . '/' . $child, $target . '/' . $child);
}
@rmdir($inner);
}
// Delete a version's folder + metadata. Refuses to delete the live one.
function delete_version(string $name): void
{
if ($name === active_version()) {
throw new RuntimeException('That version is live — make another version live before deleting it.');
}
$dir = releases_dir() . '/' . $name;
$real = realpath($dir);
$base = realpath(releases_dir());
if ($real === false || $base === false || !str_starts_with($real, $base . DIRECTORY_SEPARATOR)) {
throw new RuntimeException('Invalid version.');
}
rrmdir($real);
@unlink(meta_path($name));
}
// Recursively remove a directory (no-op if it doesn't exist).
function rrmdir(string $dir): void
{
if (!is_dir($dir)) {
return;
}
foreach (scandir($dir) ?: [] as $item) {
if ($item === '.' || $item === '..') {
continue;
}
$path = $dir . '/' . $item;
is_dir($path) && !is_link($path) ? rrmdir($path) : @unlink($path);
}
@rmdir($dir);
}