chg: use view factory

pull/9434/head
Luciano Righetti 2023-12-07 11:16:00 +01:00
parent 796a9a04bf
commit 8fff28fd5f
4 changed files with 123 additions and 528 deletions

View File

@ -9,10 +9,11 @@ use Cake\Http\Exception\NotFoundException;
class AccessLogsController extends AppController
{
public $paginate = [
'recursive' => -1,
'limit' => 60,
'fields' => ['id', 'created', 'user_id', 'org_id', 'authkey_id', 'ip', 'request_method', 'user_agent', 'request_id', 'controller', 'action', 'url', 'response_code', 'memory_usage', 'duration', 'query_count'],
'fields' => ['id', 'created', 'user_id', 'org_id', 'authkey_id', 'ip', 'request_method', 'user_agent', 'request_id', 'controller', 'action', 'url', 'response_code', 'memory_usage', 'duration', 'query_count', 'request'],
'contain' => [
'Users' => ['fields' => ['id', 'email', 'org_id']],
'Organisations' => ['fields' => ['id', 'name', 'uuid']],
@ -22,6 +23,33 @@ class AccessLogsController extends AppController
],
];
public $quickFilterFields = [
'ip',
['user_agent' => true],
['action' => true],
['url' => true],
['controller' => true]
];
public $filterFields = [
'created',
'ip',
'user',
'org_id',
'request_id',
'authkey_id',
'api_request',
'request_method',
'controller',
'action',
'url',
'user_agent',
'memory_usage',
'duration',
'query_count',
'response_code',
];
public function initialize(): void
{
parent::initialize();
@ -51,32 +79,27 @@ class AccessLogsController extends AppController
]
);
$conditions = $this->__searchConditions($params);
// $conditions = $this->__searchConditions($params);
if ($this->ParamHandler->isRest()) {
$list = $this->AccessLogs->find(
'all',
[
'conditions' => $conditions,
'contain' => $this->paginate['contain'],
]
);
foreach ($list as $item) {
if (!empty($item['request'])) {
$item['request'] = base64_encode($item['request']);
}
$afterFindHandler = function ($entry) {
if (!empty($entry['request'])) {
$entry['request'] = base64_encode($entry['request']);
}
return $this->RestResponse->viewData($list->toArray(), 'json');
}
return $entry;
};
$this->CRUD->index(
[
'filters' => $this->filterFields,
'quickFilters' => $this->quickFilterFields,
'afterFind' => $afterFindHandler,
]
);
if (empty(Configure::read('MISP.log_skip_access_logs_in_application_logs'))) {
$this->Flash->info(__('Access logs are logged in both application logs and access logs. Make sure you reconfigure your log monitoring tools and update MISP.log_skip_access_logs_in_application_logs.'));
}
$this->paginate['conditions'] = $conditions;
$list = $this->paginate();
$this->set('list', $list);
$this->set('title_for_layout', __('Access logs'));
}
public function request($id)
@ -136,110 +159,8 @@ class AccessLogsController extends AppController
$this->set('queryLog', $request['query_log']);
}
/**
* @param array $params
* @return array
*/
private function __searchConditions(array $params)
public function filtering()
{
$qbRules = [];
foreach ($params as $key => $value) {
if ($key === 'created') {
$qbRules[] = [
'id' => $key,
'operator' => is_array($value) ? 'between' : 'greater_or_equal',
'value' => $value,
];
} else {
if (is_array($value)) {
$value = implode('||', $value);
}
$qbRules[] = [
'id' => $key,
'value' => $value,
];
}
}
$this->set('qbRules', $qbRules);
$conditions = [];
if (isset($params['user'])) {
if (is_numeric($params['user'])) {
$conditions['AccessLogs.user_id'] = $params['user'];
} else {
$user = $this->Users->find(
'first',
[
'conditions' => ['Users.email' => $params['user']],
'fields' => ['id'],
]
);
if (!empty($user)) {
$conditions['AccessLogs.user_id'] = $user['id'];
} else {
$conditions['AccessLogs.user_id'] = -1;
}
}
}
if (isset($params['ip'])) {
$conditions['AccessLogs.ip'] = inet_pton($params['ip']);
}
foreach (['authkey_id', 'request_id', 'controller', 'action'] as $field) {
if (isset($params[$field])) {
$conditions['AccessLogs.' . $field] = $params[$field];
}
}
if (isset($params['url'])) {
$conditions['AccessLogs.url LIKE'] = "%{$params['url']}%";
}
if (isset($params['user_agent'])) {
$conditions['AccessLogs.user_agent LIKE'] = "%{$params['user_agent']}%";
}
if (isset($params['memory_usage'])) {
$conditions['AccessLogs.memory_usage >='] = ($params['memory_usage'] * 1024);
}
if (isset($params['memory_usage'])) {
$conditions['AccessLogs.memory_usage >='] = ($params['memory_usage'] * 1024);
}
if (isset($params['duration'])) {
$conditions['AccessLogs.duration >='] = $params['duration'];
}
if (isset($params['query_count'])) {
$conditions['AccessLogs.query_count >='] = $params['query_count'];
}
if (isset($params['request_method'])) {
$methodId = array_flip(AccessLog::REQUEST_TYPES)[$params['request_method']] ?? -1;
$conditions['AccessLogs.request_method'] = $methodId;
}
if (isset($params['org'])) {
if (is_numeric($params['org'])) {
$conditions['AccessLogs.org_id'] = $params['org'];
} else {
$org = $this->AccessLogs->Organisation->fetchOrg($params['org']);
if ($org) {
$conditions['AccessLogs.org_id'] = $org['id'];
} else {
$conditions['AccessLogs.org_id'] = -1;
}
}
}
if (isset($params['created'])) {
$tempData = is_array($params['created']) ? $params['created'] : [$params['created']];
foreach ($tempData as $k => $v) {
$tempData[$k] = $this->AccessLogs->resolveTimeDelta($v);
}
if (count($tempData) === 1) {
$conditions['AccessLogs.created >='] = date("Y-m-d H:i:s", $tempData[0]);
} else {
if ($tempData[0] < $tempData[1]) {
$temp = $tempData[1];
$tempData[1] = $tempData[0];
$tempData[0] = $temp;
}
$conditions['AND'][] = ['AccessLogs.created <=' => date("Y-m-d H:i:s", $tempData[0])];
$conditions['AND'][] = ['AccessLogs.created >=' => date("Y-m-d H:i:s", $tempData[1])];
}
}
return $conditions;
$this->CRUD->filtering();
}
}

