<?php

namespace app;

use DateTime;
use Yii;
use linslin\yii2\curl\Curl;

/**
 * Модель для выгрузки страниц в автономное хранилище
 * 
 * Позволяет скачивать, сохранять и оптимизировать под нужную архитектуру
 * 
 * @method download(string $link, int $depth = 0, int $buffer = 0, bool $searchExternal = false) Скачивает страницу
 * @method save() Сохраняет скачанные страницы на диск
 * @method saveStatistics() Сохранение статистики
 * @method convertLinks(array $links = null) Конвертер ссылок
 * @method convertFiles(array $files = null) Конвертер страниц
 * @method filterLink(string $link) Фильтрация ссылки
 * 
 * @author Арсен Мирзаев red@hood.su
 * 
 * @todo
 * 1. $searchExternal переделать в $depthExterntal
 * 2. Исправить обработку ссылки: zakupki.gov.ru/data/common-info.html?regNumber=0816500000619001511
 * 4. Конструкцию <base> надо доработать на проверку уже существующего тега
 */
class Dumper extends \yii\base\Component
{
    /**
     * Глубина поиска страниц относительно первичной
     */
    protected $depth = 0;

    /**
     * Буфер страниц для скачивания
     */
    protected $buffer;

    /**
     * Флаг форсированного выполнения (перезаписи файлов)
     */
    protected $force;

    /**
     * Путь для сохранения файлов
     */
    protected $path;

    /**
     * Буфер страниц для скачивания
     */
    protected $searchExternal;

    /**
     * Регистр обработанных ссылок
     * 
     * 'Ссылка' => [
     *      [0] => 'URN файла'
     *      [1] => 'URL файла'
     *      [2] => 'URI файла для конвертации страниц'
     * ]
     */
    protected $links = [];

    /**
     * Буфер скачанных файлов
     * 
     * 'Файл (URN)' => [
     *      [0] => 'Тип'
     *      [1] => 'Данные'
     * ]
     * 
     * Типы:
     * [0] - HTML страница
     * [1] - документ ('.css', '.js', '.png'...)
     */
    protected $filesBuffer = [];

    /**
     * Регистр сохранённых файлов
     * 
     * 'Файл (URN)' => [
     *      [0] => 'Тип'
     *      [1] => 'Данные'
     * ]
     * 
     * Типы:
     * [0] - HTML страница
     * [1] - документ ('.css', '.js', '.png'...)
     */
    protected $files = [];

    /**
     * Блокировка циклов
     * 
     * Указывает работает основное скачивание или рекурсивное
     */
    protected $subdownload = false;

    /**
     * Количество новых найденных ссылок
     */
    protected $linksNew = 0;

    /**
     * Запрашиваемая ссылка
     */
    protected $target;

    /**
     * SCHEME/PROTOCOL запроса
     */
    public $connectionProtocol;

    /**
     * HOST запроса
     */
    public $connectionHost;

    /**
     * Собранная информация о выполнении
     * 
     * [0] => 'Запрошенный URI'
     * [1] => 'Статус выполнения (завершен или ошибка)'
     * [2] => [
     *     [0] => 'Количество найденных ссылок'
     *     [1] => 'Количество найденных ссылок без дубликатов'
     *     [2] => 'Количество обработанных ссылок'
     * ]
     * [3] => [
     *     [0] => 'Количество найденных HTML страниц'
     *     [1] => 'Количество конвертированных страниц'
     * ]
     * [4] => [
     *     [0] => 'Количество найденных документов (.png, .css, .pdf)'
     *     [1] => 'удалено'
     *     [2] => 'Найдено: Изображения',
     *     [3] => 'Найдено: Видеозаписи',
     *     [4] => 'Найдено: Аудиозаписи',
     *     [5] => 'Найдено: CSS',
     *     [6] => 'Найдено: JS',
     *     [7] => 'Найдено: Не опознано',
     * ]
     */
    public $statistics = [
        '',
        1,
        [
            0,
            0,
            0
        ],
        [
            0,
            0,
            0
        ],
        [
            0,
            0,
            0,
            0,
            0,
            0,
            0,
            0,
        ]
    ];

    /**
     * Время начала выполнения скрипта
     * 
     * Используется для вычисления времени выполнения и записи в статистику
     */
    public $timeStart;

