Пример #1
0
 /**
  * Соединяемся с базой или получаем дескриптор подключения.
  *
  * Запоминаем подключения по $confKey. Полагаем, что в настройках приложения для каждого ключа описано <i>уникальное
  * сочетание</i> базы/пользователя, поскольку идентичный конфиг не имеет смысла.
  *
  * Опционально ('mysql_timezone'): после установки соединения отправляется запрос на установку часового пояса сессии.
  * Подробнее см. в доке "DB.md"
  *
  * Единственный источник исключения PDOException тут - это сам конструктор PDO. В трейсе он будет занимать первую
  * позицию, потом будет модель, вызвавшая этот метод и только потом возможно одна/две позиции по клиентскому коду.
  * Поэтому такая глубина разбора трассировки.
  *
  * @param string $confKey ключ в настройках, по которому хранится массив с кофигурацией подключения к БД
  * @return PDO объект подключения к БД
  * @throws DbException
  */
 public static function connect($confKey)
 {
     if (isset(self::$cons[$confKey])) {
         return self::$cons[$confKey];
     }
     $conf = array_merge(['options' => null, 'mysql_timezone' => '+00:00'], App::conf($confKey));
     try {
         $dbh = new PDO($conf['dsn'], $conf['user'], $conf['password'], $conf['options']);
         if ($tz = $conf['mysql_timezone']) {
             $sql = 'SET time_zone = ?';
             if (false === $dbh->prepare($sql)->execute([$tz])) {
                 throw new DbException('Ошибка установки часового пояса MySQL-сессии.' . PHP_EOL . 'Запрос: ' . str_replace('?', "'{$tz}'", $sql));
             }
         }
         self::$cons[$confKey] = $dbh;
     } catch (\PDOException $e) {
         $msg = $e->getMessage();
         $trace = $e->getTrace();
         if (isset($trace[2])) {
             $msg .= PHP_EOL . 'Инициатор подключения ' . str_replace(ROOT_PATH, '', $trace[2]['file']) . '(' . $trace[2]['line'] . ')';
         }
         if (isset($trace[3])) {
             $msg .= ' функция ' . $trace[3]['function'] . '(...)';
         }
         throw new DbException($msg, DbException::CONNECT, $e);
     }
     return self::$cons[$confKey];
 }
Пример #2
0
 /**
  * Перехватчик исключений.
  *
  * Ловит исключения, которые не были пойманы ранее. Последний шанс обработать ошибку. Например, записать в лог или
  * намылить админу. Можно так же вежливо откланяться юзеру.
  *
  * После выполнения этого обработчика программа остановится, обеспечено PHP.
  *
  * Если указано предыдущее исключение, отсюда не пишем в лог. Сей факт указывает на то, что реальное исключение
  * уже было поймано и обработано. Считаем, что необходимость логирования была решена в предыдущих обработчиках.
  *
  * Прим: для поддержки PHP 7.0 тип ожидаемого параметра расширен,
  * см. {@see http://php.net/manual/ru/function.set-exception-handler.php PHP::set_exception_handler()}
  *
  * @param \Throwable $ex
  */
 public static function exceptionHandler($ex)
 {
     $class = get_class($ex);
     $message = nl2br($ex->getMessage());
     $file = str_replace(ROOT_PATH, '/', $ex->getFile());
     $line = $ex->getLine();
     $trace = $ex->getTraceAsString();
     if (isConsoleInterface()) {
         echo 'Исключение: ' . $class . PHP_EOL . PHP_EOL . $message . PHP_EOL . PHP_EOL . 'Стек вызовов:' . PHP_EOL . $trace . PHP_EOL;
         return;
     }
     if (!headers_sent()) {
         header('500 Internal Server Error');
         header('Content-Type: text/html; charset=UTF-8');
     }
     if (DEBUG) {
         echo Render::fetch('exception.htm', compact('class', 'message', 'file', 'line', 'trace'));
     } else {
         echo Render::fetch('exception_prod.htm', ['domain' => Env::domainName()]);
         if ($ex->getPrevious() === null) {
             $logger = App::logger();
             $logger->addTyped("Class: {$class}" . PHP_EOL . "Message: {$message}" . PHP_EOL . "Source: {$file}:{$line}" . PHP_EOL . PHP_EOL . "Trace: {$trace}", $logger::EXCEPTION);
         }
     }
 }
