<?php

/**
 * @package     Dotclear
 * @subpackage  Upgrade
 *
 * @copyright   Olivier Meunier & Association Dotclear
 * @copyright   AGPL-3.0
 */
declare(strict_types=1);

namespace Dotclear\Process\Upgrade;

use Dotclear\App;
use Dotclear\Helper\File\Zip\Zip;
use Dotclear\Helper\Html\Form\Checkbox;
use Dotclear\Helper\Html\Form\Div;
use Dotclear\Helper\Html\Form\Form;
use Dotclear\Helper\Html\Form\Hidden;
use Dotclear\Helper\Html\Form\Label;
use Dotclear\Helper\Html\Form\Li;
use Dotclear\Helper\Html\Form\Link;
use Dotclear\Helper\Html\Form\None;
use Dotclear\Helper\Html\Form\Note;
use Dotclear\Helper\Html\Form\Para;
use Dotclear\Helper\Html\Form\Submit;
use Dotclear\Helper\Html\Form\Text;
use Dotclear\Helper\Html\Form\Ul;
use Dotclear\Helper\Process\TraitProcess;
use Exception;

/**
 * @brief   Upgrade process corrupted files helper.
 *
 * Taken from plugin fakemeup.
 *
 * @author  Franck Paul and contributors
 *
 * @since   2.29
 */
class Digests
{
    use TraitProcess;

    private static string $path_backup;

    private static string $path_helpus;

    private static string $path_disclaimer;

    private static string $zip_name = '';

    /**
     * List of changes.
     *
     * @var     array<string, array<string, mixed> >    $changes
     */
    private static array $changes = [
        'same'    => [],
        'changed' => [],
        'removed' => [],
    ];

    public static function init(): bool
    {
        App::upgrade()->page()->checkSuper();

        return self::status(true);
    }

    public static function process(): bool
    {
        self::$path_backup = implode(DIRECTORY_SEPARATOR, [App::config()->dotclearRoot(), 'inc', 'digests.bak']);
        self::$path_helpus = (string) (App::lang()->getFilePath(App::config()->l10nRoot(), 'help/core_fmu_helpus.html', App::lang()->getLang()) ?:
            App::lang()->getFilePath(App::config()->l10nRoot(), 'help/core_fmu_helpus.html', 'en'));
        self::$path_disclaimer = (string) (App::lang()->getFilePath(App::config()->l10nRoot(), 'help/core_fmu_disclaimer.html', App::lang()->getLang()) ?:
            App::lang()->getFilePath(App::config()->l10nRoot(), 'help/core_fmu_disclaimer.html', 'en'));

        if (isset($_POST['erase_backup']) && is_file(self::$path_backup)) {
            @unlink(self::$path_backup);
        }

        try {
            if (isset($_POST['override'])) {
                $changes = self::check(App::config()->dotclearRoot(), App::config()->digestsRoot());
                $arr     = $changes['same'];
                foreach ($changes['changed'] as $k => $v) {
                    if (is_array($v) && isset($v['new'])) {
                        $arr[$k] = $v['new'];
                    }
                }

                ksort($arr);
                self::$changes = $changes;

                $digest = '';
                foreach ($arr as $k => $v) {
                    if (is_scalar($v)) {
                        $digest .= sprintf("%s  %s\n", (string) $v, $k);
                    }
                }

                rename(App::config()->digestsRoot(), self::$path_backup);
                file_put_contents(App::config()->digestsRoot(), $digest);
                self::$zip_name = self::backup(self::$changes);
            } elseif (isset($_POST['disclaimer_ok'])) {
                self::$changes = self::check(App::config()->dotclearRoot(), App::config()->digestsRoot());
            }
        } catch (Exception $exception) {
            App::error()->add($exception->getMessage());
        }

        // Mesasges
        if (isset($_POST['override'])) {
            if (self::$zip_name === '') {
                App::upgrade()->notices()->addSuccessNotice(__('The updates have been performed.'));
            }
        } elseif (isset($_POST['disclaimer_ok'])) {
            if (count(self::$changes['changed']) === 0 && count(self::$changes['removed']) === 0) {
                App::upgrade()->notices()->addWarningNotice(__('No changed filed have been found, nothing to do!'));
            }
        } elseif (file_exists(self::$path_backup)) {
            App::upgrade()->notices()->addErrorNotice(__('This tool has already been run once.'));
        }

        return true;
    }