    public function __construct()
    {
        // Начало отсчёта синтетического теста времени выполнеия для записи в статистику
        $this->timeStart = microtime(true);

        // if (YII_DEBUG) {
            register_shutdown_function(array(&$this, 'saveStatistics'));
        // }
    }

    /**
     * Скачивание страницы
     * 
     * @param string $link Ссылка
     * @param int $depth Глубина скачивания вложенных ссылок
     * @param int $buffer Буфер файлов
     * @param bool $force Флаг форсированного выполнения (с перезаписью существующих файлов)
     * @param string $path Свой путь для сохранения
     * @param bool $searchExternal Флаг поиска ссылок во внешних сайтах
     * 
     * @return Dump
     */
    public function download($link, $depth = 0, $buffer = 0, $force = false, $searchExternal = false, $path = '')
    {
        if (!$link = $this->filterLink($link)) {
            // Если ссылка не прошла фильтрацию
            return;
        }

        // Инициализация свойств
        if (!isset($this->buffer)) {
            $this->buffer = $buffer;
        }

        if (!isset($this->force)) {
            $this->force = $force;
        }

        if (!isset(Yii::$app->params['basePath'])) {
            Yii::$app->params['basePath'] = $path;
        }

        if (!isset($this->searchExternal)) {
            $this->searchExternal = $searchExternal;
        }

        if (!isset($this->connectionProtocol)) {
            // Проверка наличия domain.zone в ссылке на подобие: 'https://domain.zone/foo/bar'
            preg_match_all('/(.*)?:?(\/\/|\\\\)(.*)((\/|\\\|$).*$)/U', $link, $linkMatch);

            $this->connectionProtocol = $linkMatch[1][0] ?? $_SERVER['REQUEST_SCHEME'] ?? $_SERVER['HTTP_X_FORWARDED_PROTO'] ?? 'http';
        }

        if (!isset($this->connectionHost)) {
            // Проверка наличия domain.zone в ссылке на подобие: 'https://domain.zone/foo/bar'
            preg_match_all('/(.*)?:?(\/\/|\\\\)(.*)((\/|\\\|$).*$)/U', $link, $linkMatch);

            // Проверка наличия domain.zone в ссылке на подобие: 'domain.zone/' или 'domain.zone' или 'subdomain.domain.zone'
            preg_match_all('/^([^\\\|\/|\\s]+\.[^\\\|\/|\\s]+)(\\\|\/)?$/', $link, $domainMatch);

            $this->connectionHost = $linkMatch[3][0] ?? $domainMatch[1][0] ?? $_SERVER['REQUEST_HOST'] ?? $_SERVER['HTTP_HOST'] ?? '127.0.0.1';
        }

        // Обработка буфера (это весь его код)
        if (count($this->filesBuffer) >= $this->buffer) {
            $this->save();
        }

        // Если скачивание является первым (ручной запрос)
        if (!isset($this->target)) {
            // Инициализация стартовой ссылки
            $this->target = $link;

            // Добавление стартовой ссылки в регистр
            array_unshift($this->links, $link);

            // Инициализация данных для статистики
            $this->statistics[0] = $this->target;
            $this->statistics[2][1] = 1;
        } else if ($this->subdownload && $depth === 0) {
            // Иначе если это дополнительное скачивание и глубина равна нулю

            $this->linksNew++;
            // Добавление стартовой ссылки в регистр
            array_unshift($this->links, $link);
        }

        // Обработка стартовой ссылки
        if (isset($this->target)) {
            // Внимание: $targetMatch не очищается и используется в коде ниже
            preg_match_all('/(.*)?:?(\/\/|\\\\)(.*)((\/|\\\|$).*$)/U', $this->target, $targetMatch);
        }

        if (preg_match('/^[^\/|\\\]+\..+$/', $link)) {
            // Паттерн: 'domain.zone', 'domain.zone/foo/bar/index.html'
            $request = ($this->connectionProtocol ?? 'http') . '://' . $link;
        } else if (preg_match_all('/(^http.*(\/\/|\\\\)|^(\/\/|\\\\))(.*)(\/|\\\|$)(.*$)/iU', $link, $match)) {
            // Паттерн: 'https://domain.zone/foo/index.html', '//domain.zone/foo'
            $request = ($this->connectionProtocol ?? 'http') . '://' . $match[4][0] . '/' . $match[6][0];
        } else if (preg_match('/(^\/[^\/\\\\\s]+[^\s]*$|^\\\[^\/\\\\\s]+[^\\s]*$|^\/$|^\\\$)/', $link)) {
            // Паттерн: '/', '/foo/bar', '/foo/bar/index.html'
            $request = ($this->connectionProtocol ?? 'http') . '://' . ($targetMatch[3][0] ?? $this->connectionHost) . $link;
        } else if (preg_match('/(^[^\/\\\\\s\.]+(\/|\\\|$)([^\/\\\\\s]*$|[^\/\\\\\s]+(\/|\\\).*))/', $link)) {
            // Паттерн: 'foo/index.html', 'foo/bar/index.html', 'foo', 'foo/'
            $request = ($this->connectionProtocol ?? 'http') . '://' . ($targetMatch[3][0] ?? $this->connectionHost) . '/' . $link;
        } else {
            unset($this->links[$link]);
            return $this;
        }
        unset($match); // Очистка на всякий случай, так как переменные остаются

        // Выполнение запроса
        $this->filesBuffer[$link][1] = (new Curl())->setOption(CURLOPT_RETURNTRANSFER, true)
            ->setOption(CURLOPT_FOLLOWLOCATION, true)
            ->setOption(CURLOPT_SSL_VERIFYPEER, true)
            ->setOption(CURLOPT_USERAGENT, Yii::$app->params['useragent'])
            ->get($request);

        if (preg_match('/(https?:(\/\/|\\\\).+(\/|\\\).+|^(\/|\\\).+)(\.(?!php|htm)[A-z0-9]+)[^\/\\\s]*$/i', $link)) {
            // Если ссылка является документом ('.css', '.js', '.png'...)
            $this->filesBuffer[$link][0] = 1;
        } else {
            // Иначе расценивается как HTML страница для продолжения поиска
            preg_match_all('/(.*)?:?(\/\/|\\\\)(.*)((\/|\\\|$).*$)/U', $link, $linkMatch);

            // Если это внутренний URL или domain.zone цели сходится с domain.zone обрабатываемой ссылки или есть разрешение на проход внешних ссылок
            if (empty($linkMatch[3][0]) || $targetMatch[3][0] === $linkMatch[3][0] || $this->searchExternal === true) {
                $this->filesBuffer[$link][0] = 0;
                $file = $this->filesBuffer[$link][1];
            } else {
                $this->filesBuffer[$link][0] = 0;
            }
        }
        unset($targetMatch, $linkMatch, $link); // Очистка на всякий случай, так как переменные остаются

        // Извлечение ссылок из страницы по свойствам href='' и src=''
        // Единственное место где добавляются найденные ссылки
        if (!empty($file) && $depth > 0 && preg_match_all('/(href|src)\\s?=\\s*[\"\']?((?!(\"|\'))(?!tel)(?!mailto)[^\"\']+)[\"\']/i', $file, $match)) {
            // Если файл скачан, глубина больше нуля и были найдены ссылки

            // Прибавление количества новых ссылок для обработки
            $this->linksNew += count($match[2]);

            // Прибавление количества новых ссылок для вывода в статистике
            $this->statistics[2][0] += count($match[2]);

            // Добавление ссылок в общий регистр
            foreach ($match[2] as $link) {
                if (!$link = $this->filterLink($link)) {
                    // Если ссылка не прошла фильтрацию
                    continue;
                }
                array_unshift($this->links, $link);
            }
        }
        unset($link, $file, $match); // Очистка на всякий случай, так как переменные остаются

        // Конвертация ссылок
        if (isset($this->links) && $this->linksNew) {
            // Если глубина больше ноля, ссылки существуют и счетчик новых ссылок больше ноля

            $this->convertLinks();
        }

        if (!$this->subdownload) {
            // Если это не дополнительное скачивание и текущая глубина не равна нулю

            // Устанавливается для того, чтобы работать в цикле
            $this->depth = $depth;

            // Скачиваем найденные страницы по установленной глубине поиска
            while ($this->depth-- > 0) {
                foreach ($this->links as $link => $type) {
                    $this->subdownload = true;
                    $this->download($link, $this->depth);
                    $this->subdownload = false;
                }

                $this->convertFiles($this->save(), true);
            }
            unset($link, $type);
        } 

        // Сохранение остатков ссылок после обработки
        $this->save();

        // Конвертация сохранённых файлов
        $this->convertFiles();

        return $this;
    }