Пример #3
0
 /**
  * Конструктор
  *
  * Логируем ошибку. В зависимости от кода пишем лог в БД или файлы: если исключение проброшено из попытки соединения,
  * сразу же пишем лог в файлы, не пытаясь еще раз ткнуться в базу.
  *
  * @param string    $message
  * @param int       $code
  * @param \Exception $previous
  */
 public function __construct($message, $code = self::QUERY, $previous = null)
 {
     $logger = App::logger();
     if ($code === self::CONNECT) {
         $logger->add(['message' => $message, 'type' => $logger::DB_CONNECT, 'file_force' => true]);
     } else {
         $logger->addTyped($message, $logger::DB_QUERY);
     }
     parent::__construct($message, $code, $previous);
 }
Пример #4
0
 /**
  * Редирект с выходом из приложения.
  *
  * Прим.: указание абсолютного URL - требование спецификации HTTP/1.1,
  * {@link http://php.net/manual/ru/function.header.php}
  *
  * Быстрая справка по кодам с редиректом {@link http://php.net/manual/ru/function.header.php#78470}
  *
  * Хитрый редирект на основе комментария {@link http://php.net/manual/ru/function.headers-sent.php#60450}
  *
  * @param string $url  новый относительный адрес, с ведущим слешем
  * @param int    $code код ответа HTTP
  */
 public static function redirect($url, $code = 302)
 {
     $url = Env::domainUrl() . $url;
     if (!headers_sent()) {
         header('location:' . $url, true, $code);
     } else {
         echo "\n                <script type='text/javascript'>\n                    window.location.href='{$url}'\n                </script>\n                <noscript>\n                    <meta http-equiv='refresh' content='0; url={$url}'/>\n                </noscript>\n            ";
     }
     App::end();
 }
Пример #5
0
 /**
  * Письмо админу с текущим сообщение лога.
  *
  * @return void
  */
 private function mailToAdmin()
 {
     $logIt = $this->logIt;
     if (!($mailTo = $this->conf['_mail'])) {
         $this->addTyped('Не задан email админа, не могу отправить сообщение от логера.', ILogger::ENGINE);
         return;
     }
     $domain = Env::domainName();
     $date = $logIt['ts']->format('Y/m/d H:i:s P');
     $rn = PHP_EOL;
     $letters['text'] = 'Сообщение от логера' . $rn . $rn . $logIt['message'] . $rn . $rn . "Тип: {$logIt['type']}{$rn}" . "Источник: {$logIt['source']}" . $rn . $rn . "URL запроса:{$logIt['request']}" . $rn . "IP юзера: {$logIt['userIP']}" . $rn . $rn . "{$date} (c) {$domain}";
     $vars = ['message' => nl2br($logIt['message']), 'type' => $logIt['type'], 'source' => $logIt['source'], 'request' => $logIt['request'], 'userIP' => $logIt['userIP'], 'date' => $date, 'homeURL' => Env::indexPage(), 'domain' => $domain];
     $letters['html'] = Render::fetch('log_letter.htm', $vars);
     $from = App::conf('noreply_mail') ?: "noreply@{$domain}";
     if (!Mailer::complex($from, $mailTo, "Сообщение от логера сайта {$domain}", $letters)) {
         $this->addTyped('Не удалось отправить сообщение от логера.', ILogger::ENGINE);
     }
 }
