new: [Dashboard] system

- Dashboard
  - modular similar to restSearch
  - build your own widgets
  - use a set of visualisation options (more coming!)
  - full access to internal functions for queries
  - auto discover core and 3rd party widgets
  - rearrange / configure widgets for each user individually
  - rearrange / resize widgets
  - settings can be configured by a site-admin on behalf of others
  - modules have a self-explain mode to guide users
  - caching mechanism for the modules / org

- set homepage / user
- various other fixes
pull/5635/head
iglocska 2020-03-01 18:05:21 +01:00
parent 4bfcc3211b
commit 0d4df7c98b
No known key found for this signature in database
GPG Key ID: BEA224F1FEF113AC
27 changed files with 1239 additions and 50 deletions

1
.gitignore vendored
View File

@ -103,3 +103,4 @@ tools/mkdocs
.ropeproject/
vagrant/.vagrant/
vagrant/*.log
/app/Lib/Dashboard/Custom/*

View File

@ -46,7 +46,7 @@ class AppController extends Controller
public $helpers = array('Utility', 'OrgImg', 'FontAwesome', 'UserName');
private $__queryVersion = '98';
private $__queryVersion = '99';
public $pyMispVersion = '2.4.122';
public $phpmin = '7.2';
public $phprec = '7.4';
@ -510,6 +510,18 @@ class AppController extends Controller
}
}
$this->components['RestResponse']['sql_dump'] = $this->sql_dump;
$this->loadModel('UserSetting');
$homepage = $this->UserSetting->find('first', array(
'recursive' => -1,
'conditions' => array(
'UserSetting.user_id' => $this->Auth->user('id'),
'UserSetting.setting' => 'homepage'
),
'contain' => array('User.id', 'User.org_id')
));
if (!empty($homepage)) {
$this->set('homepage', $homepage['UserSetting']['value']);
}
}
private function __rateLimitCheck()

View File

@ -71,6 +71,13 @@ class ACLComponent extends Component
'view' => array('*'),
'viewPicture' => array('*'),
),
'dashboards' => array(
'getForm' => array('*'),
'index' => array('*'),
'updateSettings' => array('*'),
'getEmptyWidget' => array('*'),
'renderWidget' => array('*')
),
'decayingModel' => array(
"update" => array(),
"export" => array('*'),
@ -575,7 +582,8 @@ class ACLComponent extends Component
'view' => array('*'),
'setSetting' => array('*'),
'getSetting' => array('*'),
'delete' => array('*')
'delete' => array('*'),
'setHomePage' => array('*')
),
'warninglists' => array(
'checkValue' => array('perm_auth'),

View File

@ -0,0 +1,155 @@
<?php
App::uses('AppController', 'Controller');
class DashboardsController extends AppController
{
public $components = array('Session', 'RequestHandler');
public function beforeFilter()
{
parent::beforeFilter();
$this->Security->unlockedActions = array('renderWidget', 'updateSettings', 'getForm');
}
public $paginate = array(
'limit' => 60,
'maxLimit' => 9999
);
public function index()
{
$this->loadModel('UserSetting');
$params = array(
'conditions' => array(
'UserSetting.user_id' => $this->Auth->user('id'),
'UserSetting.setting' => 'dashboard'
)
);
$userSettings = $this->UserSetting->find('first', $params);
if (empty($userSettings)) {
$userSettings = array(
'UserSetting' => array(
'setting' => 'dashboard',
'value' => array(
array(
'widget' => 'MispStatusWidget',
'config' => array(
),
'position' => array(
'x' => 0,
'y' => 0,
'width' => 2,
'height' => 2
)
)
)
)
);
}
$widgets = array();
foreach ($userSettings['UserSetting']['value'] as $widget) {
$dashboardWidget = $this->Dashboard->loadWidget($widget['widget']);
$widget['width'] = $dashboardWidget->width;
$widget['height'] = $dashboardWidget->height;
$widget['title'] = $dashboardWidget->title;
$widgets[] = $widget;
}
$this->layout = 'dashboard';
$this->set('widgets', $widgets);
}
public function getForm($action = 'edit')
{
if ($this->request->is('post') || $this->request->is('put')) {
$data = $this->request->data;
if ($action === 'edit' && !isset($data['widget'])) {
throw new InvalidArgumentException(__('No widget name passed.'));
}
if (empty($data['config'])) {
$data['config'] = '';
}
if ($action === 'add') {
$data['widget_options'] = $this->Dashboard->loadAllWidgets();
} else {
$dashboardWidget = $this->Dashboard->loadWidget($data['widget']);
$data['description'] = empty($dashboardWidget->description) ? '' : $dashboardWidget->description;
$data['params'] = empty($dashboardWidget->params) ? array() : $dashboardWidget->params;
$data['params'] = array_merge(array('alias' => __('Alias to use as the title of the widget')), $data['params']);
}
$this->set('data', $data);
$this->layout = false;
$this->render($action);
}
}
public function updateSettings()
{
if ($this->request->is('post')) {
$this->UserSetting = ClassRegistry::init('UserSetting');
if (!isset($this->request->data['value'])) {
throw new InvalidArgumentException(__('No setting data found.'));
}
$data = array(
'UserSetting' => array(
'user_id' => $this->Auth->user('id'),
'setting' => 'dashboard',
'value' => $this->request->data['value']
)
);
$result = $this->UserSetting->setSetting($this->Auth->user(), $data);
if ($result) {
return $this->RestResponse->saveSuccessResponse('Dashboard', 'updateSettings', false, false, __('Settings updated.'));
}
return $this->RestResponse->saveFailResponse('Dashboard', 'updateSettings', false, $this->UserSetting->validationErrors, $this->response->type());
}
}
public function getEmptyWidget($widget, $k = 1)
{
$dashboardWidget = $this->Dashboard->loadWidget($widget);
if (empty($dashboardWidget)) {
throw new NotFoundException(__('Invalid widget.'));
}
$this->layout = false;
$widget = array(
'config' => isset($dashboardWidget->config) ? $dashboardWidget->height : '',
'title' => $dashboardWidget->title,
'alias' => isset($dashboardWidget->alias) ? $dashboardWidget->alias : $dashboardWidget->title,
'widget' => $widget
);
$this->set('k', $k);
$this->set('widget', $widget);
}
public function renderWidget($force = false)
{
if ($this->request->is('post')) {
if (empty($this->request->data['data'])) {
$this->request->data = array('data' => $this->request->data);
}
if (empty($this->request->data['data'])) {
throw new MethodNotAllowedException(__('You need to specify the widget to use along with the configuration.'));
}
$value = $this->request->data['data'];
$dashboardWidget = $this->Dashboard->loadWidget($value['widget']);
$this->layout = false;
$this->set('title', $dashboardWidget->title);
$redis = $this->Dashboard->setupRedis();
$org_scope = $this->_isSiteAdmin() ? 0 : $this->Auth->user('org_id');
$lookup_hash = hash('sha256', $value['widget'] . $value['config']);
$data = $redis->get('misp:dashboard:' . $org_scope . ':' . $lookup_hash);
if (empty($data)) {
$data = $dashboardWidget->handler($this->Auth->user(), json_decode($value['config'], true));
$redis->set('misp:dashboard:' . $org_scope . ':' . $lookup_hash, json_encode(array('data' => $data)));
$redis->expire('misp:dashboard:' . $org_scope . ':' . $lookup_hash, 60);
} else {
$data = json_decode($data, true)['data'];
}
$this->set('data', $data);
$this->render('/Dashboards/Widgets/' . $dashboardWidget->render);
} else {
throw new MethodNotAllowedException(__('This endpoint can only be reached via POST requests.'));
}
}
}

View File

@ -25,6 +25,12 @@ class UserSettingsController extends AppController
)
);
public function beforeFilter()
{
parent::beforeFilter();
$this->Security->unlockedActions = array('setHomePage');
}
public function index()
{
$filterData = array(
@ -172,49 +178,7 @@ class UserSettingsController extends AppController
$userSetting = array(
'user_id' => $this->Auth->user('id')
);
if (!empty($this->request->data['UserSetting']['user_id']) && is_numeric($this->request->data['UserSetting']['user_id'])) {
$user = $this->UserSetting->User->find('first', array(
'recursive' => -1,
'conditions' => array('User.id' => $this->request->data['UserSetting']['user_id']),
'fields' => array('User.org_id')
));
if (
$this->_isSiteAdmin() ||
($this->_isAdmin() && ($user['User']['org_id'] == $this->Auth->user('org_id')))
) {
$userSetting['user_id'] = $this->request->data['UserSetting']['user_id'];
}
}
if (empty($this->request->data['UserSetting']['setting']) || !isset($this->request->data['UserSetting']['setting'])) {
throw new MethodNotAllowedException(__('This endpoint expects both a setting and a value to be set.'));
}
if (!$this->UserSetting->checkSettingValidity($this->request->data['UserSetting']['setting'])) {
throw new MethodNotAllowedException(__('Invalid setting.'));
}
$settingPermCheck = $this->UserSetting->checkSettingAccess($this->Auth->user(), $this->request->data['UserSetting']['setting']);
if ($settingPermCheck !== true) {
throw new MethodNotAllowedException(__('This setting is restricted and requires the following permission(s): %s', $settingPermCheck));
}
$userSetting['setting'] = $this->request->data['UserSetting']['setting'];
if ($this->request->data['UserSetting']['value'] !== '') {
$userSetting['value'] = json_encode(json_decode($this->request->data['UserSetting']['value'], true));
} else {
$userSetting['value'] = '';
}
$existingSetting = $this->UserSetting->find('first', array(
'recursive' => -1,
'conditions' => array(
'UserSetting.user_id' => $userSetting['user_id'],
'UserSetting.setting' => $userSetting['setting']
)
));
if (empty($existingSetting)) {
$this->UserSetting->create();
} else {
$userSetting['id'] = $existingSetting['UserSetting']['id'];
}
// save the setting
$result = $this->UserSetting->save(array('UserSetting' => $userSetting));
$result = $this->UserSetting->setSetting($this->Auth->user(), $this->request->data);
if ($result) {
// if we've managed to save our setting
if ($this->_isRest()) {
@ -358,4 +322,26 @@ class UserSettingsController extends AppController
throw new MethodNotAllowedException(__('Expecting POST or DELETE request.'));
}
}
public function setHomePage()
{
if (!$this->request->is('post')) {
throw new MethodNotAllowedException(__('This endpoint only aaccepts POST requests.'));
}
if (empty($this->request->data['path'])) {
$this->request->data = array('path' => $this->request->data);
}
if (empty($this->request->data['path'])) {
throw new InvalidArgumentException(__('No path POSTed.'));
}
$setting = array(
'UserSetting' => array(
'user_id' => $this->Auth->user('id'),
'setting' => 'homepage',
'value' => json_encode(array('path' => $this->request->data['path']))
)
);
$result = $this->UserSetting->setSetting($this->Auth->user(), $setting);
return $this->RestResponse->saveSuccessResponse('UserSettings', 'setHomePage', false, $this->response->type(), 'Homepage set to ' . $this->request->data['path']);
}
}

View File

@ -1195,7 +1195,19 @@ class UsersController extends AppController
// Events list
$url = $this->Session->consume('pre_login_requested_url');
if (empty($url)) {
$url = array('controller' => 'events', 'action' => 'index');
$homepage = $this->User->UserSetting->find('first', array(
'recursive' => -1,
'conditions' => array(
'UserSetting.user_id' => $this->Auth->user('id'),
'UserSetting.setting' => 'homepage'
),
'contain' => array('User.id', 'User.org_id')
));
if (!empty($homepage)) {
$url = $homepage['UserSetting']['value']['path'];
} else {
$url = array('controller' => 'events', 'action' => 'index');
}
}
$this->redirect($url);
}

View File

@ -0,0 +1,73 @@
<?php
class MispStatusWidget
{
public $title = 'MISP Status';
public $render = 'SimpleList';
public $width = 2;
public $height = 2;
public $params = array();
public $description = 'Basic widget showing some user related MISP notifications.';
public function handler($user, $options = array())
{
$this->Event = ClassRegistry::init('Event');
// the last login in the session is not updated after the login - only in the db, so let's fetch it.
$lastLogin = $user['last_login'];
$data = array();
$data[] = array(
'title' => __('Events modified'),
'value' => count($this->Event->fetchEventIds($user, false, false, false, true, $lastLogin)),
'html' => sprintf(
' (<a href="%s">%s</a>)',
Configure::read('MISP.baseurl') . '/events/index/timestamp:' . (time() - 86400),
'View'
)
);
$data[] = array(
'title' => __('Events published'),
'value' => count($this->Event->fetchEventIds($user, false, false, false, true, false, $lastLogin)),
'html' => sprintf(
' (<a href="%s">%s</a>)',
Configure::read('MISP.baseurl') . '/events/index/published:1/timestamp:' . (time() - 86400),
'View'
)
);
$notifications = $this->Event->populateNotifications($user);
if (!empty($notifications['proposalCount'])) {
$data[] = array(
'title' => __('Pending proposals'),
'value' => $notifications['proposalCount'],
'html' => sprintf(
' (<a href="%s">%s</a>)',
Configure::read('MISP.baseurl') . '/shadow_attributes/index/all:0',
'View'
)
);
}
if (!empty($notifications['proposalEventCount'])) {
$data[] = array(
'title' => __('Events with proposals'),
'value' => $notifications['proposalEventCount'],
'html' => sprintf(
' (<a href="%s">%s</a>)',
Configure::read('MISP.baseurl') . '/events/proposalEventIndex',
'View'
)
);
}
if (!empty($notifications['delegationCount'])) {
$data[] = array(
'title' => __('Delegation requests'),
'value' => $notifications['delegationCount'],
'html' => sprintf(
' (<a href="%s">%s</a>)',
Configure::read('MISP.baseurl') . '/event_delegations/index/context:pending',
'View'
)
);
}
return $data;
}
}

View File

@ -0,0 +1,77 @@
<?php
class TrendingTagsWidget
{
public $title = 'Trending Tags';
public $render = 'BarChart';
public $width = 3;
public $height = 4;
public $params = array(
'time_window' => 'The time window, going back in seconds, that should be included.',
'exclude' => 'List of substrings to exclude tags by - for example "sofacy" would exclude any tag containing sofacy.',
'include' => 'List of substrings to include tags by - for example "sofacy" would include any tag containing sofacy.'
);
public $placeholder =
'{
"time_window": "86400",
"exclude": ["tlp:", "pap:"],
"include": ["misp-galaxy:", "my-internal-taxonomy"]
}';
public $description = 'Widget showing the trending tags over the past x seconds, along with the possibility to include/exclude tags.';
public function handler($user, $options = array())
{
$this->Event = ClassRegistry::init('Event');
$params = array(
'metadata' => 1,
'timestamp' => time() - (empty($options['time_window']) ? 8640000 : $options['time_window'])
);
$eventIds = $this->Event->filterEventIds($user, $params);
$params['eventid'] = $eventIds;
$events = array();
if (!empty($eventIds)) {
$events = $this->Event->fetchEvent($user, $params);
}
$tags = array();
$tagColours = array();
$rules['exclusions'] = empty($options['exclude']) ? array() : $options['exclude'];
$rules['inclusions'] = empty($options['exclude']) ? array() : $options['exclude'];
foreach ($events as $event) {
foreach ($event['EventTag'] as $et) {
if ($this->checkTag($options, $et['Tag']['name'])) {
if (empty($tags[$et['Tag']['name']])) {
$tags[$et['Tag']['name']] = 1;
$tagColours[$et['Tag']['name']] = $et['Tag']['colour'];
} else {
$tags[$et['Tag']['name']] += 1;
}
}
}
}
arsort($tags);
$data['data'] = array_slice($tags, 0, 10);
$data['colours'] = $tagColours;
return $data;
}
private function checkTag($options, $tag)
{
if (!empty($options['exclude'])) {
foreach ($options['exclude'] as $exclude) {
if (strpos($tag, $exclude) !== false) {
return false;
}
}
}
if (!empty($options['include'])) {
foreach ($options['include'] as $include) {
if (strpos($tag, $include) !== false) {
return true;
}
}
return false;
} else {
return true;
}
}
}

50
app/Model/Dashboard.php Normal file
View File

@ -0,0 +1,50 @@
<?php
App::uses('AppModel', 'Model');
class Dashboard extends AppModel
{
public $useTable = false;
public function loadWidget($name)
{
if (file_exists(APP . 'Lib/Dashboard/' . $name . '.php')) {
App::uses($name, 'Dashboard');
} else if (file_exists(APP . 'Lib/Dashboard/Custom/' . $name . '.php')) {
App::uses($name, 'Dashboard/Custom');
} else {
throw new NotFoundException(__('Invalid widget or widget not found.'));
}
$widget = new $name();
return $widget;
}
public function loadAllWidgets()
{
$dir = new Folder(APP . 'Lib/Dashboard');
$customdir = new Folder(APP . 'Lib/Dashboard/Custom');
$widgetFiles = $dir->find('.*Widget\.php');
$customWidgetFiles = $customdir->find('.*Widget\.php');
$widgets = array();
foreach ($widgetFiles as $widgetFile) {
$className = substr($widgetFile, 0, strlen($widgetFile) -4);
$widgets[$className] = $this->__extractMeta($className, false);
}
return $widgets;
}
private function __extractMeta($className, $custom)
{
App::uses($className, 'Dashboard' . $custom ? '/Custom' : '');
$widgetClass = new $className();
$widget = array(
'widget' => $className,
'title' => $widgetClass->title,
'render' => $widgetClass->render,
'params' => empty($widgetClass->params) ? array() : $widgetClass->params,
'description' => empty($widgetClass->description) ? $widgetClass->title : $widgetClass->description,
'height' => empty($widgetClass->height) ? 1 : $widgetClass->height,
'width' => empty($widgetClass->width) ? 1 : $widgetClass->width,
'placeholder' => empty($widgetClass->placeholder) ? '' : $widgetClass->placeholder
);
return $widget;
}
}

View File

@ -1717,6 +1717,9 @@ class Event extends AppModel
'recursive' => -1,
'fields' => $fields
);
if (isset($params['order'])) {
$find_params['order'] = $params['order'];
}
if (isset($params['limit'])) {
// Get the count (but not the actual data) of results for paginators
$result_count = $this->find('count', $find_params);

View File

@ -49,6 +49,24 @@ class UserSetting extends AppModel
'dashboard_access' => array(
'placeholder' => 1,
'restricted' => 'perm_site_admin'
),
'dashboard' => array(
'placeholder' => array(
array(
'widget' => 'MispStatusWidget',
'config' => array(
),
'position' => array(
'x' => 0,
'y' => 0,
'width' => 2,
'height' => 2
)
)
)
),
'homepage' => array(
'path' => '/events/index'
)
);
@ -279,4 +297,56 @@ class UserSetting extends AppModel
}
return false;
}
public function setSetting($user, &$data)
{
$userSetting = array();
if (!empty($data['UserSetting']['user_id']) && is_numeric($data['UserSetting']['user_id'])) {
$user_to_edit = $this->User->find('first', array(
'recursive' => -1,
'conditions' => array('User.id' => $data['UserSetting']['user_id']),
'fields' => array('User.org_id')
));
if (
!empty($user['Role']['perm_site_admin']) ||
(!empty($user['Role']['perm_admin']) && ($user_to_edit['User']['org_id'] == $user['org_id']))
) {
$userSetting['user_id'] = $data['UserSetting']['user_id'];
}
}
if (empty($userSetting['user_id'])) {
$userSetting['user_id'] = $user['id'];
}
if (empty($data['UserSetting']['setting']) || !isset($data['UserSetting']['setting'])) {
throw new MethodNotAllowedException(__('This endpoint expects both a setting and a value to be set.'));
}
if (!$this->checkSettingValidity($data['UserSetting']['setting'])) {
throw new MethodNotAllowedException(__('Invalid setting.'));
}
$settingPermCheck = $this->checkSettingAccess($user, $data['UserSetting']['setting']);
if ($settingPermCheck !== true) {
throw new MethodNotAllowedException(__('This setting is restricted and requires the following permission(s): %s', $settingPermCheck));
}
$userSetting['setting'] = $data['UserSetting']['setting'];
if ($data['UserSetting']['value'] !== '') {
$userSetting['value'] = $data['UserSetting']['value'];
} else {
$userSetting['value'] = '';
}
$existingSetting = $this->find('first', array(
'recursive' => -1,
'conditions' => array(
'UserSetting.user_id' => $userSetting['user_id'],
'UserSetting.setting' => $userSetting['setting']
)
));
if (empty($existingSetting)) {
$this->create();
} else {
$userSetting['id'] = $existingSetting['UserSetting']['id'];
}
// save the setting
$result = $this->save(array('UserSetting' => $userSetting));
return true;
}
}

View File

@ -0,0 +1,189 @@
<div class="attributes <?php if (!isset($ajax) || !$ajax) echo 'form';?>">
<?php
$url_params = $action == 'add' ? 'add/' . $event_id : 'edit/' . $attribute['Attribute']['id'];
echo $this->Form->create('Attribute', array('id', 'url' => '/attributes/' . $url_params));
?>
<fieldset>
<legend><?php echo $action == 'add' ? __('Add Attribute') : __('Edit Attribute'); ?></legend>
<div id="formWarning" class="message ajaxMessage"></div>
<div id="compositeWarning" class="message <?php echo !empty($ajax) ? 'ajaxMessage' : '';?>" style="display:none;">Did you consider adding an object instead of a composite attribute?</div>
<div class="add_attribute_fields">
<?php
echo $this->Form->hidden('event_id');
echo $this->Form->input('category', array(
'empty' => __('(choose one)'),
'label' => __('Category ') . $this->element('formInfo', array('type' => 'category')),
));
echo $this->Form->input('type', array(
'empty' => __('(first choose category)'),
'label' => __('Type ') . $this->element('formInfo', array('type' => 'type')),
));
$initialDistribution = 5;
if (Configure::read('MISP.default_attribute_distribution') != null) {
if (Configure::read('MISP.default_attribute_distribution') === 'event') {
$initialDistribution = 5;
} else {
$initialDistribution = Configure::read('MISP.default_attribute_distribution');
}
}
?>
<div class="input clear"></div>
<?php
$distArray = array(
'options' => array($distributionLevels),
'label' => __('Distribution ') . $this->element('formInfo', array('type' => 'distribution')),
);
if ($action == 'add') {
$distArray['selected'] = $initialDistribution;
}
echo $this->Form->input('distribution', $distArray);
?>
<div id="SGContainer" style="display:none;">
<?php
if (!empty($sharingGroups)) {
echo $this->Form->input('sharing_group_id', array(
'options' => array($sharingGroups),
'label' => __('Sharing Group'),
));
}
?>
</div>
<?php
echo $this->Form->input('value', array(
'type' => 'textarea',
'error' => array('escape' => false),
'div' => 'input clear',
'class' => 'input-xxlarge'
));
?>
<div class="input clear"></div>
<?php
echo $this->Form->input('comment', array(
'type' => 'text',
'label' => __('Contextual Comment'),
'error' => array('escape' => false),
'div' => 'input clear',
'class' => 'input-xxlarge'
));
?>
<div class="input clear"></div>
<?php
echo $this->Form->input('to_ids', array(
'label' => __('for Intrusion Detection System'),
'type' => 'checkbox'
));
echo $this->Form->input('batch_import', array(
'type' => 'checkbox'
));
echo '<div class="input clear"></div>';
echo $this->Form->input('disable_correlation', array(
'type' => 'checkbox'
));
?>
</div>
</fieldset>
<p id="notice_message" style="display:none;"></p>
<?php if ($ajax): ?>
<div class="overlay_spacing">
<span id="submitButton" class="btn btn-primary" style="margin-bottom:5px;float:left;" title="<?php echo __('Submit'); ?>" role="button" tabindex="0" aria-label="<?php echo __('Submit'); ?>" onClick="submitPopoverForm('<?php echo $action == 'add' ? $event_id : $attribute['Attribute']['id'];?>', '<?php echo $action; ?>')"><?php echo __('Submit'); ?></span>
<span class="btn btn-inverse" style="float:right;" title="<?php echo __('Cancel'); ?>" role="button" tabindex="0" aria-label="<?php echo __('Cancel'); ?>" id="cancel_attribute_add"><?php echo __('Cancel'); ?></span>
</div>
<?php
else:
?>
<?php
echo $this->Form->button('Submit', array('class' => 'btn btn-primary'));
endif;
echo $this->Form->end();
?>
</div>
<?php
if (!$ajax) {
$event['Event']['id'] = $event_id;
$event['Event']['published'] = $published;
echo $this->element('/genericElements/SideMenu/side_menu', array('menuList' => 'event', 'menuItem' => 'addAttribute', 'event' => $event));
}
?>
<script type="text/javascript">
var notice_list_triggers = <?php echo $notice_list_triggers; ?>;
var fieldsArray = new Array('AttributeCategory', 'AttributeType', 'AttributeValue', 'AttributeDistribution', 'AttributeComment', 'AttributeToIds', 'AttributeBatchImport', 'AttributeSharingGroupId');
<?php
$formInfoTypes = array('distribution' => 'Distribution', 'category' => 'Category', 'type' => 'Type');
echo 'var formInfoFields = ' . json_encode($formInfoTypes) . PHP_EOL;
foreach ($formInfoTypes as $formInfoType => $humanisedName) {
echo 'var ' . $formInfoType . 'FormInfoValues = {' . PHP_EOL;
foreach ($info[$formInfoType] as $key => $formInfoData) {
echo '"' . $key . '": "<span class=\"blue bold\">' . h($formInfoData['key']) . '</span>: ' . h($formInfoData['desc']) . '<br />",' . PHP_EOL;
}
echo '}' . PHP_EOL;
}
?>
//
//Generate Category / Type filtering array
//
var category_type_mapping = new Array();
<?php
foreach ($categoryDefinitions as $category => $def) {
echo "category_type_mapping['" . addslashes($category) . "'] = {";
$first = true;
foreach ($def['types'] as $type) {
if ($first) $first = false;
else echo ', ';
echo "'" . addslashes($type) . "' : '" . addslashes($type) . "'";
}
echo "}; \n";
}
?>
var composite_types = <?php echo json_encode($compositeTypes); ?>;
$(document).ready(function() {
<?php
if ($action == 'edit'):
?>
checkNoticeList('attribute');
<?php
endif;
?>
initPopoverContent('Attribute');
$('#AttributeDistribution').change(function() {
if ($('#AttributeDistribution').val() == 4) $('#SGContainer').show();
else $('#SGContainer').hide();
});
$("#AttributeCategory").on('change', function(e) {
formCategoryChanged('Attribute');
if ($(this).val() === 'Internal reference') {
$("#AttributeDistribution").val('0');
$('#SGContainer').hide();
}
});
$("#AttributeCategory, #AttributeType").change(function() {
checkNoticeList('attribute');
});
$("#AttributeCategory, #AttributeType, #AttributeDistribution").change(function() {
var start = $("#AttributeType").val();
initPopoverContent('Attribute');
$("#AttributeType").val(start);
if ($.inArray(start, composite_types) > -1) {
$('#compositeWarning').show();
} else {
$('#compositeWarning').hide();
}
});
<?php if ($ajax): ?>
$('#cancel_attribute_add').click(function() {
cancelPopoverForm();
});
<?php endif; ?>
});
</script>
<?php echo $this->Js->writeBuffer(); // Write cached scripts

View File

@ -0,0 +1,32 @@
<table style="border-spacing:0px;">
<?php
if (!empty($data['logarithmic'])) {
$max = max($data['logarithmic']);
} else {
$max = max($data['data']);
}
foreach ($data['data'] as $entry => $count) {
$value = $count;
if (!empty($data['logarithmic'])) {
$value = $data['logarithmic'][$entry];
}
echo sprintf(
'<tr><td style="%s">%s</td><td style="%s">%s</td></tr>',
'text-align:right;width:33%;',
h($entry),
'width:100%',
sprintf(
'<div title="%s" style="%s">%s</div>',
h($entry) . ': ' . h($count),
sprintf(
'background-color:%s; width:%s; color:white; text-align:center;',
(empty($data['colours'][$entry]) ? '#0088cc' : h($data['colours'][$entry])),
100 * h($value) / $max . '%;'
),
h($count)
),
'&nbsp;'
);
}
?>
</table>

View File

@ -0,0 +1,9 @@
<?php
foreach ($data as $element) {
echo sprintf(
'<div><span class="bold">%s</span>: <span class="blue">%s</span>%s</div>',
h($element['title']),
empty($element['value']) ? '' : h($element['value']),
empty($element['html']) ? '' : $element['html']
);
}

View File

@ -0,0 +1,90 @@
<?php
$modelForForm = 'Dashboard';
$paramsHtml = '';
if (!empty($data['params'])) {
foreach ($data['params'] as $param => $desc) {
$paramsHtml .= sprintf(
'<span class="bold">%s</span>: %s<br />',
h($param),
h($desc)
);
}
}
echo $this->element('genericElements/Form/genericForm', array(
'form' => $this->Form,
'url' => 'updateSettings',
'data' => array(
'title' => __('Add Widget'),
'model' => 'Dashboard',
'fields' => array(
array(
'field' => 'widget',
'class' => 'input span6',
'options' => Hash::combine($data['widget_options'], '{s}.widget', '{s}.title')
),
array(
'field' => 'width',
'class' => 'input',
'type' => 'number',
'default' => 1,
'stayInLine' => 1
),
array(
'field' => 'height',
'type' => 'number',
'class' => 'input',
'default' => 1
),
array(
'field'=> 'config',
'type' => 'textarea',
'class' => 'input span6',
'div' => 'input clear',
'label' => __('Config')
)
),
'submit' => array(
'action' => 'edit',
'ajaxSubmit' => sprintf(
"submitDashboardAddWidget()"
)
),
'description' => '<p class="black widget-description"><span></p><p class="bold">Parameters</p><p class="widget-parameters"></p>'
)
));
?>
<script type="text/javascript">
var widget_options = <?= json_encode($data['widget_options']) ?>;
function setDashboardWidgetChoice() {
var current_choice = $('#DashboardWidget').val();
var current_widget_data = widget_options[current_choice];
$('#DashboardWidth').val(current_widget_data['width']);
$('.widget-description').text(current_widget_data['description']);
$('#DashboardHeight').val(current_widget_data['height']);
$('#DashboardConfig').attr('placeholder', current_widget_data['placeholder']);
$('.widget-parameters').empty();
$.each(current_widget_data['params'], function(index,value) {
$('.widget-parameters').append(
$('<span>')
.attr('class', 'bold')
.text(index)
).append(
$('<span>')
.text(': ' + value)
).append(
$('<br>')
)
});
//$('#DashboardConfig').val(JSON.stringify(current_widget_data['params'], null, 2));
}
$('#DashboardWidget').change(function() {
setDashboardWidgetChoice();
});
$(document).ready(function() {
setDashboardWidgetChoice();
});
</script>
<?php echo $this->Js->writeBuffer(); // Write cached scripts

View File

@ -0,0 +1,43 @@
<?php
$modelForForm = 'Dashboard';
$paramsHtml = '';
if (!empty($data['params'])) {
foreach ($data['params'] as $param => $desc) {
$paramsHtml .= sprintf(
'<span class="bold">%s</span>: %s<br />',
h($param),
h($desc)
);
}
}
echo $this->element('genericElements/Form/genericForm', array(
'form' => $this->Form,
'url' => 'updateSettings',
'data' => array(
'title' => __('Edit Widget'),
'model' => 'Dashboard',
'fields' => array(
array(
'field'=> 'config',
'type' => 'textarea',
'class' => 'input span6',
'div' => 'input clear',
'label' => __('Config'),
'default' => empty($data['config']) ? '' : json_encode($data['config'], JSON_PRETTY_PRINT)
)
),
'submit' => array(
'action' => 'edit',
'ajaxSubmit' => sprintf(
"submitDashboardForm('%s')",
h($data['id'])
)
),
'description' => sprintf(
'<p class="black">%s<span></p><p class="bold">Parameters</p><p>%s</p>',
h($data['description']),
$paramsHtml
)
)
));
?>

View File

@ -0,0 +1 @@
<?= $this->element('/dashboard/widget', array('widget' => $widget, 'k' => $k)); ?>

View File

@ -0,0 +1,50 @@
<div class="index">
<div class="grid-stack" data-gs-min-row:"10">
<?php
$layout = '';
foreach ($widgets as $k => $widget) {
$layout .= $this->element('/dashboard/widget', array('widget' => $widget, 'k' => $k));
}
echo $layout;
?>
</div>
<div class="hidden" id="last-element-counter" data-element-counter="<?= h($k) ?>"></div>
</div>
<?php
echo $this->element('/genericElements/SideMenu/side_menu', array('menuList' => 'dashboard', 'menuItem' => 'dashboardIndex'));
?>
<script type="text/javascript">
function resetDashboardGrid(grid) {
$('.grid-stack-item').each(function() {
updateDashboardWidget(this);
});
saveDashboardState();
$('.edit-widget').click(function() {
el = $(this).closest('.grid-stack-item');
data = {
id: el.attr('id'),
config: JSON.parse(el.attr('config')),
widget: el.attr('widget'),
alias: el.attr('alias')
}
openGenericModalPost(baseurl + '/dashboards/getForm/edit', data);
});
$('.remove-widget').click(function() {
el = $(this).closest('.grid-stack-item');
grid.removeWidget(el);
saveDashboardState();
});
}
$(document).ready(function () {
var grid = GridStack.init();
resetDashboardGrid(grid);
grid.on('change', function(event, items) {
saveDashboardState();
});
grid.on('added', function(event, items) {
resetDashboardGrid(grid);
});
});
</script>

View File

@ -0,0 +1,26 @@
<?php
echo sprintf(
'<div id="widget_%s" class="grid-stack-item" data-gs-x="%s" data-gs-y="%s" data-gs-width="%s" data-gs-height="%s" style="%s" config="%s" widget="%s">%s<div class="widget-data">&nbsp;</div></div>',
h($k),
isset($widget['position']['x']) ? h($widget['position']['x']) : 1,
isset($widget['position']['y']) ? h($widget['position']['y']) : 1,
isset($widget['position']['width']) ? h($widget['position']['width']) : 2,
isset($widget['position']['height']) ? h($widget['position']['height']) : 2,
'border: 1px solid #0088cc;',
empty($widget['config']) ? '[]' : h(json_encode($widget['config'])),
h($widget['widget']),
sprintf(
'<div class="grid-stack-item-content"><div class="widgetTitle"><span class="widgetTitleText">%s</span> %s %s</div><div class="widgetContent">%s</div></div>',
empty($widget['config']['alias']) ? h($widget['title']) : h($widget['config']['alias']),
sprintf(
'<span class="fas fa-edit edit-widget" title="%s"></span>',
__('Configure widget')
),
sprintf(
'<span class="fas fa-trash remove-widget" title="%s"></span>',
__('Remove widget')
),
'&nbsp;'
)
);
?>

View File

@ -96,7 +96,8 @@
sprintf(
'<div class="modal-body modal-body-long">%s</div>',
sprintf(
'%s<fieldset>%s%s</fieldset>%s%s',
'%s%s<fieldset>%s%s</fieldset>%s%s',
empty($data['description']) ? '' : $data['description'],
$formCreate,
$ajaxFlashMessage,
$fieldsString,

View File

@ -2,6 +2,22 @@
<ul class="nav nav-list">
<?php
switch ($menuList) {
case 'dashboard':
echo $this->element('/genericElements/SideMenu/side_menu_link', array(
'element_id' => 'dashboardIndex',
'url' => '/dashboards',
'text' => __('View Dashboard')
));
echo $this->element('/genericElements/SideMenu/side_menu_link', array(
'element_id' => 'dashboardAdd',
'url' => '#',
'text' => __('Add Widget'),
'onClick' => array(
'function' => 'openGenericModalPost',
'params' => array($baseurl . '/dashboards/getForm/add')
),
));
break;
case 'event':
$dataEventId = isset($event['Event']['id']) ? h($event['Event']['id']) : 0;
echo '<div id="hiddenSideMenuData" class="hidden" data-event-id="' . $dataEventId . '"></div>';

View File

@ -3,7 +3,7 @@
$menu = array(
array(
'type' => 'root',
'url' => $baseurl . '/',
'url' =>empty($homepage['path']) ? '$baseurl' : $baseurl . h($homepage['path']),
'html' => (Configure::read('MISP.home_logo') ? $logo = '<img src="' . $baseurl . '/img/custom/' . Configure::read('MISP.home_logo') . '" style="height:24px;">' : __('Home'))
),
array(
@ -402,12 +402,17 @@
$menu_right = array(
array(
'type' => 'root',
'url' => $baseurl . '/',
'url' => '#',
'html' => '<span class="fas fa-star" id="setHomePage" onClick="setHomePage();" title="Set the current page as your home page in MISP"></span>'
),
array(
'type' => 'root',
'url' =>empty($homepage['path']) ? '$baseurl' : $baseurl . h($homepage['path']),
'html' => '<span class="logoBlueStatic bold" id="smallLogo">MISP</span>'
),
array(
'type' => 'root',
'url' => '/users/dashboard',
'url' => '/dashboards',
'html' => sprintf(
'<span class="white" title="%s">%s%s&nbsp;&nbsp;&nbsp;%s</span>',
h($me['email']),

View File

@ -0,0 +1,136 @@
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<?php echo $this->Html->charset(); ?>
<meta name="viewport" content="width=device-width" />
<title>
<?php echo $title_for_layout, ' - '. h(Configure::read('MISP.title_text') ? Configure::read('MISP.title_text') : 'MISP'); ?>
</title>
<?php
$css_collection = array(
'bootstrap',
//'bootstrap4',
'bootstrap-datepicker',
'bootstrap-colorpicker',
'famfamfam-flags',
'font-awesome',
'jquery-ui',
'chosen.min',
'main',
'gridstack.min',
array('print', array('media' => 'print'))
);
if (Configure::read('MISP.custom_css')) {
$css_collection[] = preg_replace('/\.css$/i', '', Configure::read('MISP.custom_css'));
}
$js_collection = array(
'jquery',
'misp-touch',
'jquery-ui',
'chosen.jquery.min',
'gridstack.all'
);
echo $this->element('genericElements/assetLoader', array(
'css' => $css_collection,
'js' => $js_collection,
'meta' => 'icon'
));
?>
</head>
<body>
<div id="popover_form" class="ajax_popover_form"></div>
<div id="popover_form_large" class="ajax_popover_form ajax_popover_form_large"></div>
<div id="popover_form_x_large" class="ajax_popover_form ajax_popover_form_x_large"></div>
<div id="popover_matrix" class="ajax_popover_form ajax_popover_matrix"></div>
<div id="popover_box" class="popover_box"></div>
<div id="screenshot_box" class="screenshot_box"></div>
<div id="confirmation_box" class="confirmation_box"></div>
<div id="gray_out" class="gray_out"></div>
<div id="container">
<?php
echo $this->element('global_menu');
$topPadding = '50';
if (!empty($debugMode) && $debugMode != 'debugOff') {
$topPadding = '0';
}
?>
</div>
<div id="flashContainer" style="padding-top:<?php echo $topPadding; ?>px; !important;">
<div id="main-view-container" class="container-fluid ">
<?php
echo $this->Flash->render();
?>
</div>
</div>
<div>
<?php
echo $this->fetch('content');
?>
</div>
<?php
echo $this->element('genericElements/assetLoader', array(
'js' => array(
'bootstrap',
'bootstrap-timepicker',
'bootstrap-datepicker',
'bootstrap-colorpicker',
'misp',
'keyboard-shortcuts'
)
));
echo $this->element('footer');
echo $this->element('sql_dump');
?>
<div id = "ajax_success_container" class="ajax_container">
<div id="ajax_success" class="ajax_result ajax_success"></div>
</div>
<div id = "ajax_fail_container" class="ajax_container">
<div id="ajax_fail" class="ajax_result ajax_fail"></div>
</div>
<div class="loading">
<div class="spinner"></div>
<div class="loadingText"><?php echo __('Loading');?></div>
</div>
<script type="text/javascript">
<?php
if (!isset($debugMode)):
?>
$(window).scroll(function(e) {
$('.actions').css('left',-$(window).scrollLeft());
});
<?php
endif;
?>
var tabIsActive = true;
var baseurl = '<?php echo $baseurl; ?>';
var here = '<?php
if (substr($this->params['action'], 0, 6) === 'admin_') {
echo $baseurl . '/admin/' . h($this->params['controller']) . '/' . h(substr($this->params['action'], 6));
} else {
echo $baseurl . '/' . h($this->params['controller']) . '/' . h($this->params['action']);
}
?>';
$(document).ready(function(){
$(window).blur(function() {
tabIsActive = false;
});
$(window).focus(function() {
tabIsActive = true;
});
<?php
if (!Configure::read('MISP.disable_auto_logout') and $me):
?>
checkIfLoggedIn();
<?php
endif;
?>
if ($('.alert').text().indexOf("$flashErrorMessage") >= 0) {
var flashMessageLink = '<span class="useCursorPointer underline bold" onClick="flashErrorPopover();">here</span>';
$('.alert').html(($('.alert').html().replace("$flashErrorMessage", flashMessageLink)));
}
});
</script>
</body>
</html>

6
app/webroot/css/gridstack.min.css vendored Normal file

File diff suppressed because one or more lines are too long

View File

@ -2515,3 +2515,11 @@ table tr:hover .down-expand-button {
color: white;
border-radius: 3px;
}
.widgetTitle {
font-weight: bold;
color: #0088cc;
width: 100%;
font-size: 125%;
margin:5px;
}

File diff suppressed because one or more lines are too long

View File

@ -1185,6 +1185,22 @@ function openGenericModal(url) {
});
}
function openGenericModalPost(url, body) {
$.ajax({
data: body,
type: "post",
url: url,
success: function (data) {
$('#genericModal').remove();
$('body').append(data);
$('#genericModal').modal();
},
error: function (data, textStatus, errorThrown) {
showMessage('fail', textStatus + ": " + errorThrown);
}
});
}
function submitPopoverForm(context_id, referer, update_context_id, modal, popover_dissmis_id_to_close) {
var url = null;
var context = 'event';
@ -4859,3 +4875,113 @@ function queryDeprecatedEndpointUsage() {
format: 'yyyy-mm-dd',
});
}());
function submitDashboardForm(id) {
var configData = $('#DashboardConfig').val();
if (configData != '') {
configData = JSON.parse(configData);
} else {
configData = {};
}
configData = JSON.stringify(configData);
$('#' + id).attr('config', configData);
updateDashboardWidget($('#' + id));
$('#genericModal').modal('hide');
saveDashboardState();
}
function submitDashboardAddWidget() {
var widget = $('#DashboardWidget').val();
var config = $('#DashboardConfig').val();
var width = $('#DashboardWidth').val();
var height = $('#DashboardHeight').val();
var el = null;
var k = $('#last-element-counter').data('data-element-counter');
$.ajax({
url: baseurl + '/dashboards/getEmptyWidget/' + widget + '/' + (k+1),
type: 'GET',
success: function(data) {
el = data;
var grid = GridStack.init();
grid.addWidget(
el,
{
"width": width,
"height": height,
"autoposition": 1
}
);
},
complete: function(data) {
$('#genericModal').modal('hide');
},
error: function(data) {
handleGenericAjaxResponse({'saved':false, 'errors':['Could not fetch empty widget.']});
}
});
}
function saveDashboardState() {
var dashBoardSettings = [];
$('.grid-stack-item').each(function(index) {
if ($(this).attr('config') !== undefined && $(this).attr('widget') !== undefined) {
var config = $(this).attr('config');
config = JSON.parse(config);
var temp = {
'widget': $(this).attr('widget'),
'config': config,
'position': {
'x': $(this).attr('data-gs-x'),
'y': $(this).attr('data-gs-y'),
'width': $(this).attr('data-gs-width'),
'height': $(this).attr('data-gs-height')
}
};
dashBoardSettings.push(temp);
}
});
$.ajax({
data: {value: dashBoardSettings},
success:function (data, textStatus) {
showMessage('success', 'Dashboard settings saved.');
},
error: function (jqXHR, textStatus, errorThrown) {
showMessage('fail', textStatus + ": " + errorThrown);
},
type: "post",
url: baseurl + '/dashboards/updateSettings',
});
}
function updateDashboardWidget(element) {
var container = $(element).find('.widgetContent');
var titleText = $(element).find('.widgetTitleText');
var temp = JSON.parse($(element).attr('config'));
if (temp['alias'] !== undefined) {
titleText.text(temp['alias']);
}
$.ajax({
type: 'POST',
url: baseurl + '/dashboards/renderWidget',
data: {
config: $(element).attr('config'),
widget: $(element).attr('widget')
},
success:function (data, textStatus) {
container.html(data);
},
});
}
function setHomePage() {
$.ajax({
type: 'POST',
url: baseurl + '/userSettings/setHomePage',
data: {
path: window.location.pathname
},
success:function (data, textStatus) {
showMessage('success', 'Homepage set.');
},
});
}