<?php

namespace Acms\Services\StaticExport;

/**
 * storage/ 直下のディレクトリ名が書き出し対象（添付ファイルの実体）かを判定する。
 *
 * Why not: storage/ 直下にはバックアップ・エクスポート類の非メディアディレクトリも置かれ、
 * その一覧は運用機能の追加ごとに増減し、unzip や SiteSeeder のように任意名のディレクトリを
 * 作る経路もあるため列挙しきれない。数字ディレクトリだけを通す許可リスト（既定拒否）にする。
 */
class StorageDirPolicy
{
    /**
     * 許可名として受け付ける名前の文字種。英数字・アンダースコア・ハイフン・ピリオドのみ。
     *
     * Why not: 現在の唯一の呼び出し元（Engine::copyStorageMedia）は mbBasename() 済みの値しか
     * 渡さないためパス区切り文字は実害が無いが、この判定はクラス名どおり「安全な名前だけを true
     * にする」ことを保証する必要がある。将来 $allowedNames をパス構築に使う実装が増えた時点で
     * トラバーサルが実在化するため、判定の入口で '/' '\' 等を含む名前を拒否しておく。
     */
    private const SAFE_NAME_PATTERN = '/\A[0-9A-Za-z_.-]+\z/';

    /**
     * storage/ 直下のディレクトリ名が書き出し対象（添付ファイルの実体）かを判定する。
     *
     * ブログ数が 1000 を超えると sprintf('%03d') は 4 桁を返すため桁数は固定しない。
     *
     * @param string $name ディレクトリ名（basename）
     * @param list<string> $allowedNames 設定で追加された許可名
     * @return bool
     */
    public static function isMediaDirName(string $name, array $allowedNames): bool
    {
        if (!self::isSafeName($name)) {
            return false;
        }
        if (preg_match('/\A[0-9]+\z/', $name) === 1) {
            return true;
        }
        return in_array($name, $allowedNames, true);
    }

    /**
     * 設定文字列（カンマ区切り）から許可名の配列を作る。
     *
     * Why not: explode(',', '') は [''] を返すため、trim だけでは空文字が許可名として
     * 残ってしまい isMediaDirName() で空名ディレクトリを許可してしまう。空要素を明示的に除く。
     * 併せて isSafeName() でパストラバーサルにつながる名前（'..' や '/' を含む名前）も
     * 入力の時点で捨てる（isMediaDirName() 側の防御と二重になるが、設定値の保存内容自体を
     * 安全な状態に保つため、正規化の段階でも落としておく）。
     *
     * @param string $config
     * @return list<string>
     */
    public static function normalizeAllowedNames(string $config): array
    {
        $names = [];
        // Why not: カンマだけで分割しない。管理画面の入力欄は他の書き出し設定（除外パス等）と
        // 同じテキストエリアで改行区切りのため、改行を区切りとして受け付ける必要がある。
        // カンマも受けるのは config.system.yaml を手で書くときに 1 行で並べられるようにするため。
        $separated = preg_split('/[\r\n,]+/', $config);
        foreach ($separated === false ? [] : $separated as $name) {
            $name = trim($name);
            if ($name === '') {
                continue;
            }
            if (!self::isSafeName($name)) {
                continue;
            }
            $names[] = $name;
        }
        return $names;
    }

    /**
     * ディレクトリ名として安全な文字種のみで構成され、'.' '..' でないかを判定する。
     *
     * @param string $name
     * @return bool
     */
    private static function isSafeName(string $name): bool
    {
        if ($name === '.' || $name === '..') {
            return false;
        }
        return preg_match(self::SAFE_NAME_PATTERN, $name) === 1;
    }
}
