<?php

declare(strict_types=1);

namespace Acms\Services\Field;

use Acms\Services\Facades\Entry;
use Acms\Services\Facades\PublicStorage;
use Field;
use Field_Validation;

/**
 * file 型カスタムフィールドのアップロード処理。
 *
 * テキストファイル（PHP 等のスクリプトの可能性がある）判定・MIME と拡張子の検証・
 * 保存先パスの確保・旧ファイルの削除予約を担う。
 *
 * 移設前は FieldExtractor::extract() 内の 325 行のブランチだった。
 * ロジックはそのまま移し、外側のフィールドループを抜ける continue のみ return へ置き換えている。
 */
class FileFieldHandler extends UploadFieldHandler
{
    /**
     * @inheritDoc
     */
    public function handle(
        string $fd,
        Field_Validation $field,
        Field $post,
        Field $deleteField,
        ProcessingFiles $processing,
        array $allowFileExtensions
    ): void {
        $ARCHIVES_DIR = ARCHIVES_DIR;

        if (empty($_FILES[$fd])) {
            $deleteField->setField($fd . '@path', []);
            $deleteField->setField($fd . '@baseName', []);
            $deleteField->setField($fd . '@fileSize', []);
            $deleteField->setField($fd . '@secret', []);
            $deleteField->setField($fd . '@downloadName', []);

            $field->deleteField($fd . '@path');
            $field->deleteField($fd . '@baseName');
            $field->deleteField($fd . '@fileSize');
            $field->deleteField($fd . '@secret');
            $field->deleteField($fd . '@downloadName');

            return;
        }

        $aryC = $this->normalizer->candidates($_FILES[$fd]);

        //--------------------------
        // field copy to local vars
        foreach (['old', 'edit', 'extension', 'filename', 'secret', 'fileSize', 'downloadName', 'originalName', 'baseName'] as $key) {
            foreach ($aryC as $i => $c) {
                $_field = $fd . '@' . $key;
                if ($key === 'extension') {
                    $c[$key] = $post->isExists($_field, $i) ?
                        $post->getArray($_field) : '';
                } else {
                    $c[$key] = $post->isExists($_field, $i) ?
                        $post->get($_field, '', $i) : '';
                }
                $aryC[$i] = $c;
            }
            $post->delete($fd . '@' . $key);
        }

        // 参照用の配列を作成して，ファイル数の分だけインデックスを初期化
        $aryPath    = [];
        $aryName    = [];
        $aryOriginalName = [];
        $aryDownloadName = [];
        $arySize    = [];
        $arySecret  = [];
        foreach ($aryC as $c) {
            $aryPath[] = $aryName[] = $aryOriginalName[] = $aryDownloadName[] = $arySize[] = $arySecret[] = '';
        }

        $cnt    = count($aryPath);

        for ($i = 0; $i < $cnt; $i++) {
            $c      = $aryC[$i];
            // 各配列のインデックス位置を，ローカル変数に参照させる
            $_path  =& $aryPath[$i];
            $_name  =& $aryName[$i];
            $_orginal_name =& $aryOriginalName[$i];
            $_download_name =& $aryDownloadName[$i];
            $_size  =& $arySize[$i];
            $_secret=& $arySecret[$i];

            // @rawfilename の展開・null バイト検査・パスの半正規化・.htaccess 拒否
            // （image / file で完全に同一だった前処理）
            $sanitized = $this->normalizer->sanitizePathOptions(
                $c['old'],
                $c['filename'],
                $c['_name']
            );
            if ($sanitized === null) {
                continue;
            }
            $c['old'] = $sanitized['old'];
            $c['filename'] = $sanitized['filename'];

            // アップロード処理中のファイルかどうか
            $isProcessing = $processing->contains($ARCHIVES_DIR . $c['old']);

            //---------------------
            // シークレットチェック
            $secretCheck = ( 1
                && !sessionWithContribution()
                && !empty($c['old'])
                && ( 0
                    or 'delete' == $c['edit']
                    or !empty($c['_tmp_name'])
                )
            ) ? ($c['secret'] == md5($fd . '@' . $c['old'])) : true;

            //----------------------------
            // delete ( 指定削除 continue )
            if ('delete' === $c['edit'] && !empty($c['old']) && $secretCheck && !$isProcessing) {
                $this->markFieldFileForRemoval($field, $fd, $c['old'], 'file');
                continue;
            }

            //--------
            // upload
            if (!empty($c['_tmp_name']) and $secretCheck) {
                $tmp_name   = $c['_tmp_name'];
                if (!is_uploaded_file($tmp_name)) {
                    continue;
                }
                // 拡張子がなければリジェクト
                if (!preg_match('@\.([^.]+)$@', $c['_name'], $match)) {
                    continue;
                }

                // テキストファイル（=PHPなどのスクリプトファイル）判定
                // ファイルの先頭1000行を取得
                // 文字コードが判別不能な文字列をバイナリとみなす
                if ('on' == config('file_prohibit_textfile')) {
                    $fp = fopen($c['_tmp_name'], 'rb');
                    if ($fp === false) {
                        continue;
                    }
                    $readedLine = 0;
                    $sampleLine = 1000;
                    $sample = '';

                    while (($line = fgets($fp, 4096)) !== false) {
                        if ($readedLine++ > $sampleLine) {
                            break;
                        }
                        $sample .= $line;
                    }

                    fclose($fp);

                    // @todo security:
                    // mb_detect_encodingを利用しているが、これはUTF-16を判定できないため、バイナリファイルと見なしてしまう
                    // 冒頭をUTF-16、以後をUTF-8にすることで不正なテキストファイルをarchivesにアップロードできる可能性がある
                    // ただし、htaccessをいじられたりしない限りは基本的に問題にならない（通常はPHP等として実行できない）
                    if (false !== detectEncode($sample)) {
                        continue;
                    }
                }

                //------------------------------
                // dirname, basename, extension
                // アップロードされた実ファイルの拡張子が実質的に利用される
                // extensionオプションや、filenameオプションの制限は、
                // 意図する拡張子のファイルがアップロードされているかのチェックのみに使われる

                // 実ファイルの拡張子
                $extension  = $match[1];

                if (!empty($c['filename'])) {
                    if (!preg_match('@((?:[^/]*/)*)((?:[^.]*\.)*)(.*)$@', sprintf('%03d', BID) . '/' . $c['filename'], $match)) {
                        throw new \RuntimeException('アップロードファイルのパス解析に失敗しました。');
                    }

                    // @filenameオプションの拡張子
                    $c['filename_extension']  = $match[3];

                    // @filenameオプションの指定内に拡張子がないと，ファイル名とファイル名の拡張子が同一になる | @todo issue: 先行する正規表現を改善する
                    // ディレクトリのみでファイル名は無指定の場合は、拡張子が空になる
                    //   =>  ファイル名拡張子でチェックする意図がないものとして、filename_extensionをunsetし、以降の拡張子チェックから除外する
                    if ($c['filename'] === $c['filename_extension'] || empty($c['filename_extension'])) {
                        unset($c['filename_extension']);
                    }

                    $dirname    = $match[1];
                    $basename   = !empty($match[2]) ? $match[2] . $extension      // basenameは実ファイルの拡張子とする
                                                    : uniqueString() . '.' . $extension;
                } else {
                    $dirname    = PublicStorage::archivesDir();
                    $basename   = uniqueString() . '.' . $extension;
                }
                if (
                    // mimeタイプから判定した実ファイルの種類がアップロード許可ファイルであること
                    $this->mimeTypeValidator->validateAllowedByContent($tmp_name, $allowFileExtensions) &&
                    // "実ファイルの拡張子" が "アップロード許可拡張子コンフィグ" に含まれていること
                    $this->mimeTypeValidator->validateAllowedExtension($extension, $allowFileExtensions) &&
                    // 拡張子指定オプションが空でなければ...
                    (!$c['extension'] ||
                        // "拡張子指定オプション" が "アップロード許可拡張子コンフィグ" に含まれていること
                        $this->mimeTypeValidator->validateAllowedExtension($c['extension'], $allowFileExtensions)
                    ) &&
                    // ファイル名オプションの拡張子が未定義でなければ...
                    (!isset($c['filename_extension']) ||
                        // "ファイル名オプションの拡張子" が "アップロード許可拡張子コンフィグ" に含まれていること
                        $this->mimeTypeValidator->validateAllowedExtension($c['filename_extension'], $allowFileExtensions)
                    ) &&
                    // 保存先ディレクトリの再帰的作成
                    PublicStorage::makeDirectory($ARCHIVES_DIR . $dirname)
                ) {
                    //---------------------------
                    // delete ( 古いファイルの削除 )
                    if (!empty($c['old']) && !$isProcessing) {
                        $this->markFieldFileForRemoval($field, $fd, $c['old'], 'file');
                    }

                    //------
                    // copy
                    $path     = $dirname . $basename;
                    $candidatePath = $ARCHIVES_DIR . $path;
                    Entry::addUploadedFiles($path); // 新規バージョンとして作成する時にファイルをCOPYするかの判定に利用

                    // 重複対応。パスの確保と書き込みをまとめてアトミックに行う
                    // （書き込みが失敗したら確保したパスは自動的に削除される）
                    $realpath = PublicStorage::withUniqueFilePath(
                        $candidatePath,
                        '',
                        function (string $realpath) use ($tmp_name): string {
                            if ($content = file_get_contents($tmp_name)) {
                                PublicStorage::put($realpath, $content);
                            }
                            return $realpath;
                        }
                    );
                    $path = mb_substr($realpath, strlen($ARCHIVES_DIR));

                    $processing->add($realpath);

                    $this->notifyMediaCreated($realpath);

                    //-----
                    // set
                    $_path  = $path;
                    $_name  = PublicStorage::mbBasename($realpath);
                    $_orginal_name = $c['_name'];
                    $_download_name = $c['downloadName'];
                    $_size  = PublicStorage::getFileSize($realpath);
                    $_secret = md5($fd . '@' . $path);
                    continue;
                } else {
                    $field->setMethod($fd, 'inValidFile', false);
                }
            }

            //-----
            // old
            // 非編集アップデートの時
            if (!empty($c['old'])) {
                $_path  = $c['old'];
                $_name = $c['baseName'];
                $_orginal_name = $c['originalName'];
                $_download_name = $c['downloadName'];
                $_size  = $c['fileSize'];
                $_secret = $c['secret'];
                continue;
            }
        }

        //-----------
        // set field
        $field->setField($fd . '@path', $aryPath);
        $field->setField($fd . '@baseName', $aryName);
        $field->setField($fd . '@fileSize', $arySize);
        $field->setField($fd . '@secret', $arySecret);
        $field->setField($fd . '@originalName', $aryOriginalName);
        $field->setField($fd . '@downloadName', $aryDownloadName);

        //------------
        // validation
        $key    = $fd . '@path';
        foreach ($post->listFields() as $_fd) {
            if (preg_match('/^' . $key . ':(?:v#|validator#)(.+)$/', $_fd, $match)) {
                $method = $match[1];
                $field->setMethod($key, $method, $post->get($_fd));
                $post->delete($_fd);
            }
        }
    }
}
