'error', # Layout шаблон 'error.common' => 'error.common', # Стандартный шаблон ошибок 'error.404' => 'error.404', # 404 ошибка 'error.exception' => 'error.exception', # Исключение # Сообщения: 'message.success' => 'message.success', # сообщение "Успешно" 'message.forbidden' => 'message.forbidden', # сообщение "В доступе отказано" ]; protected $suppressWarnings = false; /** @var Run */ protected $whoops; public function __construct(\bff\contracts\Application $app) { $this->app = $app; error_reporting(-1); set_error_handler([$this, 'handleError']); if ($this->app->isDebug()) { ini_set('display_startup_errors', 1); } else { ini_set('display_errors', 'Off'); } $this->app->hook('errors.init', $this); } public function onNewRequest($request) { $this->clear(); } /** * @return Run|null */ public function whoops() { if ($this->whoops === null && $this->app->isDebug()) { if (class_exists('\Whoops\Run')) { $this->whoops = new Run(); $this->whoops->register(); } } if ($this->whoops !== null) { $this->whoops->clearHandlers(); if ($this->app->request()->isAJAX()) { $this->whoops->prependHandler(new JsonResponseHandler()); } else { $this->whoops->prependHandler(new PrettyPageHandler()); } $this->whoops->allowQuit(false); $this->whoops->writeToOutput(false); } return $this->whoops; } /** * Переопределение шаблонов * @param array $templates список шаблонов: [ключ шаблона => название шаблона в директории templatesPath, ...] * @return void */ public function setTemplates(array $templates = []) { foreach ($templates as $k => $v) { if (! empty($v) && isset($this->templates[$k])) { $this->templates[$k] = $v; } } } /** * Сохраняем сообщение об ошибке * @param string|int $error текст ошибки или ключ(например static::SUCCESS) * @param mixed $system * bool::true - системная ошибка * string::'key' - несистемная, ключ ошибки * array::['key'=>'value'] - массив ключ-значение для подмены в тексте ошибки * @param mixed $key ключ ошибки или имя input-поля * @return static объект */ public function set($error, $system = false, $key = null) { # подготавливаем текст ошибки if (is_int($error)) { $message = $this->getSystemMessage($error); } else { $message = $error; } # подставляем значения в текст if (is_array($system) && !empty($system) && is_string($message)) { $message = strtr($message, $system); } $errorData = ['sys' => ($system === true), 'errno' => $error, 'msg' => $message]; if (!isset($key) && is_string($system)) { $key = $system; } if (isset($key)) { $this->errors[$key] = $errorData; $this->field($key); } else { $this->errors[] = $errorData; } if ($system === true) { $this->app->log($message); } return $this; } /** * Получаем сообщения об ошибках * @param bool $onlyMessages только текст * @param bool $excludeSystem исключая системные * @return array */ public function get(bool $onlyMessages = true, bool $excludeSystem = true): array { if (empty($this->errors)) { return []; } if ($this->app->isDebug() || $this->app->adminFordev()) { $excludeSystem = false; } if ($onlyMessages) { $res = []; foreach ($this->errors as $k => $v) { if ($excludeSystem && $v['sys']) { continue; } $res[$k] = $v['msg']; } return $res; } if ($excludeSystem) { $res = []; foreach ($this->errors as $k => $v) { if ($v['sys']) { continue; } $res[$k] = $v; } return $res; } return $this->errors; } /** * Получаем текст последней ошибки * @return string|bool */ public function last() { if (empty($this->errors)) { return false; } $return = end($this->errors); reset($this->errors); return $return['msg'] ?? false; } /** * Помечаем ключ поля(нескольких полей), с которым связаны добавленные ошибки * @param string|array $key ключ поля(нескольких полей) * @return void */ public function field($key) { if (is_string($key)) { $this->fields[] = $key; } else { if (is_array($key)) { foreach ($key as $k) { $this->fields[] = $k; } } } } /** * Помечаем список ключей полей, с которым связаны добавленные ошибки * @return array */ public function fields(): array { return array_unique($this->fields); } /** * Помечаем невозможность выполнения операции * @param bool $return * @return mixed */ public function impossible(bool $return = false) { if ($return) { return $this->getSystemMessage(static::IMPOSSIBLE); } return $this->set(static::IMPOSSIBLE); } /** * Помечаем ошибку доступа * @param bool $return * @return mixed */ public function accessDenied(bool $return = false) { if ($return) { return $this->getSystemMessage(static::ACCESSDENIED); } return $this->set(static::ACCESSDENIED); } /** * Помечаем ошибку (ID редактируемой записи некорректный) * @param bool $return * @return mixed */ public function unknownRecord(bool $return = false) { if ($return) { return $this->getSystemMessage(static::UNKNOWNRECORD); } return $this->set(static::UNKNOWNRECORD); } /** * Помечаем ошибку (требуется перезагрузка страницы) * @param bool $return * @return mixed */ public function reloadPage(bool $return = false) { if ($return) { return $this->getSystemMessage(static::RELOAD_PAGE); } return $this->set(static::RELOAD_PAGE); } /** * Помечаем ошибку (действуют демо ограничения) * @param bool $return * @return mixed */ public function demoLimited(bool $return = false) { if ($return) { return $this->getSystemMessage(static::DEMO_LIMITED); } return $this->set(static::DEMO_LIMITED); } /** * Помечаем успешность выполнения действия * @return static */ public function success() { $this->set(static::SUCCESS); $this->app->input()->set('errno', static::SUCCESS); return $this; } /** * Получаем успешность выполнения действия * @return bool */ public function isSuccess(): bool { $errorNumber = $this->app->input()->getpost('errno', TYPE_UINT); $success = ($errorNumber == static::SUCCESS || (!$errorNumber && $this->no())); if ($errorNumber > 0 && $this->no()) { $this->set($errorNumber); } return $success; } /** * Получаем информацию об отсутствии ошибок * @param string $hook ключ хука * @param array|mixed $hookData данные для хук-вызова * @return bool true - нет; false - есть */ public function no(string $hook = '', $hookData = []): bool { if (! empty($hook) && is_string($hook)) { $this->app->hook($hook, $hookData); } return (sizeof($this->errors) == 0); } /** * Получаем информацию о наличии ошибок * @param string $hook ключ хука * @param array|mixed $hookData данные для хук-вызова * @return bool true - есть; false - нет */ public function any(string $hook = '', $hookData = []): bool { return ! $this->no($hook, $hookData); } /** * Обнуляем информацию о существующих ошибках * @return void */ public function clear() { $this->errors = []; $this->fields = []; $this->isAutohide = true; $this->suppressWarnings = false; } /** * Помечаем необходимость автоматического сворачивания блока ошибок * @param bool|null $hide true/false - помечаем требуемое состояние, null - получаем текущее * @return bool */ public function autohide(?bool $hide = null): bool { if (is_bool($hide)) { $this->isAutohide = $hide; } return $this->isAutohide; } /** * Not found exception * @param ResponseInterface|null $response объект ответа * @param ServerRequestInterface|null $request объект запроса * @throws NotFoundException */ public function notFoundException($response = null, $request = null) { throw NotFoundException::init($response, $request); } /** * 404 Error * @param ResponseInterface|null $response объект ответа * @return \bff\http\Response */ public function error404($response = null) { SEO::robotsIndex(false); return $this->errorHttp(404, $response); } /** * Отображаем HTTP ошибку * @param int $errorCode код http ошибки, например: 404 * @param ResponseInterface|null $response объект ответа или NULL * @param string|null $template название PHP шаблона или NULL (шаблон по умолчанию) * @return \bff\http\Response|void */ public function errorHttp(int $errorCode, $response = null, ?string $template = null) { if (is_null($response)) { $response = $this->app->response(); } $data = [ 'errno' => $errorCode, 'title' => _t('errors', 'Internal server error'), 'message' => '', ]; switch ($errorCode) { case 401: $response = $response->withStatus(401); $response = $response->withHeader('WWW-Authenticate', 'Basic realm="' . $this->app->request()->host(SITEHOST) . '"'); $data['title'] = _t('errors', 'Access Denied'); $data['message'] = _t('errors', 'You should enter a correct login and password to access the resource.'); break; case 403: # пользователь не прошел аутентификацию, запрет на доступ (Forbidden). $response = $response->withStatus(403); $data['title'] = _t('errors', 'Access Denied'); $data['message'] = _t('errors', 'Access to the specified page is denied'); break; case 404: $response = $response->withStatus(404); if (empty($template)) { $template = $this->templates['error.404']; } $data['title'] = _t('errors', 'Page not found!'); $data['message'] = _t('errors', 'The page you tried to log into does not exist.'); break; default: if (!empty($errorCode)) { $response = $response->withStatus($errorCode); $data['title'] = _t('errors', 'Internal server error'); $data['message'] = _t('errors', 'Internal server error occurred ([code])', [ 'code' => $errorCode, ]); } else { $data['title'] = _t('errors', 'Internal server error'); $data['message'] = _t('errors', 'Internal server error occurred'); } break; } $this->app->hook('errors.http.error', $errorCode, [ 'data' => &$data, 'response' => &$response, 'template' => &$template, ]); if ($response->getBody()->isWritable()) { $response->getBody()->write($this->viewError($data, $template)); } return $response; } /** * Выводим ошибку * @param array $data данные об ошибке: errno, title, message * @param string|null $templateName название шаблона или NULL (шаблон по умолчанию) * @return string HTML */ public function viewError(array $data = [], ?string $templateName = null) { if ($this->app->adminPanel()) { return $data['title'] . '
' . $data['message']; } if (empty($templateName)) { $templateName = $this->templates['error.common']; } $data['centerblock'] = View::template($templateName, $data); return View::layoutRender($data, $this->templates['error.layout']); } /** * Отображаем уведомление "Успешно..." (frontend) * @param string $title заголовок сообщения * @param string|int|array $message текст сообщения * @param array $opts * @return string HTML */ public function messageSuccess(string $title, $message, array $opts = []) { return $this->viewMessage($title, $message, $opts, $this->templates['message.success']); } /** * Отображаем уведомление об "Ошибке..." (frontend) * @param string $title заголовок сообщения * @param string|int|array $message текст сообщения * @param array $opts ['auth' - требуется авторизация] * @return string HTML */ public function messageForbidden(string $title, $message, array $opts = []) { return $this->viewMessage($title, $message, $opts, $this->templates['message.forbidden']); } /** * Отображаем сообщение (frontend) * @param string $title заголовок сообщения * @param string|int|array $message текст сообщения * @param array $opts ['auth' - требуется авторизация] * @param string|null $templateName название шаблона или NULL ('message.forbidden') * @return string HTML */ public function viewMessage(string $title, $message = '', array $opts = [], ?string $templateName = null) { if (empty($templateName)) { $templateName = $this->templates['message.forbidden']; } if (is_int($message)) { $message = $this->getSystemMessage($message); } return View::template($templateName, [ 'title' => $title, 'message' => $message, 'auth' => ($opts['auth'] ?? false), ]); } /** * Errors handler * @param int $level * @param string $message * @param string|null $file * @param int|null $line * @return bool */ public function handleError($level, $message, $file = null, $line = null): bool { if ($level == E_WARNING && $this->suppressWarnings) { return true; } if ( in_array($level, [ E_USER_ERROR, E_USER_WARNING, E_USER_NOTICE, E_STRICT, E_ERROR, E_NOTICE, E_WARNING, ]) ) { $this->set($message . '
' . $file . ' [' . $line . ']', true); } if (config::sys('errors.log.backtrace', false)) { $message .= PHP_EOL . ' ' . $file . ' [' . $line . ']'; foreach (debug_backtrace(false) as $v) { if (! empty($v['file']) && ! empty($v['line'])) { $message .= PHP_EOL . ' ' . $v['file'] . ' [' . $v['line'] . '] '; foreach (['class', 'type', 'function'] as $f) { if (! empty($v[$f])) { $message .= $v[$f]; } } } } } else { $message .= ' > ' . $file . ' [' . $line . ']'; } $this->app->log($message); return true; } /** * Exception handler * @param Throwable $exception * @return \bff\http\Response|mixed */ public function handleException(Throwable $exception) { # log $this->app->log($exception->getMessage() . PHP_EOL . $exception->getTraceAsString()); # report if ($this->app->adminPanel()) { $report = User::admin(); } else { $report = $this->app->isDebug(); } if ($report) { $whoops = $this->whoops(); if ($whoops) { return Response::html($whoops->handleException($exception)); } else { return Response::json([ 'exception' => [ 'message' => $exception->getMessage(), 'file' => $exception->getFile(), 'line' => $exception->getLine(), 'trace' => $exception->getTraceAsString(), ] ]); } } if ($exception instanceof BaseException) { return $exception->getResponse(); } return Response::current(); } /** * Shutdown handler * @return void */ public function handleShutdown() { $lastError = error_get_last(); if ($lastError && $lastError['type'] === E_ERROR) { $this->handleError(E_ERROR, $lastError['message'], $lastError['file'], $lastError['line']); } } /** * Подавлять ошибки * @param bool $suppress * @return void */ public function suppressWarnings(bool $suppress = true) { $this->suppressWarnings = $suppress; } /** * Сохраняем сообщение об ошибке загрузки * @param int $uploadErrorCode код ошибки загрузки * @param array $params доп. параметры ошибки * @param mixed $system bool::true - системная ошибка, string::'key' - не системная, ключ ошибки * @param mixed $key ключ ошибки или имя input-поля * @return static */ public function setUploadError(int $uploadErrorCode, array $params = [], $system = false, $key = null) { return $this->set($this->getUploadErrorMessage($uploadErrorCode, $params), $system, $key); } /** * Получаем текст ошибки загрузки файла по коду * @param int $uploadErrorCode код ошибки загрузки * @param array $params доп. параметры ошибки * @return string текст ошибки */ public function getUploadErrorMessage(int $uploadErrorCode, array $params = []): string { switch ($uploadErrorCode) { case static::FILE_UPLOAD_ERROR: $message = _t('upload', 'Error uploading file', $params); break; case static::FILE_WRONG_SIZE: $message = _t('upload', 'Incorrect file size', $params); break; case static::FILE_MAX_SIZE: $message = _t('upload', 'File exceeds the maximum size allowed', $params); break; case static::FILE_DISK_QUOTA: $message = _t('upload', 'Error uploading file, contact administrator', $params); break; case static::FILE_WRONG_TYPE: $message = _t('upload', 'Prohibited file type', $params); break; case static::FILE_WRONG_NAME: $message = _t('upload', 'Incorrect file name', $params); break; case static::FILE_ALREADY_EXISTS: $message = _t('upload', 'File with this name has already been uploaded', $params); break; case static::FILE_MAX_DIMENTION: $message = _t('upload', 'The image is too large in width / height', $params); break; default: $message = _t('upload', 'Error uploading file'); break; } return $message; } /** * Получаем текст ошибки по коду ошибки * @param int $errorCode код ошибки * @return string текст ошибки */ public function getSystemMessage(int $errorCode): string { switch ($errorCode) { case static::SUCCESS: # Operation is successfull return _t('system', 'Operation completed successfully'); case static::ACCESSDENIED: # Access denied return _t('system', 'Access Denied'); case static::UNKNOWNRECORD: case static::IMPOSSIBLE: # Unable to complete operation return _t('system', 'Unable to perform operation'); case static::RELOAD_PAGE: # Reload page and retry return _t('system', 'Refresh the page and try again'); case static::DEMO_LIMITED: # This operation is not allowed in demo mode return _t('system', 'This operation is not available in demo mode'); default: # Unknown error return _t('system', 'Unknown error'); } } }