diff --git a/.gitignore b/.gitignore
index afe719952..69dc56f31 100755
--- a/.gitignore
+++ b/.gitignore
@@ -103,3 +103,4 @@ tools/mkdocs
.ropeproject/
vagrant/.vagrant/
vagrant/*.log
+/app/Lib/Dashboard/Custom/*
diff --git a/app/Controller/AppController.php b/app/Controller/AppController.php
index 2a3dfb7ce..08c52acfd 100755
--- a/app/Controller/AppController.php
+++ b/app/Controller/AppController.php
@@ -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()
diff --git a/app/Controller/Component/ACLComponent.php b/app/Controller/Component/ACLComponent.php
index e6b1520bb..9de4c369d 100644
--- a/app/Controller/Component/ACLComponent.php
+++ b/app/Controller/Component/ACLComponent.php
@@ -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'),
diff --git a/app/Controller/DashboardsController.php b/app/Controller/DashboardsController.php
new file mode 100644
index 000000000..5744ed825
--- /dev/null
+++ b/app/Controller/DashboardsController.php
@@ -0,0 +1,155 @@
+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.'));
+ }
+ }
+}
diff --git a/app/Controller/UserSettingsController.php b/app/Controller/UserSettingsController.php
index ed4faacf4..a3485d919 100644
--- a/app/Controller/UserSettingsController.php
+++ b/app/Controller/UserSettingsController.php
@@ -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']);
+ }
}
diff --git a/app/Controller/UsersController.php b/app/Controller/UsersController.php
index f66d80c57..abc74ad90 100644
--- a/app/Controller/UsersController.php
+++ b/app/Controller/UsersController.php
@@ -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);
}
diff --git a/app/Lib/Dashboard/MispStatusWidget.php b/app/Lib/Dashboard/MispStatusWidget.php
new file mode 100644
index 000000000..0454bc08e
--- /dev/null
+++ b/app/Lib/Dashboard/MispStatusWidget.php
@@ -0,0 +1,73 @@
+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(
+ ' (%s)',
+ 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(
+ ' (%s)',
+ 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(
+ ' (%s)',
+ 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(
+ ' (%s)',
+ Configure::read('MISP.baseurl') . '/events/proposalEventIndex',
+ 'View'
+ )
+ );
+ }
+ if (!empty($notifications['delegationCount'])) {
+ $data[] = array(
+ 'title' => __('Delegation requests'),
+ 'value' => $notifications['delegationCount'],
+ 'html' => sprintf(
+ ' (%s)',
+ Configure::read('MISP.baseurl') . '/event_delegations/index/context:pending',
+ 'View'
+ )
+ );
+ }
+ return $data;
+ }
+}
diff --git a/app/Lib/Dashboard/TrendingTagsWidget.php b/app/Lib/Dashboard/TrendingTagsWidget.php
new file mode 100644
index 000000000..d329d2021
--- /dev/null
+++ b/app/Lib/Dashboard/TrendingTagsWidget.php
@@ -0,0 +1,77 @@
+ '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;
+ }
+ }
+}
diff --git a/app/Model/Dashboard.php b/app/Model/Dashboard.php
new file mode 100644
index 000000000..a5f4d0cf4
--- /dev/null
+++ b/app/Model/Dashboard.php
@@ -0,0 +1,50 @@
+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;
+ }
+}
diff --git a/app/Model/Event.php b/app/Model/Event.php
index 98d713970..79b8c7129 100755
--- a/app/Model/Event.php
+++ b/app/Model/Event.php
@@ -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);
diff --git a/app/Model/UserSetting.php b/app/Model/UserSetting.php
index c472185aa..433fbd685 100644
--- a/app/Model/UserSetting.php
+++ b/app/Model/UserSetting.php
@@ -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;
+ }
}
diff --git a/app/View/Attributes/add.ctp.bk b/app/View/Attributes/add.ctp.bk
new file mode 100644
index 000000000..89bf37355
--- /dev/null
+++ b/app/View/Attributes/add.ctp.bk
@@ -0,0 +1,189 @@
+
+element('/genericElements/SideMenu/side_menu', array('menuList' => 'event', 'menuItem' => 'addAttribute', 'event' => $event));
+ }
+?>
+
+Js->writeBuffer(); // Write cached scripts
diff --git a/app/View/Dashboards/Widgets/BarChart.ctp b/app/View/Dashboards/Widgets/BarChart.ctp
new file mode 100644
index 000000000..93d1472c8
--- /dev/null
+++ b/app/View/Dashboards/Widgets/BarChart.ctp
@@ -0,0 +1,32 @@
+
+ $count) {
+ $value = $count;
+ if (!empty($data['logarithmic'])) {
+ $value = $data['logarithmic'][$entry];
+ }
+ echo sprintf(
+ '%s | %s |
',
+ 'text-align:right;width:33%;',
+ h($entry),
+ 'width:100%',
+ sprintf(
+ '%s
',
+ 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)
+ ),
+ ' '
+ );
+ }
+?>
+
diff --git a/app/View/Dashboards/Widgets/SimpleList.ctp b/app/View/Dashboards/Widgets/SimpleList.ctp
new file mode 100644
index 000000000..56821a0ce
--- /dev/null
+++ b/app/View/Dashboards/Widgets/SimpleList.ctp
@@ -0,0 +1,9 @@
+%s: %s%s',
+ h($element['title']),
+ empty($element['value']) ? '' : h($element['value']),
+ empty($element['html']) ? '' : $element['html']
+ );
+ }
diff --git a/app/View/Dashboards/add.ctp b/app/View/Dashboards/add.ctp
new file mode 100644
index 000000000..923303578
--- /dev/null
+++ b/app/View/Dashboards/add.ctp
@@ -0,0 +1,90 @@
+ $desc) {
+ $paramsHtml .= sprintf(
+ '%s: %s
',
+ 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' => '
Parameters
'
+ )
+ ));
+?>
+
+Js->writeBuffer(); // Write cached scripts
diff --git a/app/View/Dashboards/edit.ctp b/app/View/Dashboards/edit.ctp
new file mode 100644
index 000000000..834278f12
--- /dev/null
+++ b/app/View/Dashboards/edit.ctp
@@ -0,0 +1,43 @@
+ $desc) {
+ $paramsHtml .= sprintf(
+ '%s: %s
',
+ 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(
+ '%s
Parameters
%s
',
+ h($data['description']),
+ $paramsHtml
+ )
+ )
+ ));
+?>
diff --git a/app/View/Dashboards/get_empty_widget.ctp b/app/View/Dashboards/get_empty_widget.ctp
new file mode 100644
index 000000000..dfb603389
--- /dev/null
+++ b/app/View/Dashboards/get_empty_widget.ctp
@@ -0,0 +1 @@
+= $this->element('/dashboard/widget', array('widget' => $widget, 'k' => $k)); ?>
diff --git a/app/View/Dashboards/index.ctp b/app/View/Dashboards/index.ctp
new file mode 100644
index 000000000..a614e1f35
--- /dev/null
+++ b/app/View/Dashboards/index.ctp
@@ -0,0 +1,50 @@
+
+
+ $widget) {
+ $layout .= $this->element('/dashboard/widget', array('widget' => $widget, 'k' => $k));
+ }
+ echo $layout;
+ ?>
+
+
+
+element('/genericElements/SideMenu/side_menu', array('menuList' => 'dashboard', 'menuItem' => 'dashboardIndex'));
+?>
+
diff --git a/app/View/Elements/dashboard/widget.ctp b/app/View/Elements/dashboard/widget.ctp
new file mode 100644
index 000000000..17d360810
--- /dev/null
+++ b/app/View/Elements/dashboard/widget.ctp
@@ -0,0 +1,26 @@
+%s
',
+ 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(
+ '',
+ empty($widget['config']['alias']) ? h($widget['title']) : h($widget['config']['alias']),
+ sprintf(
+ '',
+ __('Configure widget')
+ ),
+ sprintf(
+ '',
+ __('Remove widget')
+ ),
+ ' '
+ )
+ );
+?>
diff --git a/app/View/Elements/genericElements/Form/genericForm.ctp b/app/View/Elements/genericElements/Form/genericForm.ctp
index bab2eb85e..6df454402 100644
--- a/app/View/Elements/genericElements/Form/genericForm.ctp
+++ b/app/View/Elements/genericElements/Form/genericForm.ctp
@@ -96,7 +96,8 @@
sprintf(
'%s
',
sprintf(
- '%s%s%s',
+ '%s%s%s%s',
+ empty($data['description']) ? '' : $data['description'],
$formCreate,
$ajaxFlashMessage,
$fieldsString,
diff --git a/app/View/Elements/genericElements/SideMenu/side_menu.ctp b/app/View/Elements/genericElements/SideMenu/side_menu.ctp
index a6fa8ba0d..b483bc1c7 100644
--- a/app/View/Elements/genericElements/SideMenu/side_menu.ctp
+++ b/app/View/Elements/genericElements/SideMenu/side_menu.ctp
@@ -2,6 +2,22 @@
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 '';
diff --git a/app/View/Elements/global_menu.ctp b/app/View/Elements/global_menu.ctp
index 073175bc2..76e105b35 100755
--- a/app/View/Elements/global_menu.ctp
+++ b/app/View/Elements/global_menu.ctp
@@ -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 = '' : __('Home'))
),
array(
@@ -402,12 +402,17 @@
$menu_right = array(
array(
'type' => 'root',
- 'url' => $baseurl . '/',
+ 'url' => '#',
+ 'html' => ''
+ ),
+ array(
+ 'type' => 'root',
+ 'url' =>empty($homepage['path']) ? '$baseurl' : $baseurl . h($homepage['path']),
'html' => 'MISP'
),
array(
'type' => 'root',
- 'url' => '/users/dashboard',
+ 'url' => '/dashboards',
'html' => sprintf(
'%s%s %s',
h($me['email']),
diff --git a/app/View/Layouts/dashboard.ctp b/app/View/Layouts/dashboard.ctp
new file mode 100644
index 000000000..c667ce805
--- /dev/null
+++ b/app/View/Layouts/dashboard.ctp
@@ -0,0 +1,136 @@
+
+
+
+
+ Html->charset(); ?>
+
+
+
+
+ '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'
+ ));
+ ?>
+
+
+
+
+
+
+
+
+
+
+
+
+ element('global_menu');
+ $topPadding = '50';
+ if (!empty($debugMode) && $debugMode != 'debugOff') {
+ $topPadding = '0';
+ }
+ ?>
+
+
+
+ Flash->render();
+ ?>
+
+
+
+ fetch('content');
+ ?>
+
+ 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');
+ ?>
+
+
+
+
+
+
diff --git a/app/webroot/css/gridstack.min.css b/app/webroot/css/gridstack.min.css
new file mode 100644
index 000000000..d7090fd12
--- /dev/null
+++ b/app/webroot/css/gridstack.min.css
@@ -0,0 +1,6 @@
+/*!
+ * required gridstack 1.0.0 CSS for default 12 and 1 columnMode size. Use gridstack-extra.css for others
+ * https://gridstackjs.com/
+ * (c) 2014-2020 Alain Dumesny, Dylan Weiss, Pavel Reznikov
+ * gridstack.js may be freely distributed under the MIT license.
+*/:root .grid-stack-item>.ui-resizable-handle{filter:none}.grid-stack{position:relative}.grid-stack.grid-stack-rtl{direction:ltr}.grid-stack.grid-stack-rtl>.grid-stack-item{direction:rtl}.grid-stack .grid-stack-placeholder>.placeholder-content{border:1px dashed #d3d3d3;margin:0;position:absolute;top:0;left:10px;right:10px;bottom:0;width:auto;z-index:0!important;text-align:center}.grid-stack>.grid-stack-item{min-width:8.3333333333%;position:absolute;padding:0}.grid-stack>.grid-stack-item>.grid-stack-item-content{margin:0;position:absolute;top:0;left:10px;right:10px;bottom:0;width:auto;overflow-x:hidden;overflow-y:auto}.grid-stack>.grid-stack-item>.ui-resizable-handle{position:absolute;font-size:.1px;display:block;-ms-touch-action:none;touch-action:none}.grid-stack>.grid-stack-item.ui-resizable-autohide>.ui-resizable-handle,.grid-stack>.grid-stack-item.ui-resizable-disabled>.ui-resizable-handle{display:none}.grid-stack>.grid-stack-item.ui-draggable-dragging,.grid-stack>.grid-stack-item.ui-resizable-resizing{z-index:100}.grid-stack>.grid-stack-item.ui-draggable-dragging>.grid-stack-item-content,.grid-stack>.grid-stack-item.ui-resizable-resizing>.grid-stack-item-content{box-shadow:1px 4px 6px rgba(0,0,0,.2);opacity:.8}.grid-stack>.grid-stack-item>.ui-resizable-se,.grid-stack>.grid-stack-item>.ui-resizable-sw{background-image:url(data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iaXNvLTg4NTktMSI/Pgo8IS0tIEdlbmVyYXRvcjogQWRvYmUgSWxsdXN0cmF0b3IgMTYuMC4wLCBTVkcgRXhwb3J0IFBsdWctSW4gLiBTVkcgVmVyc2lvbjogNi4wMCBCdWlsZCAwKSAgLS0+CjwhRE9DVFlQRSBzdmcgUFVCTElDICItLy9XM0MvL0RURCBTVkcgMS4xLy9FTiIgImh0dHA6Ly93d3cudzMub3JnL0dyYXBoaWNzL1NWRy8xLjEvRFREL3N2ZzExLmR0ZCI+CjxzdmcgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB4bWxuczp4bGluaz0iaHR0cDovL3d3dy53My5vcmcvMTk5OS94bGluayIgdmVyc2lvbj0iMS4xIiBpZD0iQ2FwYV8xIiB4PSIwcHgiIHk9IjBweCIgd2lkdGg9IjE2cHgiIGhlaWdodD0iMTZweCIgdmlld0JveD0iMCAwIDUxMS42MjYgNTExLjYyNyIgc3R5bGU9ImVuYWJsZS1iYWNrZ3JvdW5kOm5ldyAwIDAgNTExLjYyNiA1MTEuNjI3OyIgeG1sOnNwYWNlPSJwcmVzZXJ2ZSI+CjxnPgoJPHBhdGggZD0iTTMyOC45MDYsNDAxLjk5NGgtMzYuNTUzVjEwOS42MzZoMzYuNTUzYzQuOTQ4LDAsOS4yMzYtMS44MDksMTIuODQ3LTUuNDI2YzMuNjEzLTMuNjE1LDUuNDIxLTcuODk4LDUuNDIxLTEyLjg0NSAgIGMwLTQuOTQ5LTEuODAxLTkuMjMxLTUuNDI4LTEyLjg1MWwtNzMuMDg3LTczLjA5QzI2NS4wNDQsMS44MDksMjYwLjc2LDAsMjU1LjgxMywwYy00Ljk0OCwwLTkuMjI5LDEuODA5LTEyLjg0Nyw1LjQyNCAgIGwtNzMuMDg4LDczLjA5Yy0zLjYxOCwzLjYxOS01LjQyNCw3LjkwMi01LjQyNCwxMi44NTFjMCw0Ljk0NiwxLjgwNyw5LjIyOSw1LjQyNCwxMi44NDVjMy42MTksMy42MTcsNy45MDEsNS40MjYsMTIuODUsNS40MjYgICBoMzYuNTQ1djI5Mi4zNThoLTM2LjU0MmMtNC45NTIsMC05LjIzNSwxLjgwOC0xMi44NSw1LjQyMWMtMy42MTcsMy42MjEtNS40MjQsNy45MDUtNS40MjQsMTIuODU0ICAgYzAsNC45NDUsMS44MDcsOS4yMjcsNS40MjQsMTIuODQ3bDczLjA4OSw3My4wODhjMy42MTcsMy42MTcsNy44OTgsNS40MjQsMTIuODQ3LDUuNDI0YzQuOTUsMCw5LjIzNC0xLjgwNywxMi44NDktNS40MjQgICBsNzMuMDg3LTczLjA4OGMzLjYxMy0zLjYyLDUuNDIxLTcuOTAxLDUuNDIxLTEyLjg0N2MwLTQuOTQ4LTEuODA4LTkuMjMyLTUuNDIxLTEyLjg1NCAgIEMzMzguMTQyLDQwMy44MDIsMzMzLjg1Nyw0MDEuOTk0LDMyOC45MDYsNDAxLjk5NHoiIGZpbGw9IiM2NjY2NjYiLz4KPC9nPgo8Zz4KPC9nPgo8Zz4KPC9nPgo8Zz4KPC9nPgo8Zz4KPC9nPgo8Zz4KPC9nPgo8Zz4KPC9nPgo8Zz4KPC9nPgo8Zz4KPC9nPgo8Zz4KPC9nPgo8Zz4KPC9nPgo8Zz4KPC9nPgo8Zz4KPC9nPgo8Zz4KPC9nPgo8Zz4KPC9nPgo8Zz4KPC9nPgo8L3N2Zz4K);background-repeat:no-repeat;background-position:center;-webkit-transform:rotate(45deg);-moz-transform:rotate(45deg);-ms-transform:rotate(45deg);-o-transform:rotate(45deg);transform:rotate(45deg)}.grid-stack>.grid-stack-item>.ui-resizable-se{-webkit-transform:rotate(-45deg);-moz-transform:rotate(-45deg);-ms-transform:rotate(-45deg);-o-transform:rotate(-45deg);transform:rotate(-45deg)}.grid-stack>.grid-stack-item>.ui-resizable-nw{cursor:nw-resize;width:20px;height:20px;left:10px;top:0}.grid-stack>.grid-stack-item>.ui-resizable-n{cursor:n-resize;height:10px;top:0;left:25px;right:25px}.grid-stack>.grid-stack-item>.ui-resizable-ne{cursor:ne-resize;width:20px;height:20px;right:10px;top:0}.grid-stack>.grid-stack-item>.ui-resizable-e{cursor:e-resize;width:10px;right:10px;top:15px;bottom:15px}.grid-stack>.grid-stack-item>.ui-resizable-se{cursor:se-resize;width:20px;height:20px;right:10px;bottom:0}.grid-stack>.grid-stack-item>.ui-resizable-s{cursor:s-resize;height:10px;left:25px;bottom:0;right:25px}.grid-stack>.grid-stack-item>.ui-resizable-sw{cursor:sw-resize;width:20px;height:20px;left:10px;bottom:0}.grid-stack>.grid-stack-item>.ui-resizable-w{cursor:w-resize;width:10px;left:10px;top:15px;bottom:15px}.grid-stack>.grid-stack-item.ui-draggable-dragging>.ui-resizable-handle{display:none!important}.grid-stack>.grid-stack-item[data-gs-width='1']{width:8.3333333333%}.grid-stack>.grid-stack-item[data-gs-x='1']{left:8.3333333333%}.grid-stack>.grid-stack-item[data-gs-min-width='1']{min-width:8.3333333333%}.grid-stack>.grid-stack-item[data-gs-max-width='1']{max-width:8.3333333333%}.grid-stack>.grid-stack-item[data-gs-width='2']{width:16.6666666667%}.grid-stack>.grid-stack-item[data-gs-x='2']{left:16.6666666667%}.grid-stack>.grid-stack-item[data-gs-min-width='2']{min-width:16.6666666667%}.grid-stack>.grid-stack-item[data-gs-max-width='2']{max-width:16.6666666667%}.grid-stack>.grid-stack-item[data-gs-width='3']{width:25%}.grid-stack>.grid-stack-item[data-gs-x='3']{left:25%}.grid-stack>.grid-stack-item[data-gs-min-width='3']{min-width:25%}.grid-stack>.grid-stack-item[data-gs-max-width='3']{max-width:25%}.grid-stack>.grid-stack-item[data-gs-width='4']{width:33.3333333333%}.grid-stack>.grid-stack-item[data-gs-x='4']{left:33.3333333333%}.grid-stack>.grid-stack-item[data-gs-min-width='4']{min-width:33.3333333333%}.grid-stack>.grid-stack-item[data-gs-max-width='4']{max-width:33.3333333333%}.grid-stack>.grid-stack-item[data-gs-width='5']{width:41.6666666667%}.grid-stack>.grid-stack-item[data-gs-x='5']{left:41.6666666667%}.grid-stack>.grid-stack-item[data-gs-min-width='5']{min-width:41.6666666667%}.grid-stack>.grid-stack-item[data-gs-max-width='5']{max-width:41.6666666667%}.grid-stack>.grid-stack-item[data-gs-width='6']{width:50%}.grid-stack>.grid-stack-item[data-gs-x='6']{left:50%}.grid-stack>.grid-stack-item[data-gs-min-width='6']{min-width:50%}.grid-stack>.grid-stack-item[data-gs-max-width='6']{max-width:50%}.grid-stack>.grid-stack-item[data-gs-width='7']{width:58.3333333333%}.grid-stack>.grid-stack-item[data-gs-x='7']{left:58.3333333333%}.grid-stack>.grid-stack-item[data-gs-min-width='7']{min-width:58.3333333333%}.grid-stack>.grid-stack-item[data-gs-max-width='7']{max-width:58.3333333333%}.grid-stack>.grid-stack-item[data-gs-width='8']{width:66.6666666667%}.grid-stack>.grid-stack-item[data-gs-x='8']{left:66.6666666667%}.grid-stack>.grid-stack-item[data-gs-min-width='8']{min-width:66.6666666667%}.grid-stack>.grid-stack-item[data-gs-max-width='8']{max-width:66.6666666667%}.grid-stack>.grid-stack-item[data-gs-width='9']{width:75%}.grid-stack>.grid-stack-item[data-gs-x='9']{left:75%}.grid-stack>.grid-stack-item[data-gs-min-width='9']{min-width:75%}.grid-stack>.grid-stack-item[data-gs-max-width='9']{max-width:75%}.grid-stack>.grid-stack-item[data-gs-width='10']{width:83.3333333333%}.grid-stack>.grid-stack-item[data-gs-x='10']{left:83.3333333333%}.grid-stack>.grid-stack-item[data-gs-min-width='10']{min-width:83.3333333333%}.grid-stack>.grid-stack-item[data-gs-max-width='10']{max-width:83.3333333333%}.grid-stack>.grid-stack-item[data-gs-width='11']{width:91.6666666667%}.grid-stack>.grid-stack-item[data-gs-x='11']{left:91.6666666667%}.grid-stack>.grid-stack-item[data-gs-min-width='11']{min-width:91.6666666667%}.grid-stack>.grid-stack-item[data-gs-max-width='11']{max-width:91.6666666667%}.grid-stack>.grid-stack-item[data-gs-width='12']{width:100%}.grid-stack>.grid-stack-item[data-gs-x='12']{left:100%}.grid-stack>.grid-stack-item[data-gs-min-width='12']{min-width:100%}.grid-stack>.grid-stack-item[data-gs-max-width='12']{max-width:100%}.grid-stack.grid-stack-1>.grid-stack-item{min-width:100%}.grid-stack.grid-stack-1>.grid-stack-item[data-gs-width='1']{width:100%}.grid-stack.grid-stack-1>.grid-stack-item[data-gs-x='1']{left:100%}.grid-stack.grid-stack-1>.grid-stack-item[data-gs-min-width='1']{min-width:100%}.grid-stack.grid-stack-1>.grid-stack-item[data-gs-max-width='1']{max-width:100%}.grid-stack.grid-stack-animate,.grid-stack.grid-stack-animate .grid-stack-item{-webkit-transition:left .3s,top .3s,height .3s,width .3s;-moz-transition:left .3s,top .3s,height .3s,width .3s;-ms-transition:left .3s,top .3s,height .3s,width .3s;-o-transition:left .3s,top .3s,height .3s,width .3s;transition:left .3s,top .3s,height .3s,width .3s}.grid-stack.grid-stack-animate .grid-stack-item.grid-stack-placeholder,.grid-stack.grid-stack-animate .grid-stack-item.ui-draggable-dragging,.grid-stack.grid-stack-animate .grid-stack-item.ui-resizable-resizing{-webkit-transition:left 0s,top 0s,height 0s,width 0s;-moz-transition:left 0s,top 0s,height 0s,width 0s;-ms-transition:left 0s,top 0s,height 0s,width 0s;-o-transition:left 0s,top 0s,height 0s,width 0s;transition:left 0s,top 0s,height 0s,width 0s}
\ No newline at end of file
diff --git a/app/webroot/css/main.css b/app/webroot/css/main.css
index 81c67ba84..173bda0e2 100644
--- a/app/webroot/css/main.css
+++ b/app/webroot/css/main.css
@@ -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;
+}
diff --git a/app/webroot/js/gridstack.all.js b/app/webroot/js/gridstack.all.js
new file mode 100644
index 000000000..7ddcb7647
--- /dev/null
+++ b/app/webroot/js/gridstack.all.js
@@ -0,0 +1,4 @@
+/** gridstack.js 1.0.0 - IE and older browsers Polyfills for this library @preserve*/
+Number.isNaN=Number.isNaN||function(e){return"number"==typeof e&&e!=e},Array.prototype.find||Object.defineProperty(Array.prototype,"find",{value:function(e){if(null==this)throw TypeError('"this" is null or not defined');var t=Object(this),i=t.length>>>0;if("function"!=typeof e)throw TypeError("predicate must be a function");for(var n=arguments[1],o=0;o>>0;if("function"!=typeof e)throw new TypeError("predicate must be a function");for(var n=arguments[1],o=0;o>10|55296,1023&n|56320)}function o(){x()}var e,f,_,s,r,p,c,g,w,l,h,x,C,a,T,m,u,v,y,N="sizzle"+ +new Date,b=i.document,E=0,n=0,k=le(),H=le(),S=le(),z=le(),P=function(e,t){return e===t&&(h=!0),0},D={}.hasOwnProperty,t=[],A=t.pop,M=t.push,R=t.push,W=t.slice,O=function(e,t){for(var i=0,n=e.length;i+~]|"+I+")"+I+"*"),Y=new RegExp(I+"|>"),G=new RegExp($),V=new RegExp("^"+L+"$"),Q={ID:new RegExp("^#("+L+")"),CLASS:new RegExp("^\\.("+L+")"),TAG:new RegExp("^("+L+"|[*])"),ATTR:new RegExp("^"+q),PSEUDO:new RegExp("^"+$),CHILD:new RegExp("^:(only|first|last|nth|nth-last)-(child|of-type)(?:\\("+I+"*(even|odd|(([+-]|)(\\d*)n|)"+I+"*(?:([+-]|)"+I+"*(\\d+)|))"+I+"*\\)|)","i"),bool:new RegExp("^(?:"+j+")$","i"),needsContext:new RegExp("^"+I+"*[>+~]|:(even|odd|eq|gt|lt|nth|first|last)(?:\\("+I+"*((?:-\\d)?\\d*)"+I+"*\\)|)(?=[^-]|$)","i")},K=/HTML$/i,J=/^(?:input|select|textarea|button)$/i,Z=/^h\d$/i,ee=/^[^{]+\{\s*\[native \w/,te=/^(?:#([\w-]+)|(\w+)|\.([\w-]+))$/,ie=/[+~]/,ne=new RegExp("\\\\([\\da-f]{1,6}"+I+"?|("+I+")|.)","ig"),oe=/([\0-\x1f\x7f]|^-?\d)|^-$|[^\0-\x1f\x7f-\uFFFF\w-]/g,se=function(e,t){return t?"\0"===e?"�":e.slice(0,-1)+"\\"+e.charCodeAt(e.length-1).toString(16)+" ":"\\"+e},re=_e(function(e){return!0===e.disabled&&"fieldset"===e.nodeName.toLowerCase()},{dir:"parentNode",next:"legend"});try{R.apply(t=W.call(b.childNodes),b.childNodes),t[b.childNodes.length].nodeType}catch(e){R={apply:t.length?function(e,t){M.apply(e,W.call(t))}:function(e,t){for(var i=e.length,n=0;e[i++]=t[n++];);e.length=i-1}}}function ae(t,e,i,n){var o,s,r,a,l,h,u,d=e&&e.ownerDocument,c=e?e.nodeType:9;if(i=i||[],"string"!=typeof t||!t||1!==c&&9!==c&&11!==c)return i;if(!n&&((e?e.ownerDocument||e:b)!==C&&x(e),e=e||C,T)){if(11!==c&&(l=te.exec(t)))if(o=l[1]){if(9===c){if(!(r=e.getElementById(o)))return i;if(r.id===o)return i.push(r),i}else if(d&&(r=d.getElementById(o))&&y(e,r)&&r.id===o)return i.push(r),i}else{if(l[2])return R.apply(i,e.getElementsByTagName(t)),i;if((o=l[3])&&f.getElementsByClassName&&e.getElementsByClassName)return R.apply(i,e.getElementsByClassName(o)),i}if(f.qsa&&!z[t+" "]&&(!m||!m.test(t))&&(1!==c||"object"!==e.nodeName.toLowerCase())){if(u=t,d=e,1===c&&Y.test(t)){for((a=e.getAttribute("id"))?a=a.replace(oe,se):e.setAttribute("id",a=N),s=(h=p(t)).length;s--;)h[s]="#"+a+" "+be(h[s]);u=h.join(","),d=ie.test(t)&&ve(e.parentNode)||e}try{return R.apply(i,d.querySelectorAll(u)),i}catch(e){z(t,!0)}finally{a===N&&e.removeAttribute("id")}}}return g(t.replace(F,"$1"),e,i,n)}function le(){var n=[];return function e(t,i){return n.push(t+" ")>_.cacheLength&&delete e[n.shift()],e[t+" "]=i}}function he(e){return e[N]=!0,e}function ue(e){var t=C.createElement("fieldset");try{return!!e(t)}catch(e){return!1}finally{t.parentNode&&t.parentNode.removeChild(t),t=null}}function de(e,t){for(var i=e.split("|"),n=i.length;n--;)_.attrHandle[i[n]]=t}function ce(e,t){var i=t&&e,n=i&&1===e.nodeType&&1===t.nodeType&&e.sourceIndex-t.sourceIndex;if(n)return n;if(i)for(;i=i.nextSibling;)if(i===t)return-1;return e?1:-1}function fe(t){return function(e){return"input"===e.nodeName.toLowerCase()&&e.type===t}}function pe(i){return function(e){var t=e.nodeName.toLowerCase();return("input"===t||"button"===t)&&e.type===i}}function ge(t){return function(e){return"form"in e?e.parentNode&&!1===e.disabled?"label"in e?"label"in e.parentNode?e.parentNode.disabled===t:e.disabled===t:e.isDisabled===t||e.isDisabled!==!t&&re(e)===t:e.disabled===t:"label"in e&&e.disabled===t}}function me(r){return he(function(s){return s=+s,he(function(e,t){for(var i,n=r([],e.length,s),o=n.length;o--;)e[i=n[o]]&&(e[i]=!(t[i]=e[i]))})})}function ve(e){return e&&void 0!==e.getElementsByTagName&&e}for(e in f=ae.support={},r=ae.isXML=function(e){var t=e.namespaceURI,i=(e.ownerDocument||e).documentElement;return!K.test(t||i&&i.nodeName||"HTML")},x=ae.setDocument=function(e){var t,i,n=e?e.ownerDocument||e:b;return n!==C&&9===n.nodeType&&n.documentElement&&(a=(C=n).documentElement,T=!r(C),b!==C&&(i=C.defaultView)&&i.top!==i&&(i.addEventListener?i.addEventListener("unload",o,!1):i.attachEvent&&i.attachEvent("onunload",o)),f.attributes=ue(function(e){return e.className="i",!e.getAttribute("className")}),f.getElementsByTagName=ue(function(e){return e.appendChild(C.createComment("")),!e.getElementsByTagName("*").length}),f.getElementsByClassName=ee.test(C.getElementsByClassName),f.getById=ue(function(e){return a.appendChild(e).id=N,!C.getElementsByName||!C.getElementsByName(N).length}),f.getById?(_.filter.ID=function(e){var t=e.replace(ne,d);return function(e){return e.getAttribute("id")===t}},_.find.ID=function(e,t){if(void 0!==t.getElementById&&T){var i=t.getElementById(e);return i?[i]:[]}}):(_.filter.ID=function(e){var i=e.replace(ne,d);return function(e){var t=void 0!==e.getAttributeNode&&e.getAttributeNode("id");return t&&t.value===i}},_.find.ID=function(e,t){if(void 0!==t.getElementById&&T){var i,n,o,s=t.getElementById(e);if(s){if((i=s.getAttributeNode("id"))&&i.value===e)return[s];for(o=t.getElementsByName(e),n=0;s=o[n++];)if((i=s.getAttributeNode("id"))&&i.value===e)return[s]}return[]}}),_.find.TAG=f.getElementsByTagName?function(e,t){return void 0!==t.getElementsByTagName?t.getElementsByTagName(e):f.qsa?t.querySelectorAll(e):void 0}:function(e,t){var i,n=[],o=0,s=t.getElementsByTagName(e);if("*"!==e)return s;for(;i=s[o++];)1===i.nodeType&&n.push(i);return n},_.find.CLASS=f.getElementsByClassName&&function(e,t){if(void 0!==t.getElementsByClassName&&T)return t.getElementsByClassName(e)},u=[],m=[],(f.qsa=ee.test(C.querySelectorAll))&&(ue(function(e){a.appendChild(e).innerHTML="",e.querySelectorAll("[msallowcapture^='']").length&&m.push("[*^$]="+I+"*(?:''|\"\")"),e.querySelectorAll("[selected]").length||m.push("\\["+I+"*(?:value|"+j+")"),e.querySelectorAll("[id~="+N+"-]").length||m.push("~="),e.querySelectorAll(":checked").length||m.push(":checked"),e.querySelectorAll("a#"+N+"+*").length||m.push(".#.+[+~]")}),ue(function(e){e.innerHTML="";var t=C.createElement("input");t.setAttribute("type","hidden"),e.appendChild(t).setAttribute("name","D"),e.querySelectorAll("[name=d]").length&&m.push("name"+I+"*[*^$|!~]?="),2!==e.querySelectorAll(":enabled").length&&m.push(":enabled",":disabled"),a.appendChild(e).disabled=!0,2!==e.querySelectorAll(":disabled").length&&m.push(":enabled",":disabled"),e.querySelectorAll("*,:x"),m.push(",.*:")})),(f.matchesSelector=ee.test(v=a.matches||a.webkitMatchesSelector||a.mozMatchesSelector||a.oMatchesSelector||a.msMatchesSelector))&&ue(function(e){f.disconnectedMatch=v.call(e,"*"),v.call(e,"[s!='']:x"),u.push("!=",$)}),m=m.length&&new RegExp(m.join("|")),u=u.length&&new RegExp(u.join("|")),t=ee.test(a.compareDocumentPosition),y=t||ee.test(a.contains)?function(e,t){var i=9===e.nodeType?e.documentElement:e,n=t&&t.parentNode;return e===n||!(!n||1!==n.nodeType||!(i.contains?i.contains(n):e.compareDocumentPosition&&16&e.compareDocumentPosition(n)))}:function(e,t){if(t)for(;t=t.parentNode;)if(t===e)return!0;return!1},P=t?function(e,t){if(e===t)return h=!0,0;var i=!e.compareDocumentPosition-!t.compareDocumentPosition;return i||(1&(i=(e.ownerDocument||e)===(t.ownerDocument||t)?e.compareDocumentPosition(t):1)||!f.sortDetached&&t.compareDocumentPosition(e)===i?e===C||e.ownerDocument===b&&y(b,e)?-1:t===C||t.ownerDocument===b&&y(b,t)?1:l?O(l,e)-O(l,t):0:4&i?-1:1)}:function(e,t){if(e===t)return h=!0,0;var i,n=0,o=e.parentNode,s=t.parentNode,r=[e],a=[t];if(!o||!s)return e===C?-1:t===C?1:o?-1:s?1:l?O(l,e)-O(l,t):0;if(o===s)return ce(e,t);for(i=e;i=i.parentNode;)r.unshift(i);for(i=t;i=i.parentNode;)a.unshift(i);for(;r[n]===a[n];)n++;return n?ce(r[n],a[n]):r[n]===b?-1:a[n]===b?1:0}),C},ae.matches=function(e,t){return ae(e,null,null,t)},ae.matchesSelector=function(e,t){if((e.ownerDocument||e)!==C&&x(e),f.matchesSelector&&T&&!z[t+" "]&&(!u||!u.test(t))&&(!m||!m.test(t)))try{var i=v.call(e,t);if(i||f.disconnectedMatch||e.document&&11!==e.document.nodeType)return i}catch(e){z(t,!0)}return 0":{dir:"parentNode",first:!0}," ":{dir:"parentNode"},"+":{dir:"previousSibling",first:!0},"~":{dir:"previousSibling"}},preFilter:{ATTR:function(e){return e[1]=e[1].replace(ne,d),e[3]=(e[3]||e[4]||e[5]||"").replace(ne,d),"~="===e[2]&&(e[3]=" "+e[3]+" "),e.slice(0,4)},CHILD:function(e){return e[1]=e[1].toLowerCase(),"nth"===e[1].slice(0,3)?(e[3]||ae.error(e[0]),e[4]=+(e[4]?e[5]+(e[6]||1):2*("even"===e[3]||"odd"===e[3])),e[5]=+(e[7]+e[8]||"odd"===e[3])):e[3]&&ae.error(e[0]),e},PSEUDO:function(e){var t,i=!e[6]&&e[2];return Q.CHILD.test(e[0])?null:(e[3]?e[2]=e[4]||e[5]||"":i&&G.test(i)&&(t=p(i,!0))&&(t=i.indexOf(")",i.length-t)-i.length)&&(e[0]=e[0].slice(0,t),e[2]=i.slice(0,t)),e.slice(0,3))}},filter:{TAG:function(e){var t=e.replace(ne,d).toLowerCase();return"*"===e?function(){return!0}:function(e){return e.nodeName&&e.nodeName.toLowerCase()===t}},CLASS:function(e){var t=k[e+" "];return t||(t=new RegExp("(^|"+I+")"+e+"("+I+"|$)"))&&k(e,function(e){return t.test("string"==typeof e.className&&e.className||void 0!==e.getAttribute&&e.getAttribute("class")||"")})},ATTR:function(i,n,o){return function(e){var t=ae.attr(e,i);return null==t?"!="===n:!n||(t+="","="===n?t===o:"!="===n?t!==o:"^="===n?o&&0===t.indexOf(o):"*="===n?o&&-1:\x20\t\r\n\f]*)[\x20\t\r\n\f]*\/?>(?:<\/\1>|)$/i;function z(e,i,n){return b(i)?N.grep(e,function(e,t){return!!i.call(e,t,e)!==n}):i.nodeType?N.grep(e,function(e){return e===i!==n}):"string"!=typeof i?N.grep(e,function(e){return-1)[^>]*|#([\w-]+))$/;(N.fn.init=function(e,t,i){var n,o;if(!e)return this;if(i=i||P,"string"!=typeof e)return e.nodeType?(this[0]=e,this.length=1,this):b(e)?void 0!==i.ready?i.ready(e):e(N):N.makeArray(e,this);if(!(n="<"===e[0]&&">"===e[e.length-1]&&3<=e.length?[null,e,null]:D.exec(e))||!n[1]&&t)return!t||t.jquery?(t||i).find(e):this.constructor(t).find(e);if(n[1]){if(t=t instanceof N?t[0]:t,N.merge(this,N.parseHTML(n[1],t&&t.nodeType?t.ownerDocument||t:T,!0)),S.test(n[1])&&N.isPlainObject(t))for(n in t)b(this[n])?this[n](t[n]):this.attr(n,t[n]);return this}return(o=T.getElementById(n[2]))&&(this[0]=o,this.length=1),this}).prototype=N.fn,P=N(T);var A=/^(?:parents|prev(?:Until|All))/,M={children:!0,contents:!0,next:!0,prev:!0};function R(e,t){for(;(e=e[t])&&1!==e.nodeType;);return e}N.fn.extend({has:function(e){var t=N(e,this),i=t.length;return this.filter(function(){for(var e=0;e\x20\t\r\n\f]*)/i,pe=/^$|^module$|\/(?:java|ecma)script/i,ge={option:[1,""],thead:[1,""],col:[2,""],tr:[2,""],td:[3,""],_default:[0,"",""]};function me(e,t){var i;return i=void 0!==e.getElementsByTagName?e.getElementsByTagName(t||"*"):void 0!==e.querySelectorAll?e.querySelectorAll(t||"*"):[],void 0===t||t&&H(e,t)?N.merge([e],i):i}function ve(e,t){for(var i=0,n=e.length;ix",y.noCloneChecked=!!ye.cloneNode(!0).lastChild.defaultValue;var xe=/^key/,Ce=/^(?:mouse|pointer|contextmenu|drag|drop)|click/,Te=/^([^.]*)(?:\.(.+)|)/;function Ne(){return!0}function Ee(){return!1}function ke(e,t){return e===function(){try{return T.activeElement}catch(e){}}()==("focus"===t)}function He(e,t,i,n,o,s){var r,a;if("object"==typeof t){for(a in"string"!=typeof i&&(n=n||i,i=void 0),t)He(e,a,i,n,t[a],s);return e}if(null==n&&null==o?(o=i,n=i=void 0):null==o&&("string"==typeof i?(o=n,n=void 0):(o=n,n=i,i=void 0)),!1===o)o=Ee;else if(!o)return e;return 1===s&&(r=o,(o=function(e){return N().off(e),r.apply(this,arguments)}).guid=r.guid||(r.guid=N.guid++)),e.each(function(){N.event.add(this,t,o,n,i)})}function Se(e,o,s){s?(Q.set(e,o,!1),N.event.add(e,o,{namespace:!1,handler:function(e){var t,i,n=Q.get(this,o);if(1&e.isTrigger&&this[o]){if(n.length)(N.event.special[o]||{}).delegateType&&e.stopPropagation();else if(n=a.call(arguments),Q.set(this,o,n),t=s(this,o),this[o](),n!==(i=Q.get(this,o))||t?Q.set(this,o,!1):i={},n!==i)return e.stopImmediatePropagation(),e.preventDefault(),i.value}else n.length&&(Q.set(this,o,{value:N.event.trigger(N.extend(n[0],N.Event.prototype),n.slice(1),this)}),e.stopImmediatePropagation())}})):void 0===Q.get(e,o)&&N.event.add(e,o,Ne)}N.event={global:{},add:function(t,e,i,n,o){var s,r,a,l,h,u,d,c,f,p,g,m=Q.get(t);if(m)for(i.handler&&(i=(s=i).handler,o=s.selector),o&&N.find.matchesSelector(oe,o),i.guid||(i.guid=N.guid++),(l=m.events)||(l=m.events={}),(r=m.handle)||(r=m.handle=function(e){return void 0!==N&&N.event.triggered!==e.type?N.event.dispatch.apply(t,arguments):void 0}),h=(e=(e||"").match(W)||[""]).length;h--;)f=g=(a=Te.exec(e[h])||[])[1],p=(a[2]||"").split(".").sort(),f&&(d=N.event.special[f]||{},f=(o?d.delegateType:d.bindType)||f,d=N.event.special[f]||{},u=N.extend({type:f,origType:g,data:n,handler:i,guid:i.guid,selector:o,needsContext:o&&N.expr.match.needsContext.test(o),namespace:p.join(".")},s),(c=l[f])||((c=l[f]=[]).delegateCount=0,d.setup&&!1!==d.setup.call(t,n,p,r)||t.addEventListener&&t.addEventListener(f,r)),d.add&&(d.add.call(t,u),u.handler.guid||(u.handler.guid=i.guid)),o?c.splice(c.delegateCount++,0,u):c.push(u),N.event.global[f]=!0)},remove:function(e,t,i,n,o){var s,r,a,l,h,u,d,c,f,p,g,m=Q.hasData(e)&&Q.get(e);if(m&&(l=m.events)){for(h=(t=(t||"").match(W)||[""]).length;h--;)if(f=g=(a=Te.exec(t[h])||[])[1],p=(a[2]||"").split(".").sort(),f){for(d=N.event.special[f]||{},c=l[f=(n?d.delegateType:d.bindType)||f]||[],a=a[2]&&new RegExp("(^|\\.)"+p.join("\\.(?:.*\\.|)")+"(\\.|$)"),r=s=c.length;s--;)u=c[s],!o&&g!==u.origType||i&&i.guid!==u.guid||a&&!a.test(u.namespace)||n&&n!==u.selector&&("**"!==n||!u.selector)||(c.splice(s,1),u.selector&&c.delegateCount--,d.remove&&d.remove.call(e,u));r&&!c.length&&(d.teardown&&!1!==d.teardown.call(e,p,m.handle)||N.removeEvent(e,f,m.handle),delete l[f])}else for(f in l)N.event.remove(e,f+t[h],i,n,!0);N.isEmptyObject(l)&&Q.remove(e,"handle events")}},dispatch:function(e){var t,i,n,o,s,r,a=N.event.fix(e),l=new Array(arguments.length),h=(Q.get(this,"events")||{})[a.type]||[],u=N.event.special[a.type]||{};for(l[0]=a,t=1;t\x20\t\r\n\f]*)[^>]*)\/>/gi,Pe=/