View File

@ -1,393 +1,82 @@
<?php
$fields = [
[
'name' => __('Created'),
'sort' => 'created',
'data_path' => 'created'
],
[
'name' => __('User'),
'sort' => 'User.email',
'data_path' => 'User.email'
],
[
'name' => __('IP'),
'data_path' => 'ip'
],
[
'name' => __('Organisation'),
'sort' => 'Organisation.name',
'element' => 'org',
'data_path' => 'Organisation',
'class' => 'short',
],
[
'name' => __('Request Method'),
'sort' => 'request_method',
'data_path' => 'request_method'
],
[
'name' => __('URL'),
'sort' => 'url',
'data_path' => 'url'
],
[
'name' => __('Response Code'),
'sort' => 'response_code',
'data_path' => 'response_code'
],
[
'name' => __('Memory Usage'),
'sort' => 'memory_usage',
'data_path' => 'memory_usage'
],
[
'name' => __('Duration'),
'sort' => 'duration',
'data_path' => 'duration'
],
[
'name' => __('Queries'),
'sort' => 'query_count',
'data_path' => 'query_count'
],
];
use App\Lib\Tools\JsonTool;
?>
<div class="logs index">
<h2><?= __('Access logs') ?></h2>
<div>
<div id="builder"></div>
<div style="display: flex; justify-content: flex-end; margin-top: 5px;">
<button id="qbSubmit" type="button" class="btn btn-success" style="margin-right: 5px;"> <i class="fa fa-filter"></i> <?= __('Filter'); ?></button>
<button id="qbClear" type="button" class="btn btn-xs btn-danger" title="<?= __('Clear filtering rules'); ?>"> <i class="fa fa-times"></i> <?= __('Clear'); ?></button>
</div>
</div>
<?php
echo $this->Html->script('moment.min');
echo $this->Html->script('doT');
echo $this->Html->script('extendext');
echo $this->Html->css('query-builder.default');
echo $this->Html->script('query-builder');
?>
<script>
var qbOptions = {
plugins: {
'unique-filter': null,
'filter-description': {
mode: 'inline'
},
},
conditions: ['AND'],
allow_empty: true,
filters: [{
id: 'created',
label: 'Created',
type: 'date',
operators: ['greater_or_equal', 'between'],
validation: {
format: 'YYYY-MM-DD'
},
plugin: 'datepicker',
plugin_config: {
format: 'yyyy-mm-dd',
todayBtn: 'linked',
todayHighlight: true,
autoclose: true
}
},
{
input: "text",
type: "string",
operators: [
"equal",
echo $this->element(
'genericElements/IndexTable/index_table',
[
'data' => [
'data' => $data,
'top_bar' => [
'children' => [
[
'type' => 'context_filters',
],
unique: true,
id: "ip",
label: "IP",
},
{
input: "text",
type: "string",
operators: [
"equal",
[
'type' => 'search',
'button' => __('Search'),
'placeholder' => __('Enter value to search'),
'data' => '',
'searchKey' => 'value',
'allowFilering' => true
],
unique: true,
id: "user",
label: "User",
description: "User ID or mail",
},
{
input: "text",
type: "integer",
operators: [
"equal",
],
unique: true,
id: "authkey_id",
label: "Authentication key ID",
},
{
input: "text",
type: "string",
operators: [
"equal",
],
unique: true,
id: "request_id",
label: "Request ID",
description: "Request ID from X-Request-ID HTTP header",
},
{
input: "text",
type: "string",
operators: [
"equal",
],
unique: true,
id: "org",
label: "Organisation",
description: "Organisation ID, UUID or name",
},
{
input: "select",
type: "string",
operators: [
"equal",
],
unique: true,
id: "request_method",
label: "HTTP request method",
values: ["GET", "HEAD", "POST", "PUT", "DELETE", "OPTIONS", "TRACE", "PATCH"],
},
{
input: "text",
type: "integer",
operators: [
"equal",
],
unique: true,
id: "response_code",
label: "HTTP response code",
validation: {
min: 100,
max: 599
}
},
{
input: "text",
type: "string",
operators: [
"equal",
],
unique: true,
id: "controller",
label: "Controller",
},
{
input: "text",
type: "string",
operators: [
"equal",
],
unique: true,
id: "action",
label: "Action",
},
{
input: "text",
type: "string",
operators: [
"contains",
],
unique: true,
id: "url",
label: "URL",
},
{
input: "text",
type: "string",
operators: [
"contains",
],
unique: true,
id: "user_agent",
label: "User agent",
},
{
type: "double",
operators: [
"greater_or_equal",
],
unique: true,
id: "memory_usage",
label: "Memory usage",
description: "In MB",
validation: {
min: 0,
step: 0.01
}
},
{
type: "double",
operators: [
"greater_or_equal",
],
unique: true,
id: "duration",
label: "Duration",
description: "In milliseconds (1 second is equal to 1000 milliseconds)",
validation: {
min: 0,
}
},
{
type: "integer",
operators: [
"greater_or_equal",
],
unique: true,
id: "query_count",
label: "Query count",
description: "Number of SQL queries",
validation: {
min: 0,
}
}
]
],
rules: {
condition: 'AND',
not: false,
rules: <?= JsonTool::encode($qbRules) ?>,
flags: {
no_add_group: true,
condition_readonly: true,
}
},
icons: {
add_group: 'fa fa-plus-square',
add_rule: 'fa fa-plus-circle',
remove_group: 'fa fa-minus-square',
remove_rule: 'fa fa-minus-circle',
error: 'fa fa-exclamation-triangle'
}
};
$(function() {
var $builder = $('#builder');
// Fix for Bootstrap Datepicker
$builder.on('afterUpdateRuleValue.queryBuilder', function(e, rule) {
if (rule.filter.plugin === 'datepicker') {
rule.$el.find('.rule-value-container input').datepicker('update');
}
});
var queryBuilder = $builder.queryBuilder(qbOptions);
queryBuilder = queryBuilder[0].queryBuilder;
$('#qbClear').off('click').on('click', function() {
queryBuilder.reset();
});
// Submit on enter
$builder.on('keyup', 'input[type=text], select', function(event) {
if (event.keyCode === 13) {
$('#qbSubmit').click();
}
});
$('#qbSubmit').off('click').on('click', function() {
var rules = queryBuilder.getRules({
skip_empty: true
});
passedArgs = [];
for (var key in rules.rules) {
var rule = rules.rules[key];
var k = rule.id;
var v = rule.value;
if (Array.isArray(v)) {
v = v.join('||');
}
passedArgs[k] = v;
}
var url = here;
for (var key in passedArgs) {
if (typeof key === 'number') {
url += "/" + passedArgs[key];
} else if (key !== 'page') {
url += "/" + key + ":" + encodeURIComponent(passedArgs[key]);
}
}
window.location.href = url;
});
});
</script>
<?php
// $paginator = $this->LightPaginator->prev('&laquo; ' . __('previous'), array('escape' => false), null, array('class' => 'prev disabled', 'escape' => false, 'disabledTag' => 'span'));
// $paginator .= $this->LightPaginator->numbers(array('modulus' => 20, 'separator' => '', 'tag' => 'li', 'currentClass' => 'active', 'currentTag' => 'span'));
// $paginator .= $this->LightPaginator->next(__('next') . ' &raquo;', array('tag' => 'li', 'escape' => false), null, array('tag' => 'li', 'class' => 'next disabled', 'escape' => false, 'disabledTag' => 'span'));
echo sprintf(
'<nav aria-label="%s"><div class="pagination"><ul class="pagination">%s%s%s</ul></div></nav>',
__(''),
$this->LightPaginator->prev('&laquo; ' . __('previous'), array('escape' => false), null, array('class' => 'prev disabled', 'escape' => false, 'disabledTag' => 'span')),
$this->LightPaginator->numbers(array('modulus' => 20, 'separator' => '', 'tag' => 'li', 'currentClass' => 'active', 'currentTag' => 'span')),
$this->LightPaginator->next(__('next') . ' &raquo;', array('tag' => 'li', 'escape' => false), null, array('tag' => 'li', 'class' => 'next disabled', 'escape' => false, 'disabledTag' => 'span'))
);
?>
<table class="table table-striped table-hover table-condensed">
<tr>
<th><?= $this->LightPaginator->sort('created') ?></th>
<th><?= $this->LightPaginator->sort('user_id', __('User')) ?></th>
<th><?= $this->LightPaginator->sort('ip', __('IP')) ?></th>
<th><?= $this->LightPaginator->sort('org_id', __('Org')) ?></th>
<th><?= $this->LightPaginator->sort('request_method', __('Request')) ?></th>
<th><?= $this->LightPaginator->sort('url', __('URL')) ?></th>
<th title="<?= __('HTTP response code') ?>"><?= $this->LightPaginator->sort('response_code', __('Code')) ?></th>
<th title="<?= __('Memory used during responding to request') ?>"><?= $this->LightPaginator->sort('memory_usage', __('Memory')) ?></th>
<th title="<?= __('Time used during responding to request') ?>"><?= $this->LightPaginator->sort('duration', __('Duration')) ?></th>
<th title="<?= __('SQL database query count') ?>"><?= $this->LightPaginator->sort('query_count', __('Queries')) ?></th>
</tr>
<?php foreach ($list as $item) : ?>
<tr>
<td class="short"><?= $this->Time->format($item['created']); ?></td>
<td class="short" data-search="user" data-search-value="<?= h($item['user_id']) ?>"><?php
if (isset($item['User']['email'])) {
echo '<a href="' . $baseurl . '/admin/users/view/' . h($item['User']['id']) . '">' . h($item['User']['email']) . '</a>';
} else {
echo __('<i>Deleted user #%s</i>', h($item['user_id']));
}
if (!empty($item['authkey_id'])) {
echo ' <i class="fas fa-cogs" title="' . __('Request trough API by auth key #%s', h($item['authkey_id'])) . '"></i>';
}
?></td>
<td class="short" data-search="ip" data-search-value="<?= h($item['ip']) ?>"><?= h($item['ip']) ?></td>
<td class="short" data-search="org" data-search-value="<?= h($item['org_id']) ?>">
<?php if (isset($item['Organisation']) && $item['Organisation']['id']) {
echo $this->OrgImg->getOrgLogo($item->toArray(), 24);
} else if ($item['org_id'] != 0) {
echo __('<i>Deleted org #%s</i>', h($item['org_id']));
}
?>
</td>
<td class="short" data-search="request_method" data-search-value="<?= h($item['request_method']) ?>">
<span title="<?= __("User agent: %s\nRequest ID: %s", h($item['user_agent']), h($item['request_id'])) ?>"><?= h($item['request_method']) ?></span>
<?= in_array($item['request_method'], ['POST', 'PUT']) ? ' <a href="#" class="far fa-file request" title="' . __('Show HTTP request') . '" data-log-id="' . h($item['id']) . '"></i>' : '' ?>
</td>
<td class="short" data-search="controller:action" data-search-value="<?= h($item['controller']) . ':' . h($item['action']) ?>" title="<?= __('Controller: %s, action: %s', h($item['controller']), h($item['action'])) ?>"><?= h($item['url']) ?></td>
<td class="short" data-search="response_code" data-search-value="<?= h($item['response_code']) ?>"><?= h($item['response_code']) ?></td>
<td class="short"><?= $this->Number->toReadableSize($item['memory_usage']) ?></td>
<td class="short"><?= $item['duration'] ?> ms</td>
<td class="short"><?= $item['query_count'] . ($item['has_query_log'] ? ' <a href="#" class="fas fa-database query-log" title="' . __('Show SQL queries') . '" data-log-id="' . h($item['id']) . '"></i>' : '') ?>
</td>
</tr>
<?php endforeach; ?>
</table>
<div class="pagination">
<ul>
<?= $paginator ?>
</ul>
</div>
</div>
<script>
var passedArgs = <?= $passedArgs ?>;
$('.request').click(function(e) {
e.preventDefault();
var id = $(this).data('log-id');
$.get(baseurl + "/admin/access_logs/request/" + id, function(data) {
var $popoverFormLarge = $('#popover_form_large');
$popoverFormLarge.html(data);
openPopup($popoverFormLarge);
}).fail(xhrFailCallback);
return false;
});
$('.query-log').click(function(e) {
e.preventDefault();
var id = $(this).data('log-id');
$.get(baseurl + "/admin/access_logs/queryLog/" + id, function(data) {
var $popoverFormLarge = $('#popover_form_large');
$popoverFormLarge.html(data);
openPopup($popoverFormLarge);
}).fail(xhrFailCallback);
return false;
});
$(function() {
filterSearch(function(e, searchKey, searchValue) {
if (searchKey === 'controller:action') {
var val = searchValue.split(":");
passedArgs['controller'] = encodeURIComponent(val[0]);
passedArgs['action'] = encodeURIComponent(val[1]);
} else {
passedArgs[searchKey] = encodeURIComponent(searchValue);
}
var url = here;
for (var key in passedArgs) {
if (typeof key === 'number') {
url += "/" + passedArgs[key];
} else if (key !== 'page') {
url += "/" + key + ":" + passedArgs[key];
}
}
window.location.href = url;
});
});
</script>
<?php
// TODO: [3.x-MIGRATION]
// echo $this->element('/genericElements/SideMenu/side_menu', ['menuList' => 'logs', 'menuItem' => 'listAccessLogs']);
'fields' => $fields,
'title' => empty($ajax) ? __('Access Logs') : false
]
]
);
// TODO: [3.x-MIGRATION] add ajax dialog for request body, sql queries and human readable memory usage and duration

View File

@ -1,14 +0,0 @@
<table class="table table-striped table-hover table-condensed">
<tr>
<th><?= __('Query') ?></th>
<th><?= __('Num. rows') ?></th>
<th><?= __('Took (ms)') ?></th>
</tr>
<?php foreach ($queryLog['log'] as $query): ?>
<tr>
<td><?= h($query['query']) ?></td>
<td><?= h($query['numRows']) ?></td>
<td><?= h($query['took']) ?></td>
</tr>
<?php endforeach; ?>
</table>

View File

@ -1 +0,0 @@
<div style="padding: 1em; background: white; word-wrap: break-word; white-space: pre-wrap"><?= $request ?></div>