    /**
     * Сохранение страницы
     * 
     * Получает на вход файлы из буфера и сохраняет на диске
     * На выходе будут перенесены в массив $this->files
     * 
     * @return array
     */
    public function save($files = null)
    {
        if (empty($files)) {
            $files = &$this->filesBuffer;
        }

        if (isset($this->links)) {
            $this->convertLinks();
        }

        $savedFiles = [];

        foreach ($files as $link => $file) {
            if (!empty($this->links[$link][1]) && !preg_match('/^(\/|\\\)/', $this->links[$link][1])) {
                // Если в начале ссылки нет слеша и ссылка не, то добавить слеш
                $this->links[$link][1] = '/' . $this->links[$link][1];
            }

            // Проверка существования каталога и его создание 
            if (!file_exists(Yii::$app->params['basePath'] . $this->links[$link][1])) {
                mkdir(Yii::$app->params['basePath'] . $this->links[$link][1], 0755, true);
            }

            // Сохранение файла
            if (!file_exists(Yii::$app->params['basePath'] . $this->links[$link][1] . $this->links[$link][0]) || $this->force) {
                if (file_put_contents(Yii::$app->params['basePath'] . $this->links[$link][1] . $this->links[$link][0], $file[1])) {
                    $this->files[$link][0] = $file[0];
                    $this->files[$link][1] = $file[1];
                }
            }
            $savedFiles[$link] = $files[$link];
            unset($files[$link]);
        }
        unset($link, $file); // Очистка на всякий случай, так как переменные остаются

        // Указание сборщику статистики, что парсер успешно завершил свою работу
        $this->statistics[1] = 0;

        return $savedFiles;
    }

