From e6fedb53001a23f68f698d5be5eae089d55ab1d5 Mon Sep 17 00:00:00 2001 From: Karel Wintersky Date: Tue, 9 Aug 2022 23:36:48 +0300 Subject: [PATCH] 1.0.99 * [+] Pre Curl Release * [*] Always use curl (curl/curl package) * [-] removed LongreadsHelper class * [*] `getStoredAll()` always return array * [*] `getStoredByID()` always return array * [*] `fetchPagesList()` have optional argument: projects list * [*] `fetchPagesList()` return list of available pages * [*] `getPageFullExport()` always return stdClass like structure: longread or LongreadError instance --- composer.json | 6 +- interfaces/LongreadsHelperInterface.php | 12 -- interfaces/LongreadsInterface.php | 25 ++- sources/LongreadError.php | 31 +++ sources/Longreads.php | 265 ++++++++++++++++++------ sources/LongreadsHelper.php | 131 ------------ 6 files changed, 252 insertions(+), 218 deletions(-) delete mode 100644 interfaces/LongreadsHelperInterface.php create mode 100644 sources/LongreadError.php delete mode 100644 sources/LongreadsHelper.php diff --git a/composer.json b/composer.json index 6d7fd7f..7e67850 100644 --- a/composer.json +++ b/composer.json @@ -18,10 +18,8 @@ "ext-pdo": "*", "ext-json": "*", "ext-curl": "*", - "psr/log": "^1.1" - }, - "suggest": { - "curl/curl": "^2.3" + "psr/log": "^1.1", + "curl/curl": "^2.3.0" }, "autoload": { "psr-4": { diff --git a/interfaces/LongreadsHelperInterface.php b/interfaces/LongreadsHelperInterface.php deleted file mode 100644 index eb72757..0000000 --- a/interfaces/LongreadsHelperInterface.php +++ /dev/null @@ -1,12 +0,0 @@ -error_message = $error_message; + $this->error_code = $error_code; + $this->url = $url; + } +} \ No newline at end of file diff --git a/sources/Longreads.php b/sources/Longreads.php index c7b4c3c..8ccfa5d 100644 --- a/sources/Longreads.php +++ b/sources/Longreads.php @@ -6,12 +6,12 @@ namespace AJUR\FSNews; +use Curl\Curl; use PDOException; use Psr\Log\NullLogger; use RuntimeException; use PDO; use Psr\Log\LoggerInterface; -use stdClass; class Longreads implements LongreadsInterface { @@ -96,6 +96,11 @@ class Longreads implements LongreadsInterface */ private $debug_write_raw_html; + /** + * @var bool + */ + private bool $throw_on_error; + public function __construct(PDO $pdo, array $options = [], $logger = null) { $this->api_request_types = [ @@ -124,6 +129,8 @@ public function __construct(PDO $pdo, array $options = [], $logger = null) $this->option_localize_media = (bool)($options['options.option_localize_media'] ?? true); $this->option_download_client = $options['options.download_client'] ?? 'native'; + $this->throw_on_error = (bool)($options['throw.on.error'] ?? false); + $this->debug_write_raw_html = (bool)($options['debug.write_raw_html'] ?? false); $this->sql_table = $options['sql.table'] ?? 'longreads'; @@ -166,13 +173,15 @@ public function getStoredAll(string $order_status = 'DESC', string $order_date = $sth = $this->pdo->query($sql); - return $sth->fetchAll(); + $dataset = $sth->fetchAll(); + + return $dataset ?: []; } - public function getStoredByID($id = null) + public function getStoredByID($id = null):array { if ($id <= 0) { - return false; + return []; } $sql = "SELECT * FROM {$this->sql_table} WHERE id = :id "; @@ -182,10 +191,12 @@ public function getStoredByID($id = null) 'id' => $id ]); - return $sth->fetch(); + $dataset = $sth->fetch(); + + return $dataset ?: []; } - public function import($id, $folder = null, string $import_mode = 'update') + public function import($id, string $folder = '', string $import_mode = 'update') { $import_mode = in_array($import_mode, [ 'insert', 'update' ]) ? $import_mode : 'insert'; @@ -195,7 +206,7 @@ public function import($id, $folder = null, string $import_mode = 'update') if ($import_mode == 'update') { $this->logger->debug('Запрошено обновление лонгрида', [ $id, $folder ]); } else { - $this->logger->debug('Запрошен импорт лонгрида'); + $this->logger->debug('Запрошен импорт лонгрида', [ $id ]); } try { @@ -207,7 +218,7 @@ public function import($id, $folder = null, string $import_mode = 'update') throw new RuntimeException('Не передан ID импортируемой страницы'); } - if (is_null($folder)) { + if (empty($folder)) { throw new RuntimeException('Не передана папка сохранения лонгрида'); } @@ -217,7 +228,7 @@ public function import($id, $folder = null, string $import_mode = 'update') $this->logger->debug("ID: {$id}, папка сохранения: `{$folder}`, режим импорта: `{$import_mode}`"); - $path_store = $this->path_storage . DIRECTORY_SEPARATOR . $folder; + $path_store = rtrim($this->path_storage, DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR . ltrim($folder, DIRECTORY_SEPARATOR); $this->logger->debug("Путь для сохранения лонгрида", [ $path_store ]); @@ -301,6 +312,10 @@ public function import($id, $folder = null, string $import_mode = 'update') $footer = file_get_contents($this->path_to_footer_template); + if (false === $footer) { + throw new RuntimeException("Ошибка чтения файла шаблона {$this->path_to_footer_template}"); + } + $html .= str_replace('{$smarty.now|date_format:"%Y"}', date('Y'), $footer) . "\n"; } @@ -345,7 +360,7 @@ public function import($id, $folder = null, string $import_mode = 'update') 'folder' => $folder ]; - $sql = LongreadsHelper::makeUpdateQuery($this->sql_table, $dataset, "`id` = {$id}"); + $sql = self::makeUpdateQuery($this->sql_table, $dataset, "`id` = {$id}"); } else { @@ -368,7 +383,7 @@ public function import($id, $folder = null, string $import_mode = 'update') 'filename' => $page->filename ]; - $sql = LongreadsHelper::makeInsertQuery($this->sql_table, $dataset); + $sql = self::makeInsertQuery($this->sql_table, $dataset); } $this->logger->debug('PDO SQL Query: ', [ str_replace("\r\n", "", $sql) ]); @@ -386,7 +401,7 @@ public function import($id, $folder = null, string $import_mode = 'update') // очищаем папку от файлов // удаляем папку if ($is_directory_created) { - LongreadsHelper::rmdir($path_store); + self::rmdir($path_store); } $this->logger->debug("Возникла ошибка при импорте лонгрида: ", [ $e->getMessage() ]); @@ -436,11 +451,11 @@ public function add($page = null) if ($count > 0) { $state = 'update'; $this->logger->debug('Обновляем информацию о лонгриде в БД', [ $dataset['id'] ]); - $sql = LongreadsHelper::makeReplaceQuery($this->sql_table, $dataset); + $sql = self::makeReplaceQuery($this->sql_table, $dataset); } else { $state = 'ok'; $this->logger->debug('Добавляем информацию о лонгриде в БД', [ $dataset['id'] ]); - $sql = LongreadsHelper::makeInsertQuery($this->sql_table, $dataset); + $sql = self::makeInsertQuery($this->sql_table, $dataset); } $this->logger->debug('PDO SQL Query: ', [ str_replace("\r\n", "", $sql) ]); @@ -487,7 +502,7 @@ public function deleteStored($id) throw new RuntimeException("Лонгрид с указанным идентификатором не найден в базе данных"); } - $lr_folder = $this->path_storage . DIRECTORY_SEPARATOR . $longread['folder'] . DIRECTORY_SEPARATOR; + $lr_folder = rtrim($this->path_storage, DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR . trim($longread['folder'], DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR; if (!is_dir($lr_folder)) { throw new RuntimeException('Директории с лонгридом не существует. Обратитесь к администратору!'); @@ -530,7 +545,7 @@ public function deleteStored($id) return 'ok'; } // delete() - public function itemToggleVisibility($id, $new_state = 'hide') + public function itemToggleVisibility($id, string $new_state = 'hide') { try { if (is_null($id) || $id <= 0) { @@ -570,20 +585,19 @@ public function itemToggleVisibility($id, $new_state = 'hide') * При преобразовании в JSON требуются опции * `JSON_UNESCAPED_UNICODE | JSON_INVALID_UTF8_SUBSTITUTE` * - * @todo: rename - * * @return array JSON Decoded array */ - public function fetchPagesList():array + public function fetchPagesList($projects = []):array { - $request = 'getpageslist'; $pages_list = [ "status" => "FOUND", "count" => 0, "result" => [] ]; - foreach ($this->tilda_projects_list as $pid) { + $projects_list = !empty($projects) ? $projects : $this->tilda_projects_list; + + foreach ($projects_list as $pid) { $pid_loaded_count = 0; $http_request_query = [ @@ -594,16 +608,27 @@ public function fetchPagesList():array $req_url = $this->makeApiURI('getpageslist', $http_request_query); $this->logger->debug("Запрашиваем список лонгридов в проекте ", [ $pid ]); - $this->logger->debug("URL запроса: ", [ $req_url ]); - $response = json_decode(file_get_contents($req_url)); + $curl = new Curl(); + $curl->get($req_url); + + $response = json_decode($curl->response); // JSON_UNESCAPED_UNICODE | JSON_INVALID_UTF8_SUBSTITUTE (работает для ассоциативного массива) + + $curl->close(); + + if ($curl->error) { + $this->logger->debug("[Tilda.API] Ошибка получения данных", [ $response->message, $curl->error_message, $curl->error_code ]); + // throw new RuntimeException("[Tilda.API] Ошибка получения данных для проекта {$pid} : " . $response->message); + } $this->logger->debug('Статус ответа: ', [ $response->status ]); + $pages_list['pages'][ $pid ] = []; if ($response->status === "FOUND") { foreach ($response->result as $page_info) { $pages_list['result'][] = $page_info; + $pages_list['pages'][ $pid ][] = $page_info->id; $pid_loaded_count++; } } @@ -616,7 +641,7 @@ public function fetchPagesList():array $this->logger->debug("Всего получено информации о лонгридах: ", [ $pages_list['count'] ]); if ($pages_list['count'] == 0) { - $pages_list['status'] = "ERROR"; + $pages_list['status'] = "NOT FOUND"; } return $pages_list; @@ -634,46 +659,45 @@ public function getPageFullExport($id, $associative = null) $this->logger->debug('[getPageFullExport] URL запроса к тильде:', [ $url ]); - try { - $response = file_get_contents($url); + $curl = new Curl(); - if (false === $response) { - throw new RuntimeException( "[getPageFullExport] ERROR: Не удалось получить данные с Tilda API" ); - } + try { + $curl->get($url); + $response = $curl->response; $response = json_decode($response, $associative); if (false === $response) { throw new RuntimeException( "[getPageFullExport] ERROR: Не удалось json-декодировать данные с Tilda API" ); } + if ($curl->error) { + throw new RuntimeException( "[getPageFullExport] ERROR: Не удалось получить данные с Tilda API: " . ($associative ? $response['message'] : $response->message) ); + } + + $curl->close(); + return $response; } catch (RuntimeException $e) { $this->logger->debug($e->getMessage(), [ $e->getCode(), $url ]); - return $associative ? [] : new stdClass(); + return $associative ? [] : new LongreadError($e->getMessage(), $curl->error_code, $url); } } + /* =============================================================================== */ /* ================================== PRIVATE METHODS ============================ */ - - private function downloadFile($from, $to) - { - if ($this->option_download_client === 'curl' && class_exists('\Curl\Curl')) { - return $this->downloadFileCurl($from, $to); - } else { - return $this->downloadFileNative($from, $to); - } - } + /* =============================================================================== */ /** - * @param $from - * @param $to + * Скачивает CURL-ом файл с URL по указанному пути + * + * @param string $from + * @param string $to * @return bool - * @throws RuntimeException */ - private function downloadFileCurl($from, $to): bool + private function downloadFile(string $from, string $to): bool { $file_handle = fopen($to, 'w+'); @@ -681,7 +705,7 @@ private function downloadFileCurl($from, $to): bool throw new RuntimeException("Ошибка создания файла `{$to}`"); } - $curl = new \Curl\Curl(); + $curl = new Curl(); $curl->setOpt(CURLOPT_FILE, $file_handle); $curl->get($from); @@ -693,45 +717,164 @@ private function downloadFileCurl($from, $to): bool throw new RuntimeException("Ошибка скачивания файла {$from} " . $curl->error_message); } + $curl->close(); + return !($curl->error); } /** - * @param $from - * @param $to + * Создает URI для API-запроса + * + * @param string $command + * @param array $http_request_query + * @param bool $is_https + * @return string + */ + private function makeApiURI(string $command, array $http_request_query, bool $is_https = false): string + { + $scheme = $is_https ? 'https://' : 'http://'; + + return empty($http_request_query) + ? "{$scheme}api.tildacdn.info/{$this->api_options['version']}/{$command}/" + : "{$scheme}api.tildacdn.info/{$this->api_options['version']}/{$command}/?" . http_build_query( $http_request_query ); + } + + /** + * Recursive rmdir + * + * @param $directory * @return bool - * @throws RuntimeException */ - private function downloadFileNative($from, $to) + private static function rmdir($directory): bool + { + if (!\is_dir( $directory )) { + return false; + } + + $files = \array_diff( \scandir( $directory ), [ '.', '..' ] ); + + foreach ($files as $file) { + $target = "{$directory}/{$file}"; + (\is_dir( $target )) + ? self::rmdir( $target ) + : \unlink( $target ); + } + return \rmdir( $directory ); + } + + /** + * Строит запрос REPLACE SET ... + * + * @param string $table + * @param array $dataset + * @param string $where + * @return false|string + */ + private static function makeReplaceQuery(string $table, array &$dataset, string $where = '') { - $content = file_get_contents($from); + $fields = []; - if (!$content) { - throw new RuntimeException("Ошибка получения файла {$from}"); + if (empty($dataset)) { + return false; } - if (!file_put_contents($to, $content)) { - throw new RuntimeException("Ошибка сохранения файла `{$to}`"); + $query = "REPLACE `{$table}` SET "; + + foreach ($dataset as $index => $value) { + if (\strtoupper(\trim($value)) === 'NOW()') { + $fields[] = "`{$index}` = NOW()"; + unset($dataset[ $index ]); + continue; + } + + $fields[] = "`{$index}` = :{$index}"; } - return true; + $query .= \implode(', ', $fields); + + $query .= " {$where}; "; + + return $query; } /** - * Создает URI для API-запроса + * Строит запрос INSERT INTO table * - * @param string $command - * @param array $http_request_query + * @param string $table + * @param $dataset * @return string */ - private function makeApiURI(string $command, array $http_request_query) + private static function makeInsertQuery(string $table, &$dataset):string { - return empty($http_request_query) - ? "http://api.tildacdn.info/{$this->api_options['version']}/{$command}/" - : "http://api.tildacdn.info/{$this->api_options['version']}/{$command}/?" . http_build_query( $http_request_query ); + if (empty($dataset)) { + return "INSERT INTO {$table} () VALUES (); "; + } + + $set = []; + + $query = "INSERT INTO `{$table}` SET "; + + foreach ($dataset as $index => $value) { + if (\strtoupper(\trim($value)) === 'NOW()') { + $set[] = "`{$index}` = NOW()"; + unset($dataset[ $index ]); + continue; + } + + $set[] = "`{$index}` = :{$index}"; + } + + $query .= \implode(', ', $set) . ' ;'; + + return $query; } + /** + * Строит запрос UPDATE table SET + * + * @param string $table + * @param $dataset + * @param $where_condition + * @return string + */ + private static function makeUpdateQuery(string $table, &$dataset, $where_condition):string + { + $set = []; + + if (empty($dataset)) { + return false; + } + + $query = "UPDATE `{$table}` SET"; + + foreach ($dataset as $index => $value) { + if (\strtoupper(\trim($value)) === 'NOW()') { + $set[] = "`{$index}` = NOW()"; + unset($dataset[ $index ]); + continue; + } + + $set[] = "`{$index}` = :{$index}"; + } + + $query .= \implode(', ', $set); + + if (\is_array($where_condition)) { + $where_condition = \key($where_condition) . ' = ' . \current($where_condition); + } + if ( \is_string($where_condition ) && !\strpos($where_condition, 'WHERE')) { + $where_condition = " WHERE {$where_condition}"; + } + + if (\is_null($where_condition)) { + $where_condition = ''; + } + + $query .= " $where_condition ;"; + + return $query; + } } diff --git a/sources/LongreadsHelper.php b/sources/LongreadsHelper.php deleted file mode 100644 index 44e6b34..0000000 --- a/sources/LongreadsHelper.php +++ /dev/null @@ -1,131 +0,0 @@ - $value) { - if (strtoupper(trim($value)) === 'NOW()') { - $fields[] = "`{$index}` = NOW()"; - unset($dataset[ $index ]); - continue; - } - - $fields[] = "`{$index}` = :{$index}"; - } - - $query .= implode(', ', $fields); - - $query .= "{$where}; "; - - return $query; - } - - /** - * Строит INSERT-запрос на основе массива данных для указанной таблицы. - * В массиве допустима конструкция 'key' => 'NOW()' - * В этом случае она будет добавлена в запрос и удалена из набора данных (он пере). - * - * @param $table -- таблица - * @param $dataset -- передается по ссылке, мутабелен - * @return string -- результирующая строка запроса - */ - public static function makeInsertQuery(string $table, &$dataset):string - { - if (empty($dataset)) { - return "INSERT INTO {$table} () VALUES (); "; - } - - $set = []; - - $query = "INSERT INTO `{$table}` SET "; - - foreach ($dataset as $index => $value) { - if (strtoupper(trim($value)) === 'NOW()') { - $set[] = "`{$index}` = NOW()"; - unset($dataset[ $index ]); - continue; - } - - $set[] = "`{$index}` = :{$index}"; - } - - $query .= implode(', ', $set) . ' ;'; - - return $query; - } - - /** - * Build UPDATE query by dataset for given table - * - * @param $tablename - * @param $dataset - * @param $where_condition - * @return bool|string - */ - public static function makeUpdateQuery(string $table, &$dataset, $where_condition):string - { - $set = []; - - if (empty($dataset)) { - return false; - } - - $query = "UPDATE `{$table}` SET"; - - foreach ($dataset as $index => $value) { - if (strtoupper(trim($value)) === 'NOW()') { - $set[] = "`{$index}` = NOW()"; - unset($dataset[ $index ]); - continue; - } - - $set[] = "`{$index}` = :{$index}"; - } - - $query .= implode(', ', $set); - - if (is_array($where_condition)) { - $where_condition = key($where_condition) . ' = ' . current($where_condition); - } - - if ( is_string($where_condition ) && !strpos($where_condition, 'WHERE')) { - $where_condition = " WHERE {$where_condition}"; - } - - if (is_null($where_condition)) { - $where_condition = ''; - } - - $query .= " $where_condition ;"; - - return $query; - } - -} \ No newline at end of file