<?php
/**
 * -------------------------------------------------------------------------
 *
 * Модуль контроллера "Парсер".
 *
 * -------------------------------------------------------------------------
 *
 * Я сделал этот модуль таким образом, чтобы поддерживал произвольное число
 * дочерних модулей, которые фактически и явяляются парсерами данных с
 * конкретных интернет-платформ. Моему приложению требовалось парсить только
 * с платформы "Труд всем", поэтому в качестве дочернего модуля сюда вложен
 * лишь модуль Trudvsem. Но Вы можете вложить любое количество своих, надо
 * только в каждом случае создать здесь дочернюю папку и одноимённый
 * PHP-файл, где написать алгоритм парсинга вакансий из того источника.
 * Очевидно, Ваш парсер будет работать с иной структурой данных, а значит,
 * чтобы вывести её в браузер или канал мессенджера, к парсеру потребуется
 * приложить такие макеты:
 *
 *     repost.vacancies/Themes/default/snippets/vacancy-card/ИМЯ_ПАРСЕРА.tpl
 *     repost.vacancies/Themes/default/max/vacancy/ИМЯ_ПАРСЕРА.tpl
 *     repost.vacancies/Themes/default/telegram/vacancy/ИМЯ_ПАРСЕРА.tpl
 *
 * -------------------------------------------------------------------------
 *
 * @package    MimimiFramework
 * @subpackage Examples / Repost Vacancies
 * @license    GPL-2.0
 *             https://opensource.org/license/gpl-2-0/
 * @copyright  2022 MiMiMi Community
 *             https://mimimi.software/
 *
 * -------------------------------------------------------------------------
 */

    /**
     * ---------------------------------------------------------------------
     *
     * Подключаем из папки ядра фреймворка файл "mimimi.core/NodeModule.php".
     * Там объявлен класс "MimimiNodeModule", который является простейшей
     * заготовкой для модулей, поддерживающих дочерние, вызываемые через
     * оператор Стрелка. Этот класс подходит как основа для реализуемого
     * ниже модуля.
     *
     * ---------------------------------------------------------------------
     */

    mimimiInclude ( 'NodeModule.php' );

    /**
     * ---------------------------------------------------------------------
     *
     * Создаём на основе класса той заготовки новый класс, в котором напишем
     * программный код текущего модуля. Обратите внимание как задано имя
     * нового класса - оно сложено из имени класса вышестоящего модуля, то
     * есть "MyMimimiControllers", и имени текущего PHP-файла без расширения.
     *
     * ---------------------------------------------------------------------
     */

    class MyMimimiControllersParser extends MimimiNodeModule {

        /**
         * -----------------------------------------------------------------
         *
         * Метод: Маршрутизировать запрос к сопоставленному макету шаблона.
         *
         * -----------------------------------------------------------------
         *
         * Этот метод обслуживает следующие URL-ы:
         *
         *     https://ваш.сайт/parse
         *
         * В результате произойдёт генерация пустой страницы. А если запрос
         * был сделан администратором, пройдёт генерация HTML-кода страницы
         * на основе такого макета:
         *
         *     repost.vacancies/Themes/default/parse.tpl
         *
         * -----------------------------------------------------------------
         *
         * @public
         * @param   string  $url  (необязательный) Относительный URL запрошенной страницы.
         * @return  void
         *
         * -----------------------------------------------------------------
         */

        public function run ( $url = '' ) {
            $report = [ 'Подготовка к запуску.' ];
            $status = $this->checkTimer ( );
            if ( $status === NULL ) {
                $report[ ] = '└─> К сожалению, парсер выключен в настройках сайта!';
            } else if ( $status === FALSE ) {
                $report[ ] = '├─> К сожалению, ещё не наступило время работы парсера!';
                $report[ ] = '│   └─> Оно установлено с '  . $this->app->models->settings->get ( 'day_starts' ) . ':00' .
                                                    ' до ' . $this->app->models->settings->get ( 'day_ends'   ) . ':00';
                $report[ ] = '└─> Сейчас на сайте ' . date ( 'H:i' );
            } else if ( $status === 0 ) {
                $shift = date_create ( ) ->getOffset ( );
                $time  = intval ( $this->app->models->settings->get ( 'last_scan'  ) )
                       + intval ( $this->app->models->settings->get ( 'scan_pause' ) )
                       - time ( )
                       - $shift;
                $report[ ] = '├─> К сожалению, ещё не закончилась пауза между запусками парсера!';
                $report[ ] = '│   └─> Она установлена равной ' . $this->app->models->settings->get ( 'scan_pause' ) . ' секунд.';
                $report[ ] = '└─> Осталось ' . date ( 'H:i:s', $time );
            } else {
                $this->app->models->vacancies->removeOld ( );
                $rows = $this->app->models->tasks->select ( );
                $report[ ] = '└─> Ок! Получаем список активных заданий.';
                $report[ ] = '    └─> Имеется заданий: ' . count ( $rows ) . '.';
                if ( $rows ) {
                    $report[ ] = '        └─> Переходим к последовательному выполнению каждого.';
                    $response = $this->parse ( $rows );
                    $report   = array_merge ( $report, $response );
                }
                $this->app->models->settings->set ( 'last_scan', time ( ) );
            }
            if ( $this->owner->dashboard->hasAdmin ( ) ) {
                $this->owner->dashboard->render ( 'parse.tpl', $report );
                return;
            }
            $this->app->views->html->nothing ( );
        }

        /**
         * -----------------------------------------------------------------
         *
         * Метод: Проверить что время парсинга наступило.
         *
         * -----------------------------------------------------------------
         *
         * @protected
         * @return  null|bool|int  TRUE  если время наступило.
         *                         FALSE если ещё не наступило.
         *                         0     если ещё не истекла обязательная пауза с момента прошлого запуска.
         *                         NULL  если парсер отключен в настройках сайта.
         *
         * -----------------------------------------------------------------
         */

        protected function checkTimer ( ) {
            if ( $this->app->models->settings->get ( 'parser_active' ) ) {
                $now  = time ( );
                $hour = date ( 'G', $now );
                if ( $this->app->models->settings->get ( 'day_starts' ) <= $hour
                &&   $this->app->models->settings->get ( 'day_ends'   ) >  $hour ) {
                    $next = intval ( $this->app->models->settings->get ( 'last_scan'  ) )
                          + intval ( $this->app->models->settings->get ( 'scan_pause' ) );
                    return $now >= $next ? TRUE
                                         : 0;
                }
                return FALSE;
            }
            return NULL;
        }

        /**
         * -----------------------------------------------------------------
         *
         * Метод: Парсить источники согласно списку заданий.
         *
         * -----------------------------------------------------------------
         *
         * @protected
         * @param   array  $tasks  Список записей о заданиях для парсера.
         * @return  array          Список строк, дающих простой отчёт о действиях, совершённых парсером.
         *
         * -----------------------------------------------------------------
         */

        protected function parse ( $tasks ) {
            $num = 1;
            $report = [ ];
            foreach ( $tasks as $row ) {
                $module = $row[ 'module' ];
                $report[ ] = '';
                $report[ ] = 'Задание ' . $num . ': ' . $row[ 'name' ];
                $report[ ] = '└─> Предписано выполнить через модуль ' . $module;
                if ( $this->has->$module ) {
                    $response = $this->$module->run ( $row );
                    $report   = array_merge ( $report, $response );
                } else {
                    $report[ ] = '    └─> Ошибка: В парсере нет такого модуля!';
                }
                $num++;
            }
            return $report;
        }

        /**
         * -----------------------------------------------------------------
         *
         * Метод: Скачать документ в формате JSON.
         *
         * -----------------------------------------------------------------
         *
         * @public
         * @param   string          $url     Абсолютный URL скачиваемого документа.
         * @param   array           $params  (необязательный) Список полей, передаваемых документу как POST-параметры.
         * @return  array|int|null           ARRAY если документ успешно скачан и декодирован как ожидаемый массив.
         *                                   0     если документ скачан, но там не JSON структура.
         *                                   NULL  если не удалось скачать.
         *
         * -----------------------------------------------------------------
         */

        public function downloadJSON ( $url, $params = FALSE ) {
            $handle = @ curl_init ( $url );
            if ( $handle !== FALSE ) {
                if ( is_array ( $params ) ) {
                    @ curl_setopt ( $handle, CURLOPT_POST,       TRUE    );
                    @ curl_setopt ( $handle, CURLOPT_POSTFIELDS, $params );
                }
                $site = preg_replace ( '~^(https?://[^/]+/).*$~ui', '$1', $url );
                $params = FALSE;
                @ curl_setopt ( $handle, CURLOPT_REFERER, $site );
                @ curl_setopt ( $handle, CURLOPT_USERAGENT, 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/142.0.0.0 Safari/537.36' );
                @ curl_setopt ( $handle, CURLOPT_SSL_VERIFYPEER, FALSE );
                @ curl_setopt ( $handle, CURLOPT_SSL_VERIFYHOST, FALSE );
                @ curl_setopt ( $handle, CURLOPT_RETURNTRANSFER, TRUE  );
                @ curl_setopt ( $handle, CURLOPT_CONNECTTIMEOUT, 5     );
                @ curl_setopt ( $handle, CURLOPT_TIMEOUT,        10    );
                $result = @ curl_exec ( $handle );
                if ( $result !== FALSE ) {
                    if ( is_string ( $result ) ) {
                        try {
                            $params = @ json_decode ( $result, TRUE );
                        } catch ( Exception $e ) {}
                        if ( ! is_array ( $params ) ) {
                            $params = 0;
                        }
                    }
                }
                @ curl_close ( $handle );
                return $params;
            }
            return NULL;
        }

        /**
         * -----------------------------------------------------------------
         *
         * Метод: Получить список имён дочерних модулей парсера.
         *
         * -----------------------------------------------------------------
         *
         * @public
         * @return  array  Массив строк (они все в нижнем регистре).
         *
         * -----------------------------------------------------------------
         */

        public function listModules ( ) {
            $list = [ ];
            $dirs = mimimiFolders ( $this->getNodePath ( ) );
            foreach ( $dirs as $name ) {
                $name = preg_replace ( '~[\W_]+~u',        '_',     $name );
                $name = preg_replace ( '~([^_])([A-Z])~u', '$1_$2', $name );
                $name = strtolower   (                              $name );
                if ( $this->has->$name ) {
                    $list[ ] = $name;
                }
            }
            return $list;
        }

        /**
         * -----------------------------------------------------------------
         *
         * Инициализируем симулятор "неймспейса", так как в силу конструкции
         * класса "MimimiNodeModule" такое действие предписано совершать в
         * каждом наследуемом классе, если начиная с его узла тоже требуется
         * поддержать способность обращаться к его дочерним модулям через
         * оператор Стрелка (->).
         *
         * Как Вы можете видеть, в папке этого модуля находится один парсер:
         * Trudvsem, к которому нам придётся обращаться в любом случае.
         * Поэтому мы и выполнили сейчас инициализацию "неймспейса".
         *
         * Под неймспейсом понимается уникализация имён вложенных модульных
         * классов. Посмотрите как назван относительно текущего класса
         * упомянутый класс дочернего модуля.
         *
         * -----------------------------------------------------------------
         *
         * @var    string
         * @access protected
         *
         * -----------------------------------------------------------------
         */

        protected $myNodeFile = __FILE__;
    }