    public static function render(): void
    {
        $download = isset($_GET['download']) && is_string($download = $_GET['download']) ? $download : '';
        if ($download !== '' && preg_match('/^fmu_backup_\d{14}.zip$/', $download)) {
            $f = App::config()->varRoot() . DIRECTORY_SEPARATOR . $download;
            if (is_file($f)) {
                $c = (string) file_get_contents($f);
                header('Content-Disposition: attachment;filename=' . $download);
                header('Content-Type: application/x-zip');
                header('Content-Length: ' . strlen($c));
                echo $c;
                dotclear_exit();
            }
        }

        App::upgrade()->page()->open(
            __('Files'),
            '',
            App::upgrade()->page()->breadcrumb(
                [
                    __('Dotclear update') => '',
                    __('Corrupted files') => '',
                ]
            )
        );

        if (App::error()->flag()) {
            App::upgrade()->page()->close();

            return;
        }

        echo (new Div())
            ->items([
                (new Note())
                    ->class('static-msg')
                    ->text(__('On this page, you can bypass corrupted files or modified files in order to perform update.')),
            ])
            ->render();

        if (isset($_POST['override'])) {
            $item = self::$zip_name === '' ? (new None()) : (new Text(
                null,
                is_file(self::$path_helpus) ?
                sprintf((string) file_get_contents(self::$path_helpus), App::upgrade()->url()->get('upgrade.digests', ['download' => self::$zip_name]), self::$zip_name, 'fakemeup@dotclear.org') :
                '<a href="' . App::upgrade()->url()->get('upgrade.digests', ['download' => self::$zip_name]) . '">' . __('Download backup of digests file.') . '</a>'
            ));

            echo (new Div())
                ->class('fieldset')
                ->items([
                    $item,
                    (new Para())->items([
                        (new Link())
                            ->class('button submit')
                            ->href(App::upgrade()->url()->get('upgrade.upgrade'))
                            ->text(__('Update Dotclear')),
                    ]),
                ])
            ->render();
        } elseif (isset($_POST['disclaimer_ok'])) {
            if (count(self::$changes['changed']) === 0 && count(self::$changes['removed']) === 0) {
                echo (new Div())
                    ->class('fieldset')
                    ->items([
                        (new Note())
                            ->text(__('Digests file is up to date.')),
                        (new Link())
                            ->class('button submit')
                            ->href(App::upgrade()->url()->get('upgrade.upgrade'))
                            ->text(__('Update Dotclear')),
                    ])
                    ->render();
            } else {
                $changed       = [];
                $block_changed = '';
                if (count(self::$changes['changed']) !== 0) {
                    foreach (self::$changes['changed'] as $k => $v) {
                        if (is_array($v)
                            && isset($v['old'])
                            && isset($v['new'])
                            && is_scalar($v['old'])
                            && is_scalar($v['new'])
                        ) {
                            $changed[] = (new Li())->text(sprintf('%s [old:%s, new:%s]', $k, (string) $v['old'], (string) $v['new']));
                        }
                    }

                    $block_changed = (new Div())
                        ->items([
                            (new Para())
                                ->items([
                                    (new Text(null, __('The following files will have their checksum faked:'))),
                                ]),
                            (new Ul())
                                ->items($changed),
                        ])
                        ->render();
                }

                $removed       = [];
                $block_removed = '';
                if (count(self::$changes['removed']) !== 0) {
                    foreach (self::$changes['removed'] as $k => $v) {
                        $removed[] = (new Li())->text((string) $k);
                    }

                    $block_removed = (new Div())
                        ->items([
                            (new Para())
                                ->items([
                                    (new Text(null, __('The following files digests will have their checksum cleaned:'))),
                                ]),
                            (new Ul())
                                ->items($removed),
                        ])
                        ->render();
                }

                echo (new Form('frm-override'))
                    ->class('fieldset')
                    ->action(App::upgrade()->url()->get('upgrade.digests'))
                    ->method('post')
                    ->fields([
                        (new Text(null, $block_changed)),
                        (new Text(null, $block_removed)),
                        (new Submit(['confirm'], __('Still ok to continue'))),
                        (new Hidden(['override'], (string) 1)),
                        App::nonce()->formNonce(),
                    ])
                    ->render();
            }
        } elseif (file_exists(self::$path_backup)) {
            echo (new Form('frm-erase'))
                ->class('fieldset')
                ->action(App::upgrade()->url()->get('upgrade.digests'))
                ->method('post')
                ->fields([
                    (new Para())
                        ->items([
                            (new Checkbox('erase_backup'))
                                ->value(1)
                                ->label((new Label(__('Remove the backup digest file, I want to play again'), Label::INSIDE_TEXT_AFTER))),
                        ]),
                    (new Para())
                        ->items([
                            (new Submit(['confirm'], __('Continue'))),
                            App::nonce()->formNonce(),
                        ]),
                ])
            ->render();
        } else {
            echo (new Form('frm-disclaimer'))
                ->class('fieldset')
                ->action(App::upgrade()->url()->get('upgrade.digests'))
                ->method('post')
                ->fields([
                    (new Div())
                        ->items([(new Text(null, is_file(self::$path_disclaimer) ? (string) file_get_contents(self::$path_disclaimer) : '...'))]),
                    (new Para())
                        ->items([
                            (new Checkbox('disclaimer_ok'))
                                ->value(1)
                                ->label((new Label(__('I have read and understood the disclaimer and wish to continue anyway.'), Label::INSIDE_TEXT_AFTER))),
                        ]),
                    (new Para())
                        ->items([
                            (new Submit(['confirm'], __('Continue'))),
                            App::nonce()->formNonce(),
                        ]),
                ])
                ->render();
        }

        App::upgrade()->page()->close();
    }