Пример #6
0
 /**
  * Получение URL по заданному маршруту
  * @dataProvider route2UrlProvider
  * @param string $expectUrl
  * @param array  $route
  * @param array  $params
  */
 public function test_url($expectUrl, $route, $params = [])
 {
     $url = App::router()->url($route, $params);
     $this->assertEquals($expectUrl, $url);
 }
Пример #7
0
 /**
  * Построение URL по описанию, используя карту роутов.
  *
  * Перебираем роуты в заданном пространстве имен. Сравниваем указанные контроллер/действие с правой частью роута.
  * Если нашли совпадение справа (будет массив), анализируем правило слева, выбирая необходимые подстановки -
  * "required params". Если сможем их обеспечить из массива совпадения и/или переданных параметров - роут найден.
  * Иначе продолжаем поиск.
  *
  * Все параметры, которые не пойдут в подстановки, допишутся в URL в качестве строки запроса. Если у элемента
  * не задано значение, а есть только ключ, типа ['p' => null], он попадет в URL без значения.
  *
  * Прим: поиск роута не зависит от регистра. Это немного упрощает требования к описанию параметров функции.
  *
  * Если ничего не найдем, проброс ошибки. Вообще ситуация некритичная, но иначе можно прозевать исчезновение роута
  * и получить битую ссылку.
  *
  * @param mixed $route  <b>неассоциативный</b> массив 2-х элементов ["пространство имен", "контроллер/действие"]
  * @param array $params доп.параметры для передачи в адрес. Ассоциативный массив ['имя параметра' => 'значение']
  * @return string готовый <b>относительный</b> URL
  * @throws RouteException
  */
 public function url($route, array $params = [])
 {
     list($ns, $ctrlAct) = $route;
     $ctrlAct = trim($ctrlAct, '/');
     $routes = App::conf("router.routes.{$ns}", false);
     if (!$routes) {
         throw new RouteException("Нет карты роутов для пространства имен '{$ns}'");
     }
     $match = null;
     $paramsBackup = $params;
     foreach ($routes as $left => $right) {
         $left = ltrim($left, '/');
         $right = ltrim($right, '/');
         $pattern = preg_replace('~(<(.+)>)~U', '(?P<$2>[^/]+)', $right);
         $pattern = "~^{$pattern}\$~Ui";
         if (preg_match($pattern, $ctrlAct, $matchCtrlAct)) {
             if (preg_match_all('~<([a-z0-9_]+):.+>~U', $left, $requiredParams)) {
                 $requiredParams = array_flip($requiredParams[1]);
                 foreach ($requiredParams as $k => &$v) {
                     if (isset($matchCtrlAct[$k])) {
                         $v = $matchCtrlAct[$k];
                     } else {
                         if (isset($params[$k])) {
                             $v = $params[$k];
                             // удаляем из параметров то, что будет использовано в подстановке
                             unset($params[$k]);
                         } else {
                             $params = $paramsBackup;
                             continue 2;
                         }
                     }
                 }
                 unset($v);
             } else {
                 $requiredParams = [];
             }
             break;
         }
     }
     //dd($left, $match);//DBG
     if ($matchCtrlAct) {
         if ($requiredParams) {
             $placeholders = [];
             foreach ($requiredParams as $key => $v) {
                 $placeholders[] = "~(<{$key}:.+>)~U";
             }
             $left = preg_replace($placeholders, $requiredParams, $left);
         }
         if (preg_match('~[^/a-z0-9_-]~i', $left)) {
             $arr = explode('/', $left);
             $arr = array_map('urlencode', $arr);
             $left = implode('/', $arr);
         }
         $url = '/' . $left;
         if ($params) {
             foreach ($params as $k => &$v) {
                 $v = urlencode($k) . ($v ? '=' . urlencode($v) : '');
             }
             $url .= '?' . implode('&', $params);
         }
         return $url;
     } else {
         $strParams = [];
         foreach ($params as $k => $v) {
             $strParams[] = "{$k} => {$v}";
         }
         $strParams = count($strParams) ? 'параметры [' . implode(', ', $strParams) . ']' : 'без параметров';
         throw new RouteException("не могу построить URL по заданным значениям: ['{$ns}', '{$ctrlAct}'], {$strParams}");
     }
 }