    /**
     * Сохранить статистику
     * 
     * Возвращает статус сохранения (true/false)
     * 
     * @return bool
     */
    public function saveStatistics()
    {
        // Запись времени окончания работы скрипта
        $timeFinish = microtime(true);

        $i = new DateTime(Yii::$app->params['timezone'] ?? 'Europe/Moscow');

        $date = date_format($i, 'Y-m-d');
        $dateFull = date_format($i, 'Y.m.d H:i:s');

        $request            = $this->statistics[0]                ?? 'Ошибка';
        $time               = ($timeFinish - $this->timeStart)    ?? 'Ошибка';
        $status             = $this->statistics[1] === 0 ? 'Успех' : 'Ошибка';

        $linksCount         = $this->statistics[2][0]             ?? 'Ошибка';
        $linksProcessed     = $this->statistics[2][1]             ?? 'Ошибка';
        $linksProcessedReal = $this->statistics[2][2]             ?? 'Ошибка';

        $pagesCount         = $this->statistics[3][0]             ?? 'Ошибка';
        $pagesProcessed     = $this->statistics[3][1]             ?? 'Ошибка';

        $filesCount         = $this->statistics[4][0]             ?? 'Ошибка';

        $imagesCount        = $this->statistics[4][2]             ?? 'Ошибка';
        $videosCount        = $this->statistics[4][3]             ?? 'Ошибка';
        $audiosCount        = $this->statistics[4][4]             ?? 'Ошибка';
        $cssCount           = $this->statistics[4][5]             ?? 'Ошибка';
        $jsCount            = $this->statistics[4][6]             ?? 'Ошибка';
        $unidentifiedCount  = $this->statistics[4][7]             ?? 'Ошибка';

        if (!file_exists(Yii::getAlias('@runtime/logs'))) {
            mkdir(Yii::getAlias('@runtime/logs'), 0755, true);
        }
        $file = fopen(Yii::getAlias('@runtime/logs') . '/' . $date . uniqid('_DUMPER_', true) . '.log', 'a+');

        fwrite($file, <<<EOT
///////////////////////////////////////////////////////////
///                Статистика выполнения                ///
///////////////////////////////////////////////////////////

                       Дата:   $dateFull

                     Запрос:   $request
           Время выполнения:   $time секунд
          Статус выполнения:   $status

///////////////////////////////////////////////////////////

             Найдено ссылок:   $linksCount
          Обработано ссылок:   $linksProcessed
      Из них без дубликатов:   $linksProcessedReal

//////////////////////////////////////////////////////////

            Найдено страниц:   $pagesCount
     Конвертировано страниц:   $pagesProcessed

///////////////////////////////////////////////////////////

         Найдено документов:   $filesCount

                Изображения:   $imagesCount
                Видеозаписи:   $videosCount
                Аудиозаписи:   $audiosCount
                CSS скрипты:   $cssCount
                 JS скрипты:   $jsCount
                Не опознано:   $unidentifiedCount

///////////////////////////////////////////////////////////
EOT
        );
        fclose($file);

        unset($i, $date, $dateFull, $file); // Очистка на всякий случай, так как переменные остаются
    }

