* → writes .fixed.zip next to the original * * php mg-repair-backup.php * → writes to a specific path * * Standalone — no Grav, no Composer autoloader, no plugin context needed. */ declare(strict_types=1); if (PHP_SAPI !== 'cli') { http_response_code(403); echo "This script is CLI-only. Run it from a terminal."; exit(1); } if (!extension_loaded('zip')) { fwrite(STDERR, "PHP zip extension is required. Install php-zip and try again.\n"); exit(2); } $src = $argv[1] ?? null; $dst = $argv[2] ?? null; if ($src === null) { fwrite(STDERR, "usage: php " . basename(__FILE__) . " []\n"); exit(2); } if (!is_file($src)) { fwrite(STDERR, "Input zip not found: {$src}\n"); exit(2); } if ($dst === null) { // Default: write next to the source with .fixed.zip suffix, so the // user can compare or roll back if anything looks off. if (substr($src, -4) === '.zip') { $dst = substr($src, 0, -4) . '.fixed.zip'; } else { $dst = $src . '.fixed.zip'; } } if (realpath($src) !== false && realpath(dirname($dst)) !== false && realpath($src) === realpath($dst)) { fwrite(STDERR, "Refusing to write output to the same path as input.\n"); exit(2); } $in = new ZipArchive(); $rc = $in->open($src); if ($rc !== true) { fwrite(STDERR, "Could not open input zip (ZipArchive code {$rc}): {$src}\n"); exit(2); } @unlink($dst); $out = new ZipArchive(); $rc = $out->open($dst, ZipArchive::CREATE | ZipArchive::OVERWRITE); if ($rc !== true) { $in->close(); fwrite(STDERR, "Could not create output zip (ZipArchive code {$rc}): {$dst}\n"); exit(2); } $total = $in->numFiles; $normalized = 0; $copied = 0; $failed = []; for ($i = 0; $i < $total; $i++) { $name = $in->getNameIndex($i); if ($name === false) { $failed[] = "(index {$i}: getNameIndex failed)"; continue; } $fixed = str_replace('\\', '/', $name); if ($fixed !== $name) $normalized++; $isDir = substr($fixed, -1) === '/'; if ($isDir) { $out->addEmptyDir(rtrim($fixed, '/')); $copied++; continue; } $bytes = $in->getFromIndex($i); if ($bytes === false) { $failed[] = $name; continue; } if (!$out->addFromString($fixed, $bytes)) { $failed[] = $name; continue; } $copied++; } $in->close(); $closeOk = $out->close(); echo "── repair summary ──\n"; echo "input: {$src}\n"; echo "output: {$dst}\n"; echo "entries scanned: {$total}\n"; echo "entries fixed: {$normalized} (had backslashes)\n"; echo "entries copied: {$copied}\n"; echo "close ok: " . ($closeOk ? 'yes' : 'NO') . "\n"; if ($failed !== []) { echo "FAILED entries: " . count($failed) . "\n"; foreach ($failed as $f) echo " - {$f}\n"; } if ($normalized === 0 && $failed === []) { echo "\nNote: input zip already had all forward-slashed entry names — it\n"; echo " was already valid. If you're still seeing flat extraction,\n"; echo " the problem is with your extractor, not the zip.\n"; } echo "\nNext step: extract {$dst} as you normally would.\n"; echo " Windows: right-click → Extract All…\n"; echo " macOS: double-click, or unzip in Terminal\n"; echo " Linux: unzip " . escapeshellarg($dst) . "\n"; exit($failed !== [] || !$closeOk ? 1 : 0);