Пример #8
0
 /**
  * Имя домена, типа "my.site.com".
  *
  * Пытаемся его получить из $_SERVER['HTTP_HOST']. Иначе ищем в настройках приложения ('domain').
  *
  * @return string
  */
 public static function domainName()
 {
     return isset($_SERVER['HTTP_HOST']) ? $_SERVER['HTTP_HOST'] : (App::conf('domain', false) ?: '');
 }
Пример #9
0
 /**
  * Типовой валидатор "bounds".
  *
  * Проверка рационального числа в заданных пределах. Полезно для проверки чисел с плавающей точкой, в дополнение
  * к filter_var(FILTER_VALIDATE_FLOAT). Описание:
  * <pre>
  * $desc = [
  *     'min'     => number,
  *     'max'     => number,
  *     'message' => string, // свое сообщение об ошибке. Необязательно.
  * ];
  * </pre>
  *
  * Любой из параметров можно пропустить. Значение 'NULL' - не проверять границу с этой стороны.
  *
  * Проверяемое значение может быть равно NULL, только если до этого были ошибки в других валидаторах. При этом
  * выходим без дополнительных сообщений.
  *
  * @param array  $desc  описание валидатора
  * @param number $data  проверяемые данные
  * @param mixed  $value куда писать валидное значение
  * @param mixed  $error куда писать ошибку
  * @return bool
  * @throws \LogicException
  */
 protected function validatorBounds(&$desc, &$data, &$value, &$error)
 {
     if (is_null($data)) {
         $this->isValid = false;
         return false;
     }
     // Предохранитель для разработчика
     if (!is_numeric($data)) {
         throw new \LogicException('Проверка границ числа не применима к такому типу данных: ' . gettype($value));
     }
     $min = isset($desc['min']) ? (double) $desc['min'] : null;
     $max = isset($desc['max']) ? (double) $desc['max'] : null;
     $message = isset($desc['message']) ? App::t($desc['message']) : '';
     $data *= 1;
     // приведение к типу
     if (!is_null($min) && $data < $min) {
         $errMsg[] = $message ?: App::t('Значение меньше допустимого, минимум M', ['M' => $min]);
     } elseif (!is_null($min) && $data > $max) {
         $errMsg[] = $message ?: App::t('Значение больше допустимого, максимум M', ['M' => $max]);
     } else {
         $errMsg = null;
     }
     if (!($passed = !$errMsg)) {
         $this->isValid = false;
         $error = is_array($error) ? array_merge($error, $errMsg) : $errMsg;
     }
     return $passed;
 }
Пример #10
0
<?php

/**
 * Главный скрипт приложения 'app'
 */