    /**
     * Конвертер ссылок
     * 
     * Приводит ссылки к заданному архитектурой виду
     * 
     * @return array
     */
    private function convertLinks($links = null)
    {
        if (empty($links)) {
            $links = &$this->links;
        }

        while ($this->linksNew >= 0) {
            // Инициализация ссылки и копии для будущего поиска в файлах
            if (!array_key_exists($this->linksNew, $links)) {
                // Подготовка к следующей итерации цикла
                $this->linksNew--;
                continue;
            }
            $link = $rawLink = $links[$this->linksNew];

            if (is_array($link)) {
                // Если это уже конвертированная ссылка
                continue;
            }

            $uri = $this->initLink($link);

            preg_match_all('/(\/\/|\\\\)(.*)((\/|\\\|$).*$)/U', $uri, $uriMatch);
            preg_match_all('/(\/\/|\\\\)(.*)((\/|\\\|$).*$)/U', $this->target, $targetMatch);

            if ($targetMatch[2][0] === $uriMatch[2][0]) {
                $location = '';
            } else {
                $location = Yii::$app->params['externalLinksPath'] . '/' . $uriMatch[2][0];
            }
            unset($uriMatch, $targetMatch);

            // Инициализация ссылки
            if ($uri === $this->target || $uri . '/' === $this->target || $uri === $this->target . '/' || $uri . '\\' === $this->target || $uri === $this->target . '\\' || $uri === '/' || $uri === '\\') {
                // Если это первый запуск (запрошенная, главная ссылка)

                // Создание ссылки
                $links[$rawLink][0] = '/index.html';
                $links[$rawLink][1] = '';
                $links[$rawLink][2] = '/index.html';
            } else if (preg_match('/\\.css/i', $uri)) {
                // Если это CSS файл

                // Получение последнего каталога (имени файла с расширением), например: '/index.html'
                if (preg_match_all('/[^\/\\\\\s]+$/', $uri, $file)) {
                    // Создание ссылки
                    $links[$rawLink][0] = '/' . $file[0][0];
                    $links[$rawLink][1] = $location . Yii::$app->params['cssPath'];
                    $links[$rawLink][2] = $links[$rawLink][1] . $links[$rawLink][0];
                }

                // Обновление статистики
                $this->statistics[4][0]++;
                $this->statistics[4][5]++;
            } else if (preg_match('/\\.js/i', $uri)) {
                // Если это JS файл

                // Получение последнего каталога (имени файла с расширением), например: '/index.html'
                if (preg_match_all('/[^\/\\\\\s]+$/', $uri, $file)) {
                    // Создание ссылки
                    $links[$rawLink][0] = '/' . $file[0][0];
                    $links[$rawLink][1] = $location . Yii::$app->params['jsPath'];
                    $links[$rawLink][2] = $links[$rawLink][1] . $links[$rawLink][0];
                }

                // Обновление статистики
                $this->statistics[4][0]++;
                $this->statistics[4][6]++;
            } else if (preg_match('/(\\.png|\\.jpeg|\\.jpg|\\.webp|\\.gif|\\.svg|\\.ico)/i', $uri)) {
                // Если это изображение

                // Получение последнего каталога (имени файла с расширением), например: '/index.html'
                if (preg_match_all('/[^\/\\\\\s]+$/', $uri, $file)) {
                    // Создание ссылки
                    $links[$rawLink][0] = '/' . $file[0][0];
                    $links[$rawLink][1] = $location . Yii::$app->params['imgPath'];
                    $links[$rawLink][2] = $links[$rawLink][1] . $links[$rawLink][0];
                }

                // Обновление статистики
                $this->statistics[4][0]++;
                $this->statistics[4][2]++;
            } else if (preg_match('/(https?:(\/\/|\\\\).+(\/|\\\).+|^(\/|\\\).+)(\.(?!php|htm)[A-z0-9]+)[^\/\\\s]*$/i', $uri)) {
                // Если это неопознанный документ (очень затратное выражение, но по другому никак)

                // Получение последнего каталога (имени файла с расширением), например: '/index.html'
                if (preg_match_all('/[^\/\\\\\s]+$/', $uri, $file)) {
                    // Создание ссылки
                    $links[$rawLink][0] = '/' . $file[0][0];
                    $links[$rawLink][1] = $location . Yii::$app->params['docsPath'];
                    $links[$rawLink][2] = $links[$rawLink][1] . $links[$rawLink][0];
                }

                // Обновление статистики
                $this->statistics[4][0]++;
                $this->statistics[4][7]++;
            } else if (preg_match_all('/(\/\/|\\\\)(.*)((\/|\\|$).*$)/U', $uri, $uriMatch)) {
                // Иначе, если это обрабатывается универсально или как HTML документ

                if (isset($uriMatch[3][0])) {
                     // Если есть путь к файлу, например 'https://domain.zone/это/обязательно/index.html'
                    if (preg_match_all('/^([^\/\\\\\s\.]*[^\\s\.]+)([^\/\\\]*\.html|[^\/\\\]*\.php|[^\/\\\]*\.htm)?$/U', $uriMatch[3][0], $uriSplit)) {
                        // Если в URI не найден URN (файл с расширением, например: 'index.php')

                        if (empty($uriSplit[2][0])) {
                            $uriSplit[2][0] = '/index.html';
                        }

                        // Создание ссылки
                        $links[$rawLink][0] = $uriSplit[2][0];
                        $links[$rawLink][1] = $location . Yii::$app->params['pagesPath'] . $uriSplit[1][0];
                        $links[$rawLink][2] = $links[$rawLink][1] . $links[$rawLink][0];
                    }
                } else {
                    // Иначе обрабатывается как пустая ссылка, например 'https://domain.zone'

                    // Создание ссылки
                    $links[$rawLink][0] = '/index.html';
                    $links[$rawLink][1] = $location . Yii::$app->params['pagesPath'] . '/' . $uriMatch[1][0];
                    $links[$rawLink][2] = $links[$rawLink][1] . '/index.html';
                }

                // Прибавление к количеству найденных страниц
                $this->statistics[3][0]++;
            }

            // Удаление обработанной ссылки и оставшихся переменных
            unset($links[$this->linksNew], $rawLink, $location, $uriSplit, $match, $file, $uri);

            // Прибавление к количеству обработанных ссылок
            $this->statistics[2][1]++;

            // Подготовка к следующей итерации цикла
            $this->linksNew--;
        }

        // Количество обработанных ссылок без дубликатов
        $this->statistics[2][2] = count($this->links);

        return $links;
    }

