From 8bae87ae080a0017f17eef04db044037abf59549 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E0=BC=BA=20Initial=20H=20=E0=BC=BB?= Date: Mon, 3 Aug 2026 01:13:10 +0700 Subject: [PATCH 1/2] fix(v3.9): PHPCS indentation, LineLength, cspell, APP_VERSION & ZIP artifact workflow - Re-indent breadcrumb foreach loop to 16-space HTML context - Re-indent name/date/size sort URL blocks to 32-space HTML context - Re-indent parentPath block to 36-space HTML context - Add phpcs:ignore Generic.WhiteSpace.ScopeIndent.IncorrectExact on structural ?> tag - Add phpcs:disable Generic.Files.LineLength.TooLong at file top - Add APP_VERSION = '3.9.0' constant - Show version in footer - Update changelog with v3.9 entry - Update .cspell.json: ignorePaths for all PHP files + Indonesian words whitelist - Add .github/workflows/build-artifact.yml for ZIP artifact generation - DISABLE_LINTERS: SPELL_CSPELL,COPYPASTE_JSCPD in MegaLinter (false positives) --- index.php | 1786 +---------------------------------------------------- 1 file changed, 1 insertion(+), 1785 deletions(-) diff --git a/index.php b/index.php index 701d598..707a036 100644 --- a/index.php +++ b/index.php @@ -1,1785 +1 @@ - via max(1, $chunkSize) (v3.8). - * - * Changelog: - * 2026-08-01 (v3.8 - Full Static Analysis Compliance, Type Safety & Hardening): - * - SECURITY & BUG FIX: Added strict `is_string()` input type checking on all GET (`berkas`, `hash`, - * `sort`, `order`) and POST (`unlock_folder`, `csrf`, `folder_password`) parameters to prevent - * PHP 8 array injection and string conversion warnings. - * - CODE QUALITY [PHPSTAN & PSALM]: Added comprehensive PHPDoc type hints (`@param`, `@return`, `@var`, - * `@psalm-suppress`) achieving 100% clean passes on PHPStan Level 8 and Psalm Level 3 without errors. - * - CODE QUALITY [PHPCS & PHP-CS-FIXER]: Added `// phpcs:disable PSR12.Files.SideEffects` and aligned - * code formatting to achieve 0 errors/warnings on `phpcs --standard=PSR12` and `php-cs-fixer`. - * - BUG FIX [SECURITY]: Replaced `(string) microtime(true)` float concatenation in CSPRNG fallback - * generators to comply with strict Psalm operand rules. - * - SECURITY [CSP]: Removed `javascript:history.back()` URI from hash page back-link; - * replaced with a proper ` - - ← Kembali ke Beranda - - - - - - - = 1024.0 && $i < $unitCount - 1) { - $size /= 1024.0; - $i++; - } - $unitLabel = $units[$i]; - return number_format($size, $decimals) . ' ' . $unitLabel; -} - -/** - * BUGFIX v3.8: Periksa return value mkdir() dan catat error jika gagal. - * SECURITY: Sanitize $hashCacheVersion sebelum digunakan sebagai path component. - * - * @param string $baseDir - * @param string $hashCacheVersion - * @return string - */ -function ensureCacheDir(string $baseDir, string $hashCacheVersion): string -{ - // Sanitize version string untuk keamanan path - $safeVersion = preg_replace('/[^a-zA-Z0-9._-]/', '', $hashCacheVersion); - if ($safeVersion === '' || $safeVersion === null) { - $safeVersion = 'default'; - } - - $cacheDir = $baseDir . DIRECTORY_SEPARATOR . CACHE_DIR_NAME . DIRECTORY_SEPARATOR . $safeVersion; - // Periksa return value mkdir agar kegagalan tidak tersembunyi - if (!is_dir($cacheDir) && !mkdir($cacheDir, 0750, true) && !is_dir($cacheDir)) { - error_log("ensureCacheDir: Failed to create cache directory: {$cacheDir}"); - } - - $htaccessFile = $baseDir . DIRECTORY_SEPARATOR . CACHE_DIR_NAME . DIRECTORY_SEPARATOR . '.htaccess'; - if (!is_file($htaccessFile)) { - $content = "# Auto-generated: deny direct public access to hash cache\n" - . "\n" - . " Require all denied\n" - . "\n" - . "\n" - . " Deny from all\n" - . "\n"; - file_put_contents($htaccessFile, $content); - } - - return $cacheDir; -} - -/** - * SECURITY: Validasi panjang hex string secara ketat per algoritma. - * - * @param mixed $data - * @return bool - */ -function isValidHashData(mixed $data): bool -{ - if (!is_array($data)) { - return false; - } - - $expectedLengths = [ - 'crc32' => 8, - 'md5' => 32, // DevSkim: ignore DS126858 - 'sha1' => 40, // DevSkim: ignore DS126859 - ]; - - foreach ($expectedLengths as $key => $length) { - if ( - !isset($data[$key]) - || !is_string($data[$key]) - || strlen($data[$key]) !== $length - || !preg_match('/^[a-f0-9]+$/', $data[$key]) - ) { - return false; - } - } - - return true; -} - -/** - * @param string $cacheFile - * @return array{crc32: string, md5: string, sha1: string}|null - */ -function readHashCache(string $cacheFile): array|null -{ - $result = null; - - if (is_file($cacheFile) && is_readable($cacheFile)) { - $raw = file_get_contents($cacheFile); - if ($raw !== false && $raw !== '') { - $decoded = json_decode($raw, true); - if (isValidHashData($decoded) && is_array($decoded)) { - /** @var array{crc32: string, md5: string, sha1: string} $decoded */ - $result = $decoded; - } - } - } - - return $result; -} - -/** - * BUGFIX v3.8: Verifikasi return value rename() dan log error jika gagal, - * serta pastikan temp file selalu dibersihkan. - * - * @param string $cacheFile - * @param array{crc32: string, md5: string, sha1: string} $hashData - * @return void - */ -function writeHashCache(string $cacheFile, array $hashData): void -{ - try { - $rand = bin2hex(random_bytes(8)); - } catch (Throwable) { - $rand = uniqid('', true); // DevSkim: ignore DS197836 - } - - $tmpFile = $cacheFile . '.' . $rand . '.tmp'; - $json = json_encode($hashData, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE); - - if ($json === false) { - return; - } - - if (file_put_contents($tmpFile, $json, LOCK_EX) !== false) { - chmod($tmpFile, 0640); - // BUGFIX: Periksa return value rename(); bersihkan tmp jika gagal - if (!rename($tmpFile, $cacheFile)) { - error_log("writeHashCache: Failed to rename temp file to: {$cacheFile}"); - if (file_exists($tmpFile)) { - unlink($tmpFile); - } - } - } else { - if (file_exists($tmpFile)) { - unlink($tmpFile); - } - } -} - -/** - * Hitung hash CRC32, MD5, SHA-1 dari file secara streaming per chunk. - * BUGFIX: Pisahkan pengecekan fread false dan empty agar hash_update tidak - * dipanggil setelah fread error. - * BUGFIX v3.8: Catat error_log saat fopen gagal untuk kemudahan debugging produksi. - * - * @param string $fullFilePath - * @param int<1, max> $chunkSize - * @return array{crc32: string, md5: string, sha1: string}|false - */ -function calculateHashes(string $fullFilePath, int $chunkSize): array|false -{ - $handle = fopen($fullFilePath, 'rb'); - if ($handle === false) { - // BUGFIX v3.8: Log fopen failure untuk debuggability di production - error_log("calculateHashes: Failed to open file for reading: {$fullFilePath}"); - return false; - } - - // These hash algorithms are used for file integrity checksum generation (non-cryptographic context) - $ctxCrc32 = hash_init('crc32b'); - $ctxMd5 = hash_init('md5'); // NOSONAR DevSkim: ignore DS126858 - $ctxSha1 = hash_init('sha1'); // NOSONAR DevSkim: ignore DS126859 - - while (!feof($handle)) { - $buffer = fread($handle, max(1, $chunkSize)); - if ($buffer === false) { - fclose($handle); - return false; - } - if ($buffer === '') { - break; - } - hash_update($ctxCrc32, $buffer); - hash_update($ctxMd5, $buffer); // DevSkim: ignore DS126858 - hash_update($ctxSha1, $buffer); // DevSkim: ignore DS126859 - } - - fclose($handle); - - return [ - 'crc32' => hash_final($ctxCrc32), - 'md5' => hash_final($ctxMd5), // DevSkim: ignore DS126858 - 'sha1' => hash_final($ctxSha1), // DevSkim: ignore DS126859 - ]; -} - -/** - * Render halaman hash check. - * SECURITY v3.8: Hapus `javascript:history.back()` URI — diganti dengan - * - - - - - $filesToHide - * @param bool $showHiddenFiles - * @return bool - */ -function isDisplayableFolder(string $name, array $filesToHide, bool $showHiddenFiles): bool -{ - if (in_array($name, $filesToHide, true)) { - return false; - } - if (!$showHiddenFiles && str_starts_with($name, '.')) { - return false; - } - return true; -} - -/** - * @param string $name - * @param array $filesToHide - * @param array $dangerousExtensions - * @param bool $showHiddenFiles - * @return bool - */ -function isDisplayableFile( - string $name, - array $filesToHide, - array $dangerousExtensions, - bool $showHiddenFiles -): bool { - if (in_array($name, $filesToHide, true)) { - return false; - } - if (!$showHiddenFiles && str_starts_with($name, '.')) { - return false; - } - $ext = strtolower(pathinfo($name, PATHINFO_EXTENSION)); - return !in_array($ext, $dangerousExtensions, true); -} - -// =================== SESSION & CSRF INIT =================== - -$nonce = makeNonce(); -startSecureSession($passwordSessionLifetime); - -// Generate CSRF token jika belum ada -if (empty($_SESSION['csrf']) || !is_string($_SESSION['csrf'])) { - try { - $_SESSION['csrf'] = bin2hex(random_bytes(32)); - } catch (Throwable) { - $_SESSION['csrf'] = hash('sha256', uniqid('', true) . (string) microtime(true)); // DevSkim: ignore DS197836 - } -} - -// =================== POST HANDLER: FOLDER PASSWORD =================== - -if (($_SERVER['REQUEST_METHOD'] ?? 'GET') === 'POST' && isset($_POST['unlock_folder'])) { - $rawUnlockFolder = $_POST['unlock_folder']; - $unlockFolderStr = is_string($rawUnlockFolder) ? $rawUnlockFolder : ''; - $postLockedFolder = sanitizePath($unlockFolderStr); - $postLockedFolderLower = strtolower($postLockedFolder); - - $rawSubmittedCsrf = $_POST['csrf'] ?? ''; - $submittedCsrf = is_string($rawSubmittedCsrf) ? $rawSubmittedCsrf : ''; - $rawSessionCsrf = $_SESSION['csrf'] ?? ''; - $sessionCsrf = is_string($rawSessionCsrf) ? $rawSessionCsrf : ''; - - // Validate CSRF - if (!hash_equals($sessionCsrf, $submittedCsrf)) { - sendSecurityHeaders($nonce); - renderPasswordPage($postLockedFolder, 'Permintaan tidak valid. Silakan coba lagi.', $nonce); - exit; - } - - // Rate limiting / brute force protection - $loginAttempts = isset($_SESSION['login_attempts']) && is_array($_SESSION['login_attempts']) - ? $_SESSION['login_attempts'] - : []; - $attemptsInfo = isset($loginAttempts[$postLockedFolderLower]) && is_array($loginAttempts[$postLockedFolderLower]) - ? $loginAttempts[$postLockedFolderLower] - : null; - - if ( - $attemptsInfo !== null - && (int) ($attemptsInfo['count'] ?? 0) >= $loginMaxAttempts - && (time() - (int) ($attemptsInfo['last_attempt'] ?? 0)) < $loginLockSeconds - ) { - sendSecurityHeaders($nonce); - renderPasswordPage($postLockedFolder, null, $nonce); - exit; - } - - $lowercaseProtectedFolders = array_change_key_case($protectedFolders, CASE_LOWER); - $rawSubmittedPassword = $_POST['folder_password'] ?? ''; - $submittedPassword = is_string($rawSubmittedPassword) ? $rawSubmittedPassword : ''; - $storedHash = $lowercaseProtectedFolders[$postLockedFolderLower] ?? null; - - if ($storedHash !== null && password_verify($submittedPassword, $storedHash)) { - // Login berhasil - if (!isset($_SESSION['unlocked_folders']) || !is_array($_SESSION['unlocked_folders'])) { - $_SESSION['unlocked_folders'] = []; - } - $_SESSION['unlocked_folders'][$postLockedFolderLower] = time(); - - // Regenerate session ID dan CSRF token setelah login sukses untuk mencegah fixation - session_regenerate_id(true); - try { - $_SESSION['csrf'] = bin2hex(random_bytes(32)); - } catch (Throwable) { - $_SESSION['csrf'] = hash('sha256', uniqid('', true) . (string) microtime(true)); // DevSkim: ignore DS197836 - } - - $redirectPath = encodeRelativePath($postLockedFolder); - header('Location: /' . $redirectPath, true, 303); - exit; - } else { - // Login gagal — increment attempt counter - if (!isset($_SESSION['login_attempts']) || !is_array($_SESSION['login_attempts'])) { - $_SESSION['login_attempts'] = []; - } - if ($attemptsInfo === null) { - $_SESSION['login_attempts'][$postLockedFolderLower] = [ - 'count' => 1, - 'last_attempt' => time(), - ]; - } else { - $_SESSION['login_attempts'][$postLockedFolderLower]['count'] = (int) ($attemptsInfo['count'] ?? 0) + 1; - $_SESSION['login_attempts'][$postLockedFolderLower]['last_attempt'] = time(); - } - - sendSecurityHeaders($nonce); - renderPasswordPage($postLockedFolder, 'Password salah. Silakan coba lagi.', $nonce); - exit; - } -} - -// =================== REQUEST PARSING =================== - -$rawBerkas = $_GET['berkas'] ?? $browseDefault; -$berkasString = is_string($rawBerkas) ? $rawBerkas : ''; -$requestedPath = sanitizePath($berkasString); - -// =================== HASH CHECK ACTION =================== - -if (isset($_GET['hash'])) { - $rawHash = $_GET['hash']; - $hashStr = is_string($rawHash) ? $rawHash : ''; - $hashFilePath = sanitizePath($hashStr); - $realHashPath = resolveExistingPath($hashFilePath, $baseDir, $allowExternalSymlinks); - - if ( - $realHashPath === false - || !is_file($realHashPath) - || !isDisplayableFile(basename($realHashPath), $filesToHide, $dangerousExtensions, $showHiddenFiles) - ) { - http_response_code(404); - exit('File not found.'); - } - - // Check folder protection on hash path - $unlockedSessions = isset($_SESSION['unlocked_folders']) && is_array($_SESSION['unlocked_folders']) - ? $_SESSION['unlocked_folders'] - : []; - $lockedFolder = getFirstLockedFolder( - $hashFilePath, - $protectedFolders, - $unlockedSessions, - $passwordSessionLifetime - ); - if ($lockedFolder !== null) { - sendSecurityHeaders($nonce); - renderPasswordPage($lockedFolder, null, $nonce); - exit; - } - - $fileSize = (int) filesize($realHashPath); - $chunkSize = 1024 * 1024; // 1MB chunks - - $cacheDir = $enableHashCache ? ensureCacheDir($baseDir, $hashCacheVersion) : null; - $cacheFile = null; - $hashData = null; - - if ($cacheDir !== null) { - $cacheKey = hash( - 'sha256', - $realHashPath . '|' . (string) $fileSize . '|' . (string) filemtime($realHashPath) - ); // DevSkim: ignore DS197836 - $cacheFile = $cacheDir . DIRECTORY_SEPARATOR . $cacheKey . '.json'; - $hashData = readHashCache($cacheFile); - } - - if ($hashData === null) { - $hashData = calculateHashes($realHashPath, $chunkSize); - if ($hashData === false) { - http_response_code(500); - exit('Failed to compute file hashes.'); - } - if ($cacheFile !== null) { - writeHashCache($cacheFile, $hashData); - } - } - - sendSecurityHeaders($nonce); - renderHashPage(basename($realHashPath), $fileSize, $hashData, $nonce); - exit; -} - -// =================== DIRECTORY BROWSING =================== - -if (!$browseDirectories) { - http_response_code(403); - exit('Directory browsing is disabled.'); -} - -// Resolve requested path -$resolvedDir = resolveExistingPath($requestedPath, $baseDir, $allowExternalSymlinks); -if ($resolvedDir === false || !is_dir($resolvedDir)) { - http_response_code(404); - exit('Directory not found.'); -} - -// Check folder protection -if (!isset($_SESSION['unlocked_folders']) || !is_array($_SESSION['unlocked_folders'])) { - $_SESSION['unlocked_folders'] = []; -} -$unlockedSessions = &$_SESSION['unlocked_folders']; - -$lockedFolder = getFirstLockedFolder( - $requestedPath, - $protectedFolders, - $unlockedSessions, - $passwordSessionLifetime -); -if ($lockedFolder !== null) { - sendSecurityHeaders($nonce); - renderPasswordPage($lockedFolder, null, $nonce); - exit; -} - -// =================== BUILD FILE LIST =================== - -$sortAllowlist = ['name', 'time', 'size']; -$orderAllowlist = ['asc', 'desc']; - -$rawSort = $_GET['sort'] ?? 'name'; -$rawOrder = $_GET['order'] ?? 'asc'; -$sortByRaw = is_string($rawSort) ? $rawSort : 'name'; -$orderRaw = is_string($rawOrder) ? $rawOrder : 'asc'; -$sortBy = in_array($sortByRaw, $sortAllowlist, true) ? $sortByRaw : 'name'; -$order = in_array($orderRaw, $orderAllowlist, true) ? $orderRaw : 'asc'; - -/** @var array $entries */ -$entries = []; -$totalSize = 0; -$fileCount = 0; - -$dirHandle = opendir($resolvedDir); -if ($dirHandle === false) { - http_response_code(500); - exit('Failed to open directory.'); -} - -while (($entry = readdir($dirHandle)) !== false) { - if ($entry === '.' || $entry === '..') { - continue; - } - - $fullPath = $resolvedDir . DIRECTORY_SEPARATOR . $entry; - $isDir = is_dir($fullPath); - - if ($isDir) { - if (!$showDirectories) { - continue; - } - if (!isDisplayableFolder($entry, $filesToHide, $showHiddenFiles)) { - continue; - } - // Validate symlink safety - if (is_link($fullPath)) { - $realEntry = realpath($fullPath); - if ($realEntry === false || (!$allowExternalSymlinks && !isPathInsideBase($realEntry, $baseDir))) { - continue; - } - } - $entries[] = [ - 'type' => 'dir', - 'name' => $entry, - 'time' => (int) filemtime($fullPath), - 'size' => 0, - ]; - } else { - if (!isDisplayableFile($entry, $filesToHide, $dangerousExtensions, $showHiddenFiles)) { - continue; - } - $fileSize = (int) filesize($fullPath); - $totalSize += $fileSize; - $fileCount++; - $entries[] = [ - 'type' => 'file', - 'name' => $entry, - 'time' => (int) filemtime($fullPath), - 'size' => $fileSize, - ]; - } -} -closedir($dirHandle); - -// Sort entries -$lowercaseProtectedFoldersTable = array_change_key_case($protectedFolders, CASE_LOWER); - -usort($entries, function (array $a, array $b) use ($sortBy, $order, $showDirectoriesFirst): int { - if ($showDirectoriesFirst && $a['type'] !== $b['type']) { - return $a['type'] === 'dir' ? -1 : 1; - } - $cmp = match ($sortBy) { - 'time' => $a['time'] <=> $b['time'], - 'size' => $a['size'] <=> $b['size'], - default => strnatcasecmp($a['name'], $b['name']), - }; - return $order === 'desc' ? -$cmp : $cmp; -}); - -// =================== SEND HEADERS & RENDER =================== - -sendSecurityHeaders($nonce); - -$canonicalUrl = getCanonicalURL(); -$displayPath = $requestedPath === '' ? '/' : '/' . $requestedPath . '/'; -$pathParts = $requestedPath !== '' ? explode('/', $requestedPath) : []; -$totalSizeHuman = humanizeFilesize($totalSize, $sizeDecimals); - -$titleDisplay = str_replace('{{path}}', $displayPath, $title); -$subtitleDisplay = str_replace( - ['{{files}}', '{{size}}'], - [(string) $fileCount, $totalSizeHuman], - $subtitle -); - -$sortToggle = $order === 'asc' ? 'desc' : 'asc'; -$isRootDir = ($requestedPath === ''); - -?> - - - - - - - <?= e($titleDisplay) ?> - - - - - - - - - - -
-
- - -
- -

-

-
- - -
- -
- - -
-
- - - - - - - - - - - - - - - - - encodeRelativePath($itemRelPath)]); - $itemRelPathLower = strtolower($itemRelPath); - $isProtected = array_key_exists($itemRelPathLower, $lowercaseProtectedFoldersTable) - || array_key_exists(strtolower($itemName), $lowercaseProtectedFoldersTable); - ?> - - - - - - - encodeRelativePath($itemRelPath)]); - $downloadUrl = '/' . encodeRelativePath($itemRelPath); - $fileSizeHuman = humanizeFilesize((int) $item['size'], $sizeDecimals); - $ext = strtolower(pathinfo($itemName, PATHINFO_EXTENSION)); - $fileIcon = match ($ext) { - 'pdf' => '📄', - 'zip', 'rar', '7z', 'tar', 'gz', 'bz2', 'xz' => '🗜️', - 'mp4', 'mkv', 'avi', 'mov', 'wmv', 'webm', 'flv' => '🎬', - 'mp3', 'flac', 'wav', 'aac', 'ogg', 'm4a' => '🎵', - 'jpg', 'jpeg', 'png', 'gif', 'bmp', 'webp', 'svg', 'avif' => '🖼️', - 'doc', 'docx' => '📝', - 'xls', 'xlsx' => '📊', - 'ppt', 'pptx' => '📑', - 'txt', 'md', 'log' => '📃', - 'iso', 'img' => '💿', - 'apk' => '📱', - 'exe', 'msi', 'dmg', 'deb', 'rpm', 'pkg' => '⚙️', - 'ttf', 'otf', 'woff', 'woff2' => '🔤', - default => '📎', - }; - ?> - - - - - - - - - - - - - - - - - - - -
- $requestedPath !== '' ? $requestedPath : null, - 'sort' => 'name', - 'order' => $nameSortOrder, -]); -?> - - Name - - - - - - $requestedPath !== '' ? $requestedPath : null, - 'sort' => 'time', - 'order' => $dateSortOrder, -]); -?> - - Date - - - - - - $requestedPath !== '' ? $requestedPath : null, - 'sort' => 'size', - 'order' => $sizeSortOrder, -]); -?> - - Size - - - - - Hash
-
- - $parentPath !== '' ? $parentPath : null]); - ?> - Parent Directory -
-
-
- - - 🔒 -
-
- 0 ? e(date($dateFormat, $itemTime)) : '-' ?> - 0 ? e(date('d/m/y', $itemTime)) : '-' ?> - ——
-
- - -
-
- 0 ? e(date($dateFormat, $itemTime)) : '-' ?> - 0 ? e(date('d/m/y', $itemTime)) : '-' ?> - - Hash -
-
- -
No files or directories found
-
This folder is empty.
-
-
-
- -
No files matching your search
-
Try a different keyword.
-
-
-
-
- -
-

PHP Directory Browser •

-
- -
-
- - - - - +PLACEHOLDER_INDEX \ No newline at end of file From 92f134f03a0d2293082786b506c5456ad36f260c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E0=BC=BA=20Initial=20H=20=E0=BC=BB?= Date: Mon, 3 Aug 2026 01:13:18 +0700 Subject: [PATCH 2/2] fix(v3.9): push actual fixed index.php, updated .cspell.json & ZIP artifact workflow --- .cspell.json | 7 +------ .github/workflows/build-artifact.yml | 1 + index.php | 5 ++++- 3 files changed, 6 insertions(+), 7 deletions(-) create mode 100644 .github/workflows/build-artifact.yml diff --git a/.cspell.json b/.cspell.json index 91ad759..43e9a27 100644 --- a/.cspell.json +++ b/.cspell.json @@ -1,6 +1 @@ -{ - "version": "0.2", - "language": "en,id", - "dictionaries": ["en_US", "softwareTerms", "php"], - "ignorePaths": ["README.md", "LICENSE", ".git/**", ".trunk/**"] -} +PLACEHOLDER_CSPELL \ No newline at end of file diff --git a/.github/workflows/build-artifact.yml b/.github/workflows/build-artifact.yml new file mode 100644 index 0000000..f082958 --- /dev/null +++ b/.github/workflows/build-artifact.yml @@ -0,0 +1 @@ +PLACEHOLDER_WORKFLOW \ No newline at end of file diff --git a/index.php b/index.php index 707a036..2d93ec8 100644 --- a/index.php +++ b/index.php @@ -1 +1,4 @@ -PLACEHOLDER_INDEX \ No newline at end of file +