use kira\core\App;
mb_internal_encoding('UTF-8');
define('APP_NAMESPACE', 'app');
define('ROOT_PATH', str_replace('\\', '/', rtrim(__DIR__, '/')) . '/');
define('APP_PATH', ROOT_PATH . 'application/');
define('VIEWS_PATH', APP_PATH . 'views/');
define('TEMP_PATH', APP_PATH . 'temp/');
define('MAIN_CONFIG', APP_PATH . 'conf/main.php');
define('DEBUG', true);
ini_set('display_errors', (int) DEBUG);
ini_set('display_startup_errors', (int) DEBUG);
error_reporting(DEBUG ? E_ALL : 0);
$composer = (require ROOT_PATH . 'vendor/autoload.php');
App::setComposer($composer);
unset($composer);
date_default_timezone_set(App::conf('timezone'));
App::router()->callAction();
Пример #11
0
 /**
  * Проверка права запускать скрипты через Консоль
  *
  * В режиме отладки - разрешено без вопросов, иначе - при указании правильного ключа. Количество ошибок считается.
  * Ключ и допустимое количество прописываем в конфиге приложения:
  * <pre>
  * 'console => [
  *      'ties' => int|0
  *      'key' => string|''
  * ]
  * </pre>
  *
  * <b>Внимание!</b> При отсутствии этого конфига, а так же со значениями по умолчанию, доступ будет разрешен.
  *
  * В дальнейшем можно добавить контроль на запрещенные/разрешенные IP и т.п.
  *
  * Неудавшиеся попытки логируются, после NN попыток консоль вообще блокируется через файл [TEMP_PATH/console.lock],
  * при блокировке админу пойдет письмо.
  *
  * @param string $key ключ доступа, спарсенный из запроса
  * @return null если нет права запуска, прямо тут все и закончится. Поэтому возвращаемое значение не важно.
  */
 private function checkAccess(string $key)
 {
     if (DEBUG) {
         return;
     }
     $lockFile = TEMP_PATH . 'convisor.lock';
     $count = file_exists($lockFile) ? file_get_contents($lockFile) : 0;
     $tries = (int) App::conf('convisor.tries', false);
     $critical = $tries && $count >= $tries;
     if ($critical) {
         exit('Консоль заблокирована' . PHP_EOL);
     }
     $allow = (string) App::conf('convisor.key', false) == $key;
     if (!$allow) {
         $count++;
         $msg = "{$count} неудачная попытка запуска скрипта в консоли: {$this->script} " . implode(' ', $this->params);
         if ($critical) {
             App::logger()->add(['message' => $msg, 'type' => 'deny console', 'notify' => true]);
         } else {
             App::logger()->addTyped($msg, 'deny console');
         }
         file_put_contents($lockFile, $count);
         exit('Запуск запрещен' . PHP_EOL);
     }
     FS::deleteFile($lockFile);
 }
Пример #12
0
 /**
  * Валидатор даты
  *
  * Проверяется соответствие формату и реальность даты. Решил не мудрить с настройками. Валидатор разрешает только
  * формат "dd.mm.yyyy"
  *
  * Возвращаемое значение приводится в формат MySQL "yyyy-mm-dd" для корректного сохранения в БД.
  *
  * @param string $date
  * @return array возвращаем в формате yyyy-mm-dd (mysql)
  */
 public static function date($date)
 {
     if (!preg_match('~\\d{2}\\.\\d{2}\\.\\d{4}~', $date)) {
         $msg = App::t('Неверный формат даты. Ожидается "FORMAT".', ['FORMAT' => 'dd.mm.yyyy']);
         return ['error' => $msg];
     }
     list($d, $m, $y) = explode('.', $date);
     if (!checkdate($m, $d, $y)) {
         return ['error' => App::t('Нереальная дата')];
     }
     return ['value' => sprintf('%d-%d-%d', $y, $m, $d)];
 }
Пример #13
0
 /**
  * Добавление flash-сообщения в сессию
  *
  * Флаг перезаписи: true - любое значение переписываем, false - массивы объединяем, строки дописываем, <b>целые</b>
  * числа суммируем, с другими данными не работаем. При этом тип новых данных приводим к имеющемуся в сессии.
  *
  * @param string $key   ключ в сессии
  * @param mixed  $data  данные для записи
  * @param bool   $force флаг перезаписи. True - любое значение переписываем, false - массивы объединяем,
  *                      строки дописываем, числа суммируем. При этом тип новых данных приводим к имеющемуся в сессии.
  * @return void
  */
 public static function addFlash($key, $data, $force = false)
 {
     self::init();
     $data = App::t($data);
     if (isset($_SESSION['flash'][$key]) && !$force) {
         $curData = $_SESSION['flash'][$key];
         if (is_array($curData)) {
             $data = array_merge($curData, (array) $data);
         } elseif (!is_numeric($curData)) {
             $data = $curData . $data;
         } elseif (preg_match('~^\\d+$~', $curData)) {
             $data = (int) $curData + intval($data);
         }
     }
     $_SESSION['flash'][$key] = $data;
 }