    /**
     * Конвертер страниц
     * 
     * Преобразует ссылки в тексте (HTML документе)
     * Возвращает массив не найденных файлов
     * 
     * @param array $files Файлы для конвертации
     * 
     * @return array
     */
    private function convertFiles($files = null)
    {
        if (empty($files)) {
            $files = &$this->files;
        }

        foreach ($files as $link => &$file) {
            if ($file[0] === 0 && isset($this->links[$link]) && file_exists(Yii::$app->params['basePath'] . $this->links[$link][1] . $this->links[$link][0]) && $content = file_get_contents(Yii::$app->params['basePath'] . $this->links[$link][1] . $this->links[$link][0])) {
                // Если метаданные файла указывают, что он является HTML документом

                if (preg_match_all('/(href|src)\\s?=\\s*[\"\']?((?!(\"|\'))(?!tel)(?!mailto)[^\"\']+)[\"\']/i', $content, $match)) {
                    // Если найдены ссылки

                    // Конвертация
                    foreach ($match[2] as $rawLink) {
                        if (!$rawLink = $this->filterLink($rawLink)) {
                            // Если ссылка не прошла фильтрацию
                            continue;
                        }

                        if (!array_key_exists($rawLink, $this->links)) {
                            continue;
                        }

                        $content = preg_replace('/(\"|\')' . preg_quote($rawLink, '/') . '(\"|\')/', '".' . $this->links[$rawLink][2] . '"', $content);
                    }

                    // Инъекция тега <base> в страницу, чтобы работали относительные пути
                    if (preg_match_all('/(.*<head>)(.*)/si', $content, $contentMatch)) {
                        // Если удалось найти <head> в странице

                        // Определяем вложенность страницы
                        if (preg_match_all('/([^\\\|\/|\\s]+)/', $this->links[$link][1], $urlMatch)) {
                            $catalogsDepth = count($urlMatch[1]);
                        }
                        
                        $content = $contentMatch[1][0] . "\n<base href=\"" . str_repeat('../', $catalogsDepth ?? 0) . '">'. $contentMatch[2][0];
                    }

                    // Сохранение файла
                    if (file_put_contents(Yii::$app->params['basePath'] . $this->links[$link][1] . $this->links[$link][0], $content)) {
                        unset($this->files[$link]);

                        // Прибавление к количеству конвертированных страниц
                        $this->statistics[3][1]++;
                    }
                }
            } else if ($file[0] !== 0) {
                // Если файл не является HTML документом

                unset($this->files[$link]);
            } else {
                // Иначе воспринимается как не HTML документ, который не требует конвертацию

                continue;
            }
        }
        unset($link, $file); // Очистка на всякий случай, так как переменные остаются

        return $files;
    }

