'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');
}
}
}