Chris@0: get('database'), Chris@0: $container->get('module_handler'), Chris@0: $container->get('date.formatter'), Chris@0: $container->get('form_builder') Chris@0: ); Chris@0: } Chris@0: Chris@0: /** Chris@0: * Constructs a DbLogController object. Chris@0: * Chris@0: * @param \Drupal\Core\Database\Connection $database Chris@0: * A database connection. Chris@0: * @param \Drupal\Core\Extension\ModuleHandlerInterface $module_handler Chris@0: * A module handler. Chris@0: * @param \Drupal\Core\Datetime\DateFormatterInterface $date_formatter Chris@0: * The date formatter service. Chris@0: * @param \Drupal\Core\Form\FormBuilderInterface $form_builder Chris@0: * The form builder service. Chris@0: */ Chris@0: public function __construct(Connection $database, ModuleHandlerInterface $module_handler, DateFormatterInterface $date_formatter, FormBuilderInterface $form_builder) { Chris@0: $this->database = $database; Chris@0: $this->moduleHandler = $module_handler; Chris@0: $this->dateFormatter = $date_formatter; Chris@0: $this->formBuilder = $form_builder; Chris@18: $this->userStorage = $this->entityTypeManager()->getStorage('user'); Chris@0: } Chris@0: Chris@0: /** Chris@0: * Gets an array of log level classes. Chris@0: * Chris@0: * @return array Chris@0: * An array of log level classes. Chris@0: */ Chris@0: public static function getLogLevelClassMap() { Chris@0: return [ Chris@0: RfcLogLevel::DEBUG => 'dblog-debug', Chris@0: RfcLogLevel::INFO => 'dblog-info', Chris@0: RfcLogLevel::NOTICE => 'dblog-notice', Chris@0: RfcLogLevel::WARNING => 'dblog-warning', Chris@0: RfcLogLevel::ERROR => 'dblog-error', Chris@0: RfcLogLevel::CRITICAL => 'dblog-critical', Chris@0: RfcLogLevel::ALERT => 'dblog-alert', Chris@0: RfcLogLevel::EMERGENCY => 'dblog-emergency', Chris@0: ]; Chris@0: } Chris@0: Chris@0: /** Chris@0: * Displays a listing of database log messages. Chris@0: * Chris@0: * Messages are truncated at 56 chars. Chris@0: * Full-length messages can be viewed on the message details page. Chris@0: * Chris@0: * @return array Chris@16: * A render array as expected by Chris@16: * \Drupal\Core\Render\RendererInterface::render(). Chris@0: * Chris@0: * @see Drupal\dblog\Form\DblogClearLogConfirmForm Chris@0: * @see Drupal\dblog\Controller\DbLogController::eventDetails() Chris@0: */ Chris@0: public function overview() { Chris@0: Chris@0: $filter = $this->buildFilterQuery(); Chris@0: $rows = []; Chris@0: Chris@0: $classes = static::getLogLevelClassMap(); Chris@0: Chris@0: $this->moduleHandler->loadInclude('dblog', 'admin.inc'); Chris@0: Chris@0: $build['dblog_filter_form'] = $this->formBuilder->getForm('Drupal\dblog\Form\DblogFilterForm'); Chris@0: Chris@0: $header = [ Chris@0: // Icon column. Chris@0: '', Chris@0: [ Chris@0: 'data' => $this->t('Type'), Chris@0: 'field' => 'w.type', Chris@0: 'class' => [RESPONSIVE_PRIORITY_MEDIUM], Chris@0: ], Chris@0: [ Chris@0: 'data' => $this->t('Date'), Chris@0: 'field' => 'w.wid', Chris@0: 'sort' => 'desc', Chris@0: 'class' => [RESPONSIVE_PRIORITY_LOW], Chris@0: ], Chris@0: $this->t('Message'), Chris@0: [ Chris@0: 'data' => $this->t('User'), Chris@0: 'field' => 'ufd.name', Chris@0: 'class' => [RESPONSIVE_PRIORITY_MEDIUM], Chris@0: ], Chris@0: [ Chris@0: 'data' => $this->t('Operations'), Chris@0: 'class' => [RESPONSIVE_PRIORITY_LOW], Chris@0: ], Chris@0: ]; Chris@0: Chris@0: $query = $this->database->select('watchdog', 'w') Chris@0: ->extend('\Drupal\Core\Database\Query\PagerSelectExtender') Chris@0: ->extend('\Drupal\Core\Database\Query\TableSortExtender'); Chris@0: $query->fields('w', [ Chris@0: 'wid', Chris@0: 'uid', Chris@0: 'severity', Chris@0: 'type', Chris@0: 'timestamp', Chris@0: 'message', Chris@0: 'variables', Chris@0: 'link', Chris@0: ]); Chris@0: $query->leftJoin('users_field_data', 'ufd', 'w.uid = ufd.uid'); Chris@0: Chris@0: if (!empty($filter['where'])) { Chris@0: $query->where($filter['where'], $filter['args']); Chris@0: } Chris@0: $result = $query Chris@0: ->limit(50) Chris@0: ->orderByHeader($header) Chris@0: ->execute(); Chris@0: Chris@0: foreach ($result as $dblog) { Chris@0: $message = $this->formatMessage($dblog); Chris@0: if ($message && isset($dblog->wid)) { Chris@0: $title = Unicode::truncate(Html::decodeEntities(strip_tags($message)), 256, TRUE, TRUE); Chris@0: $log_text = Unicode::truncate($title, 56, TRUE, TRUE); Chris@0: // The link generator will escape any unsafe HTML entities in the final Chris@0: // text. Chris@0: $message = $this->l($log_text, new Url('dblog.event', ['event_id' => $dblog->wid], [ Chris@0: 'attributes' => [ Chris@0: // Provide a title for the link for useful hover hints. The Chris@0: // Attribute object will escape any unsafe HTML entities in the Chris@0: // final text. Chris@0: 'title' => $title, Chris@0: ], Chris@0: ])); Chris@0: } Chris@0: $username = [ Chris@0: '#theme' => 'username', Chris@0: '#account' => $this->userStorage->load($dblog->uid), Chris@0: ]; Chris@0: $rows[] = [ Chris@0: 'data' => [ Chris@0: // Cells. Chris@0: ['class' => ['icon']], Chris@0: $this->t($dblog->type), Chris@0: $this->dateFormatter->format($dblog->timestamp, 'short'), Chris@0: $message, Chris@0: ['data' => $username], Chris@0: ['data' => ['#markup' => $dblog->link]], Chris@0: ], Chris@0: // Attributes for table row. Chris@0: 'class' => [Html::getClass('dblog-' . $dblog->type), $classes[$dblog->severity]], Chris@0: ]; Chris@0: } Chris@0: Chris@0: $build['dblog_table'] = [ Chris@0: '#type' => 'table', Chris@0: '#header' => $header, Chris@0: '#rows' => $rows, Chris@0: '#attributes' => ['id' => 'admin-dblog', 'class' => ['admin-dblog']], Chris@0: '#empty' => $this->t('No log messages available.'), Chris@0: '#attached' => [ Chris@0: 'library' => ['dblog/drupal.dblog'], Chris@0: ], Chris@0: ]; Chris@0: $build['dblog_pager'] = ['#type' => 'pager']; Chris@0: Chris@0: return $build; Chris@0: Chris@0: } Chris@0: Chris@0: /** Chris@0: * Displays details about a specific database log message. Chris@0: * Chris@0: * @param int $event_id Chris@0: * Unique ID of the database log message. Chris@0: * Chris@0: * @return array Chris@0: * If the ID is located in the Database Logging table, a build array in the Chris@16: * format expected by \Drupal\Core\Render\RendererInterface::render(). Chris@0: */ Chris@0: public function eventDetails($event_id) { Chris@0: $build = []; Chris@0: if ($dblog = $this->database->query('SELECT w.*, u.uid FROM {watchdog} w LEFT JOIN {users} u ON u.uid = w.uid WHERE w.wid = :id', [':id' => $event_id])->fetchObject()) { Chris@0: $severity = RfcLogLevel::getLevels(); Chris@0: $message = $this->formatMessage($dblog); Chris@0: $username = [ Chris@0: '#theme' => 'username', Chris@0: '#account' => $dblog->uid ? $this->userStorage->load($dblog->uid) : User::getAnonymousUser(), Chris@0: ]; Chris@0: $rows = [ Chris@0: [ Chris@0: ['data' => $this->t('Type'), 'header' => TRUE], Chris@0: $this->t($dblog->type), Chris@0: ], Chris@0: [ Chris@0: ['data' => $this->t('Date'), 'header' => TRUE], Chris@0: $this->dateFormatter->format($dblog->timestamp, 'long'), Chris@0: ], Chris@0: [ Chris@0: ['data' => $this->t('User'), 'header' => TRUE], Chris@0: ['data' => $username], Chris@0: ], Chris@0: [ Chris@0: ['data' => $this->t('Location'), 'header' => TRUE], Chris@18: $this->createLink($dblog->location), Chris@0: ], Chris@0: [ Chris@0: ['data' => $this->t('Referrer'), 'header' => TRUE], Chris@18: $this->createLink($dblog->referer), Chris@0: ], Chris@0: [ Chris@0: ['data' => $this->t('Message'), 'header' => TRUE], Chris@0: $message, Chris@0: ], Chris@0: [ Chris@0: ['data' => $this->t('Severity'), 'header' => TRUE], Chris@0: $severity[$dblog->severity], Chris@0: ], Chris@0: [ Chris@0: ['data' => $this->t('Hostname'), 'header' => TRUE], Chris@0: $dblog->hostname, Chris@0: ], Chris@0: [ Chris@0: ['data' => $this->t('Operations'), 'header' => TRUE], Chris@0: ['data' => ['#markup' => $dblog->link]], Chris@0: ], Chris@0: ]; Chris@0: $build['dblog_table'] = [ Chris@0: '#type' => 'table', Chris@0: '#rows' => $rows, Chris@0: '#attributes' => ['class' => ['dblog-event']], Chris@0: '#attached' => [ Chris@0: 'library' => ['dblog/drupal.dblog'], Chris@0: ], Chris@0: ]; Chris@0: } Chris@0: Chris@0: return $build; Chris@0: } Chris@0: Chris@0: /** Chris@0: * Builds a query for database log administration filters based on session. Chris@0: * Chris@0: * @return array|null Chris@0: * An associative array with keys 'where' and 'args' or NULL if there were Chris@0: * no filters set. Chris@0: */ Chris@0: protected function buildFilterQuery() { Chris@0: if (empty($_SESSION['dblog_overview_filter'])) { Chris@0: return; Chris@0: } Chris@0: Chris@0: $this->moduleHandler->loadInclude('dblog', 'admin.inc'); Chris@0: Chris@0: $filters = dblog_filters(); Chris@0: Chris@0: // Build query. Chris@0: $where = $args = []; Chris@0: foreach ($_SESSION['dblog_overview_filter'] as $key => $filter) { Chris@0: $filter_where = []; Chris@0: foreach ($filter as $value) { Chris@0: $filter_where[] = $filters[$key]['where']; Chris@0: $args[] = $value; Chris@0: } Chris@0: if (!empty($filter_where)) { Chris@0: $where[] = '(' . implode(' OR ', $filter_where) . ')'; Chris@0: } Chris@0: } Chris@0: $where = !empty($where) ? implode(' AND ', $where) : ''; Chris@0: Chris@0: return [ Chris@0: 'where' => $where, Chris@0: 'args' => $args, Chris@0: ]; Chris@0: } Chris@0: Chris@0: /** Chris@0: * Formats a database log message. Chris@0: * Chris@0: * @param object $row Chris@0: * The record from the watchdog table. The object properties are: wid, uid, Chris@0: * severity, type, timestamp, message, variables, link, name. Chris@0: * Chris@0: * @return string|\Drupal\Core\StringTranslation\TranslatableMarkup|false Chris@0: * The formatted log message or FALSE if the message or variables properties Chris@0: * are not set. Chris@0: */ Chris@0: public function formatMessage($row) { Chris@0: // Check for required properties. Chris@0: if (isset($row->message, $row->variables)) { Chris@0: $variables = @unserialize($row->variables); Chris@0: // Messages without variables or user specified text. Chris@0: if ($variables === NULL) { Chris@0: $message = Xss::filterAdmin($row->message); Chris@0: } Chris@0: elseif (!is_array($variables)) { Chris@0: $message = $this->t('Log data is corrupted and cannot be unserialized: @message', ['@message' => Xss::filterAdmin($row->message)]); Chris@0: } Chris@0: // Message to translate with injected variables. Chris@0: else { Chris@0: $message = $this->t(Xss::filterAdmin($row->message), $variables); Chris@0: } Chris@0: } Chris@0: else { Chris@0: $message = FALSE; Chris@0: } Chris@0: return $message; Chris@0: } Chris@0: Chris@0: /** Chris@18: * Creates a Link object if the provided URI is valid. Chris@18: * Chris@18: * @param string|null $uri Chris@18: * The uri string to convert into link if valid. Chris@18: * Chris@18: * @return \Drupal\Core\Link|string|null Chris@18: * Return a Link object if the uri can be converted as a link. In case of Chris@18: * empty uri or invalid, fallback to the provided $uri. Chris@18: */ Chris@18: protected function createLink($uri) { Chris@18: if (UrlHelper::isValid($uri, TRUE)) { Chris@18: return new Link($uri, Url::fromUri($uri)); Chris@18: } Chris@18: return $uri; Chris@18: } Chris@18: Chris@18: /** Chris@0: * Shows the most frequent log messages of a given event type. Chris@0: * Chris@0: * Messages are not truncated on this page because events detailed herein do Chris@0: * not have links to a detailed view. Chris@0: * Chris@0: * @param string $type Chris@0: * Type of database log events to display (e.g., 'search'). Chris@0: * Chris@0: * @return array Chris@16: * A build array in the format expected by Chris@16: * \Drupal\Core\Render\RendererInterface::render(). Chris@0: */ Chris@0: public function topLogMessages($type) { Chris@0: $header = [ Chris@0: ['data' => $this->t('Count'), 'field' => 'count', 'sort' => 'desc'], Chris@0: ['data' => $this->t('Message'), 'field' => 'message'], Chris@0: ]; Chris@0: Chris@0: $count_query = $this->database->select('watchdog'); Chris@0: $count_query->addExpression('COUNT(DISTINCT(message))'); Chris@0: $count_query->condition('type', $type); Chris@0: Chris@0: $query = $this->database->select('watchdog', 'w') Chris@0: ->extend('\Drupal\Core\Database\Query\PagerSelectExtender') Chris@0: ->extend('\Drupal\Core\Database\Query\TableSortExtender'); Chris@0: $query->addExpression('COUNT(wid)', 'count'); Chris@0: $query = $query Chris@0: ->fields('w', ['message', 'variables']) Chris@0: ->condition('w.type', $type) Chris@0: ->groupBy('message') Chris@0: ->groupBy('variables') Chris@0: ->limit(30) Chris@0: ->orderByHeader($header); Chris@0: $query->setCountQuery($count_query); Chris@0: $result = $query->execute(); Chris@0: Chris@0: $rows = []; Chris@0: foreach ($result as $dblog) { Chris@0: if ($message = $this->formatMessage($dblog)) { Chris@0: $rows[] = [$dblog->count, $message]; Chris@0: } Chris@0: } Chris@0: Chris@17: $build['dblog_top_table'] = [ Chris@0: '#type' => 'table', Chris@0: '#header' => $header, Chris@0: '#rows' => $rows, Chris@0: '#empty' => $this->t('No log messages available.'), Chris@0: '#attached' => [ Chris@0: 'library' => ['dblog/drupal.dblog'], Chris@0: ], Chris@0: ]; Chris@0: $build['dblog_top_pager'] = ['#type' => 'pager']; Chris@0: Chris@0: return $build; Chris@0: } Chris@0: Chris@0: }