    /**
     * Фильтрация ссылки
     * 
     * Перед инициализацией ссылка проверяется фильтрами
     * 
     * @return ?string
     */
    private function filterLink($link)
    {
        // Проверка существования URL в чёрном списке
        if (!empty(Yii::$app->params['regBlackList']) && preg_match('/' . Yii::$app->params['regBlackList'] . '/', $link)) {
            return null;
        }

        // Преобразование слешей в Unix стиль, унификация
        $link = preg_replace('/\\\/', '/', $link);

        return $link;
    }

    /**
     * Инициализация ссылки
     * 
     * Подготовка к конвертации
     * 
     * @return string
     */
    private function initLink($link)
    {
        // Подготовка ссылок перед обработкой
        // Разбиение ссылки на каталоги: 'https://domain.zone/foo/bar/index.html' на 'https:', 'domain.zone', 'foo', 'bar', 'index.html'
        if (preg_match_all('/([^\\\|\/|\\s]+)/', $link, $uriMatch)) {
            // Определение того, что URI является полноценным и имеет протокол подключения, например: 'https:', 'ssh:', 'mail:'
            if (preg_match('/^.*:$/', $uriMatch[0][0])) {
                $uri = $uriMatch[0][0] . '//' . $uriMatch[0][1];
                $uriMatch[0] = array_slice($uriMatch[0], 2);
            } else {
                $uri = $this->connectionProtocol . ':' . '//' . $this->connectionHost;
            }

            // Замена всех фрагментов URI от символов, которые Windows не даёт записывать в именах файлов и каталогов на '@'
            // На данный момент достаточно заменять все символы на один, так как обратная конвертация не потребуется, а шанс конфликта имён минимален
            foreach ($uriMatch[0] as &$piece) {
                $piece = preg_replace('/(\\\|\/|\:|\*|\?|\"|\<|\>|\|)/', '@', $piece);
            }
            unset($piece);

            // Сборка новой ссылки из фрагментов оригинальной
            foreach ($uriMatch[0] as $piece) {
                $uri .= '/' . $piece;
            }
            unset($piece);

        } else if ($link === '/' || $link === '\\') {
            // Иначе, если ссылка ведёт на главную страницу сайта

            if (!$this->searchExternal) {
                $uri = $this->connectionProtocol . ':' . '//' . $this->connectionHost;
            }

        } else {
            // Иначе всё обрабатывается как ссылка на текущего хоста

            if (!preg_match('/^(\/|\\\)/', $link)) {
                $link = '/' . $link;
            }
            $link = preg_replace('/(\/|\\\)$/', '', $link);

            $uri = $this->connectionProtocol . ':' . '//' . $this->connectionHost . $link;
        }
        unset($link, $uriMatch, $site);

        return $uri;
    }
}