    /**
     * Check digest file.
     *
     * @param   string  $root           The root
     * @param   string  $digests_file   The digests file
     *
     * @throws  Exception
     *
     * @return  array<string, array<string, mixed>>
     */
    private static function check(string $root, string $digests_file): array
    {
        if (!is_readable($digests_file)) {
            throw new Exception(__('Unable to read digests file.'));
        }

        $opts     = FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES;
        $contents = file($digests_file, $opts);
        if (!$contents) {
            return [
                'same'    => [],
                'changed' => [],
                'removed' => [],
            ];
        }

        $changed = [];
        $same    = [];
        $removed = [];

        $none = true;
        foreach ($contents as $digest) {
            if (!preg_match('#^([\da-f]{32})\s+(.+?)$#', $digest, $m)) {
                continue;
            }

            $none     = false;
            $md5      = $m[1];
            $filename = $root . '/' . $m[2];

            # Invalid checksum
            if (is_readable($filename)) {
                $md5_new = md5_file($filename);
                if ($md5 == $md5_new) {
                    $same[$m[2]] = $md5;
                } else {
                    $changed[$m[2]] = ['old' => $md5,'new' => $md5_new];
                }
            } else {
                $removed[$m[2]] = true;
            }
        }

        # No checksum found in digests file
        if ($none) {
            throw new Exception(__('Invalid digests file.'));
        }

        return [
            'same'    => $same,
            'changed' => $changed,
            'removed' => $removed,
        ];
    }

    /**
     * Backup digest.
     *
     * @param   array<string, array<string, mixed>>     $changes    The changes
     *
     * @return  string  False on error, zip name on success
     */
    private static function backup(array $changes): string
    {
        $zip_name      = sprintf('fmu_backup_%s.zip', date('YmdHis'));
        $zip_file      = sprintf('%s/%s', App::config()->varRoot(), $zip_name);
        $checksum_file = sprintf('%s/fmu_checksum_%s.txt', App::config()->varRoot(), date('Ymd'));

        $c_data = 'Fake Me Up Checksum file - ' . date('d/m/Y H:i:s') . "\n\n" .
            'Dotclear version : ' . App::config()->dotclearVersion() . "\n\n";
        if (count($changes['removed'])) {
            $c_data .= "== Removed files ==\n";
            foreach (array_keys($changes['removed']) as $k) {
                $c_data .= sprintf(" * %s\n", $k);
            }

            $c_data .= "\n";
        }

        if (file_exists($zip_file)) {
            @unlink($zip_file);
        }

        $b_fp = @fopen($zip_file, 'wb');
        if ($b_fp === false) {
            return '';
        }

        $b_zip = new Zip($b_fp);

        if (count($changes['changed'])) {
            $c_data .= "== Invalid checksum files ==\n";
            foreach ($changes['changed'] as $k => $v) {
                $name = substr($k, 2);
                if (is_array($v)
                    && isset($v['old'])
                    && isset($v['new'])
                    && is_scalar($v['old'])
                    && is_scalar($v['new'])
                ) {
                    $c_data .= sprintf(" * %s [expected: %s ; current: %s]\n", $k, (string) $v['old'], (string) $v['new']);

                    try {
                        $b_zip->addFile(App::config()->dotclearRoot() . '/' . $name, $name);
                    } catch (Exception $e) {
                        $c_data .= $e->getMessage();
                    }
                }
            }
        }

        file_put_contents($checksum_file, $c_data);
        $b_zip->addFile($checksum_file, basename($checksum_file));

        $b_zip->write();
        fclose($b_fp);
        $b_zip->close();

        @unlink($checksum_file);

        return $zip_name;
    }
}
