Merge pull request #6412 from mokaddem/feature-event-report

[feature] Event Report
pull/6400/head
Alexandre Dulaunoy 2020-10-09 18:32:50 +02:00 committed by GitHub
commit e19f4a6013
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
47 changed files with 25651 additions and 50 deletions

View File

@ -150,6 +150,16 @@ class ACLComponent extends Component
'index' => array('*'),
'view' => array('*'),
),
'eventReports' => array(
'add' => array('perm_add'),
'view' => array('*'),
'viewSummary' => array('*'),
'edit' => array('perm_add'),
'delete' => array('perm_add'),
'restore' => array('perm_add'),
'index' => array('*'),
'getProxyMISPElements' => array('*'),
),
'events' => array(
'add' => array('perm_add'),
'addIOC' => array('perm_add'),

View File

@ -83,6 +83,18 @@ class RestResponseComponent extends Component
'optional' => array('network_name')
)
),
'EventReport' => array(
'add' => array(
'description' => "POST a report in JSON format to create a report for the provided event",
'mandatory' => array('name'),
'optional' => array('distribution', 'content')
),
'edit' => array(
'description' => "POST a report in JSON format to update the report",
'mandatory' => array(),
'optional' => array('name', 'distribution', 'content')
)
),
'Feed' => array(
'add' => array(
'description' => "POST a MISP Feed descriptor JSON to this API to add a Feed.",
@ -383,7 +395,7 @@ class RestResponseComponent extends Component
return '[]';
}
public function saveFailResponse($controller, $action, $id = false, $validationErrors, $format = false)
public function saveFailResponse($controller, $action, $id = false, $validationErrors, $format = false, $data = null)
{
$this->autoRender = false;
$response = array();
@ -395,12 +407,15 @@ class RestResponseComponent extends Component
$response['saved'] = false;
$response['name'] = 'Could not ' . $stringifiedAction . ' ' . Inflector::singularize($controller);
$response['message'] = $response['name'];
if (!is_null($data)) {
$response['data'] = $data;
}
$response['url'] = $this->__generateURL($action, $controller, $id);
$response['errors'] = $validationErrors;
return $this->__sendResponse($response, 403, $format);
}
public function saveSuccessResponse($controller, $action, $id = false, $format = false, $message = false)
public function saveSuccessResponse($controller, $action, $id = false, $format = false, $message = false, $data = null)
{
$action = $this->__dissectAdminRouting($action);
if (!$message) {
@ -410,6 +425,9 @@ class RestResponseComponent extends Component
$response['success'] = true;
$response['name'] = $message;
$response['message'] = $response['name'];
if (!is_null($data)) {
$response['data'] = $data;
}
$response['url'] = $this->__generateURL($action, $controller, $id);
return $this->__sendResponse($response, 200, $format);
}

View File

@ -13,7 +13,7 @@ class EventGraphController extends AppController
parent::beforeFilter();
}
public function view($event_id = false)
public function view($event_id = false, $graph_id = null)
{
if ($event_id === false) {
throw new MethodNotAllowedException(__('No event ID set.'));
@ -40,12 +40,16 @@ class EventGraphController extends AppController
}
// fetch eventGraphs
$conditions = [
'EventGraph.event_id' => $event_id,
'EventGraph.org_id' => $org_id
];
if (!is_null($graph_id)) {
$conditions['EventGraph.id'] = $graph_id;
}
$eventGraphs = $this->EventGraph->find('all', array(
'order' => 'EventGraph.timestamp DESC',
'conditions' => array(
'EventGraph.event_id' => $event_id,
'EventGraph.org_id' => $org_id
),
'conditions' => $conditions,
'contain' => array(
'User' => array(
'fields' => array(

View File

@ -0,0 +1,351 @@
<?php
App::uses('AppController', 'Controller');
class EventReportsController extends AppController
{
public $components = array(
'Security',
'AdminCrud',
'RequestHandler'
);
public $paginate = array(
'limit' => 60,
'order' => array(
'EventReport.event_id' => 'ASC',
'EventReport.name' => 'ASC'
),
'recursive' => -1,
'contain' => array(
'SharingGroup' => array('fields' => array('id', 'name', 'uuid')),
'Event' => array(
'fields' => array('Event.id', 'Event.orgc_id', 'Event.org_id', 'Event.info', 'Event.user_id', 'Event.date'),
'Orgc' => array('fields' => array('Orgc.id', 'Orgc.name')),
'Org' => array('fields' => array('Org.id', 'Org.name'))
)
)
);
public function add($eventId = false)
{
if ($this->request->is('get') && $this->_isRest()) {
return $this->RestResponse->describe('EventReports', 'add', false, $this->response->type());
}
if ($eventId === false) {
throw new MethodNotAllowedException(__('No event ID set.'));
}
$event = $this->__canModifyReport($eventId);
if ($this->request->is('post') || $this->request->is('put')) {
if (!isset($this->request->data['EventReport'])) {
$this->request->data['EventReport'] = $this->request->data;
}
$report = $this->request->data;
$errors = $this->EventReport->addReport($this->Auth->user(), $report, $eventId);
$redirectTarget = array('controller' => 'events', 'action' => 'view', $eventId);
if (!empty($errors)) {
return $this->__getFailResponseBasedOnContext($errors, array(), 'add', $this->EventReport->id, $redirectTarget);
} else {
$successMessage = __('Report saved.');
$report = $this->EventReport->simpleFetchById($this->Auth->user(), $this->EventReport->id);
return $this->__getSuccessResponseBasedOnContext($successMessage, $report, 'add', false, $redirectTarget);
}
}
$this->set('event_id', $eventId);
$this->set('action', 'add');
$this->__injectDistributionLevelToViewContext();
$this->__injectSharingGroupsDataToViewContext();
}
public function view($reportId, $ajax=false)
{
$report = $this->EventReport->simpleFetchById($this->Auth->user(), $reportId);
if ($this->_isRest()) {
return $this->RestResponse->viewData($report, $this->response->type());
}
$this->set('ajax', $ajax);
$this->set('id', $reportId);
$this->set('report', $report);
$this->__injectDistributionLevelToViewContext();
$this->__injectPermissionsToViewContext($this->Auth->user(), $report);
}
public function getProxyMISPElements($reportId)
{
if (!$this->_isRest()) {
throw new MethodNotAllowedException(__('This function can only be reached via the API.'));
}
$report = $this->EventReport->simpleFetchById($this->Auth->user(), $reportId);
$proxyMISPElements = $this->EventReport->getProxyMISPElements($this->Auth->user(), $report['EventReport']['event_id']);
return $this->RestResponse->viewData($proxyMISPElements, $this->response->type());
}
public function viewSummary($reportId)
{
$report = $this->EventReport->simpleFetchById($this->Auth->user(), $reportId);
$proxyMISPElements = $this->EventReport->getProxyMISPElements($this->Auth->user(), $report['EventReport']['event_id']);
$this->set('proxyMISPElements', $proxyMISPElements);
$this->set('id', $reportId);
$this->set('report', $report);
$this->__injectDistributionLevelToViewContext();
$this->__injectPermissionsToViewContext($this->Auth->user(), $report);
}
public function edit($id)
{
$savedReport = $this->EventReport->fetchIfAuthorized($this->Auth->user(), $id, 'edit', $throwErrors=true, $full=true);
if ($this->request->is('post') || $this->request->is('put')) {
$newReport = $this->request->data;
$newReport = $this->__applyDataFromSavedReport($newReport, $savedReport);
$errors = $this->EventReport->editReport($this->Auth->user(), $newReport, $savedReport['EventReport']['event_id']);
$redirectTarget = array('controller' => 'eventReports', 'action' => 'view', $id);
if (!empty($errors)) {
return $this->__getFailResponseBasedOnContext($validationErrors, array(), 'edit', $id, $redirectTarget);
} else {
$successMessage = __('Report saved.');
$report = $this->EventReport->simpleFetchById($this->Auth->user(), $this->EventReport->id);
return $this->__getSuccessResponseBasedOnContext($successMessage, $report, 'edit', $id, $redirectTarget);
}
} else {
$this->request->data = $savedReport;
}
$this->set('id', $savedReport['EventReport']['id']);
$this->set('event_id', $savedReport['EventReport']['event_id']);
$this->set('action', 'edit');
$this->__injectDistributionLevelToViewContext();
$this->__injectSharingGroupsDataToViewContext();
$this->render('add');
}
public function delete($id, $hard=false)
{
$report = $this->EventReport->fetchIfAuthorized($this->Auth->user(), $id, 'delete', $throwErrors=true, $full=false);
if ($this->request->is('post')) {
$errors = $this->EventReport->deleteReport($this->Auth->user(), $report, $hard=$hard);
$redirectTarget = $this->referer();
if (empty($errors)) {
$successMessage = __('Event Report %s %s deleted', $id, $hard ? __('hard') : __('soft'));
$report = $hard ? null : $this->EventReport->simpleFetchById($this->Auth->user(), $id);
return $this->__getSuccessResponseBasedOnContext($successMessage, $report, 'delete', $id, $redirectTarget);
} else {
$errorMessage = __('Event Report %s could not be %s deleted.%sReasons: %s', $id, $hard ? __('hard') : __('soft'), PHP_EOL, json_encode($errors));
return $this->__getFailResponseBasedOnContext($errorMessage, array(), 'edit', $id, $redirectTarget);
}
} else {
if (!$this->request->is('ajax')) {
throw new MethodNotAllowedException(__('This function can only be reached via AJAX.'));
} else {
$this->layout = 'ajax';
$this->set('report', $report);
$this->render('ajax/delete');
}
}
}
public function restore($id)
{
$report = $this->EventReport->fetchIfAuthorized($this->Auth->user(), $id, 'edit', $throwErrors=true, $full=false);
if ($this->request->is('post')) {
$errors = $this->EventReport->restoreReport($this->Auth->user(), $id);
$redirectTarget = $this->referer();
if (empty($errors)) {
$successMessage = __('Event Report %s restored', $id);
$report = $this->EventReport->simpleFetchById($this->Auth->user(), $id);
return $this->__getSuccessResponseBasedOnContext($successMessage, $report, 'restore', $id, $redirectTarget);
} else {
$errorMessage = __('Event Report %s could not be %s restored.%sReasons: %s', $id, PHP_EOL, json_encode($errors));
return $this->__getFailResponseBasedOnContext($errorMessage, array(), 'restore', $id, $redirectTarget);
}
} else {
if (!$this->request->is('ajax')) {
throw new MethodNotAllowedException(__('This function can only be reached via AJAX.'));
} else {
$this->layout = 'ajax';
$this->set('report', $report);
}
}
}
public function index()
{
$filters = $this->IndexFilter->harvestParameters(['event_id', 'value', 'context', 'index_for_event', 'extended_event']);
$filters['embedded_view'] = $this->request->is('ajax');
$compiledConditions = $this->__generateIndexConditions($filters);
if ($this->_isRest()) {
$reports = $this->EventReport->find('all', [
'recursive' => -1,
'conditions' => $compiledConditions,
'contain' => $this->EventReport->defaultContain,
]);
return $this->RestResponse->viewData($reports, $this->response->type());
} else {
$this->paginate['conditions']['AND'][] = $compiledConditions;
$reports = $this->paginate();
$this->set('reports', $reports);
$this->__injectIndexVariablesToViewContext($filters);
if (!empty($filters['index_for_event'])) {
$this->set('extendedEvent', !empty($filters['extended_event']));
$this->render('ajax/indexForEvent');
}
}
}
private function __generateIndexConditions($filters = [])
{
$aclConditions = $this->EventReport->buildACLConditions($this->Auth->user());
$eventConditions = [];
if (!empty($filters['event_id'])) {
$extendingEventIds = [];
if (!empty($filters['extended_event'])) {
$extendingEventIds = $this->EventReport->Event->getExtendingEventIdsFromEvent($this->Auth->user(), $filters['event_id']);
}
$eventConditions = ['EventReport.event_id' => array_merge([$filters['event_id']], $extendingEventIds)];
}
$contextConditions = [];
if (empty($filters['context'])) {
$filters['context'] = 'default';
}
if ($filters['context'] == 'deleted') {
$contextConditions['EventReport.deleted'] = true;
} elseif ($filters['context'] == 'default') {
$contextConditions['EventReport.deleted'] = false;
}
$searchConditions = [];
if (empty($filters['value'])) {
$filters['value'] = '';
} else {
$searchall = '%' . strtolower($filters['value']) . '%';
$searchConditions = array(
'OR' => array(
'LOWER(EventReport.name) LIKE' => $searchall,
'LOWER(EventReport.content) LIKE' => $searchall,
'EventReport.id' => $searchall,
'EventReport.uuid' => $searchall
)
);
}
$compiledConditions = [
'AND' => [
$aclConditions,
$eventConditions,
$contextConditions,
$searchConditions,
]
];
return $compiledConditions;
}
private function __getSuccessResponseBasedOnContext($message, $data = null, $action = '', $id = false, $redirect = array())
{
if ($this->_isRest()) {
if (!is_null($data)) {
return $this->RestResponse->viewData($data, $this->response->type());
} else {
return $this->RestResponse->saveSuccessResponse($this->EventReport->alias, $action, $id, false, $message);
}
} elseif ($this->request->is('ajax')) {
return $this->RestResponse->saveSuccessResponse($this->EventReport->alias, $action, $id, false, $message, $data);
} else {
$this->Flash->success($message);
$this->redirect($redirect);
}
return;
}
private function __getFailResponseBasedOnContext($message, $data = null, $action = '', $id = false, $redirect = array())
{
if (is_array($message)) {
$message = implode(', ', $message);
}
if ($this->_isRest()) {
if (!is_null($data)) {
return $this->RestResponse->viewData($data, $this->response->type());
} else {
return $this->RestResponse->saveFailResponse('EventReport', $action, $id, $message, false);
}
} elseif ($this->request->is('ajax')) {
return $this->RestResponse->saveFailResponse('EventReport', $action, $id, $message, false, $data);
} else {
$this->Flash->error($message);
$this->redirect($this->referer());
}
return;
}
private function __injectIndexVariablesToViewContext($filters)
{
if (!empty($filters['context'])) {
$this->set('context', $filters['context']);
} else {
$this->set('context', 'default');
}
if (!empty($filters['event_id'])) {
$this->set('event_id', $filters['event_id']);
}
if (isset($filters['embedded_view'])) {
$this->set('embedded_view', $filters['embedded_view']);
} else {
$this->set('embedded_view', false);
}
if (!empty($filters['value'])) {
$this->set('searchall', $filters['value']);
} else {
$this->set('searchall', '');
}
$this->__injectDistributionLevelToViewContext();
}
private function __injectDistributionLevelToViewContext()
{
$distributionLevels = $this->EventReport->Event->Attribute->distributionLevels;
$this->set('distributionLevels', $distributionLevels);
$initialDistribution = 5;
$configuredDistribution = Configure::check('MISP.default_attribute_distribution');
if ($configuredDistribution != null && $configuredDistribution != 'event') {
$initialDistribution = $configuredDistribution;
}
$this->set('initialDistribution', $initialDistribution);
}
private function __injectSharingGroupsDataToViewContext()
{
$sgs = $this->EventReport->Event->SharingGroup->fetchAllAuthorised($this->Auth->user(), 'name', 1);
$this->set('sharingGroups', $sgs);
}
private function __injectPermissionsToViewContext($user, $report)
{
$canEdit = $this->EventReport->canEditReport($user, $report) === true;
$this->set('canEdit', $canEdit);
}
private function __canModifyReport($eventId)
{
$event = $this->EventReport->Event->fetchSimpleEvent($this->Auth->user(), $eventId, array());
if (empty($event)) {
throw new NotFoundException(__('Invalid event'));
}
if (!$this->__canModifyEvent($event)) {
throw new ForbiddenException(__('You do not have permission to do that.'));
}
return $event;
}
private function __applyDataFromSavedReport($newReport, $savedReport)
{
if (!isset($newReport['EventReport'])) {
$newReport = array('EventReport' => $newReport);
}
$fieldList = $this->EventReport->captureFields;
$ignoreFieldList = ['id', 'uuid', 'event_id', 'deleted'];
foreach ($fieldList as $field) {
if (!in_array($field, $ignoreFieldList) && isset($newReport['EventReport'][$field])) {
$savedReport['EventReport'][$field] = $newReport['EventReport'][$field];
}
}
return $savedReport;
}
}

View File

@ -627,7 +627,14 @@ class EventsController extends AppController
$v = $filterString;
break;
case 'minimal':
$this->paginate['conditions']['AND'][] = array('NOT' => array('Event.attribute_count' => 0));
$tableName = $this->Event->EventReport->table;
$eventReportQuery = sprintf('EXISTS (SELECT id, deleted FROM %s WHERE %s.event_id = Event.id and %s.deleted = 0)', $tableName, $tableName, $tableName);
$this->paginate['conditions']['AND'][] = [
'OR' => [
['Event.attribute_count >' => 0],
[$eventReportQuery]
]
];
break;
default:
continue 2;

View File

@ -802,7 +802,7 @@ class ServersController extends AppController
}
}
if ($this->_isRest()) {
return $this->RestResponse->saveSuccessResponse('Servers', 'push', array(sprintf(__('Push complete. %s events pushed, %s events could not be pushed.', $result[0], $result[1]))), $this->response->type());
return $this->RestResponse->saveSuccessResponse('Servers', 'push', __('Push complete. %s events pushed, %s events could not be pushed.', count($result[0]), count($result[1])), $this->response->type());
} else {
$this->set('successes', $result[0]);
$this->set('fails', $result[1]);

View File

@ -27,6 +27,12 @@ class TagsController extends AppController
public $helpers = array('TextColour');
public function beforeFilter()
{
parent::beforeFilter();
$this->Security->unlockedActions[] = 'search';
}
public function index($favouritesOnly = false)
{
$this->loadModel('Attribute');
@ -1106,7 +1112,7 @@ class TagsController extends AppController
$this->render('/Events/view_graph');
}
public function search($tag = false)
public function search($tag = false, $strictTagNameOnly = false, $searchIfTagExists = true)
{
if (isset($this->request->data['Tag'])) {
$this->request->data = $this->request->data['Tag'];
@ -1119,41 +1125,59 @@ class TagsController extends AppController
if (!is_array($tag)) {
$tag = array($tag);
}
$conditions = array();
foreach ($tag as $k => $t) {
$tag[$k] = strtolower($t);
$conditions['OR'][] = array('LOWER(GalaxyCluster.value)' => $tag[$k]);
}
foreach ($tag as $k => $t) {
$conditions['OR'][] = array('AND' => array('GalaxyElement.key' => 'synonyms', 'LOWER(GalaxyElement.value) LIKE' => $t));
}
$this->loadModel('GalaxyCluster');
$elements = $this->GalaxyCluster->GalaxyElement->find('all', array(
'recursive' => -1,
'conditions' => $conditions,
'contain' => array('GalaxyCluster.tag_name')
));
foreach ($elements as $element) {
$tag[] = strtolower($element['GalaxyCluster']['tag_name']);
}
$conditions = array();
foreach ($tag as $k => $t) {
$conditions['OR'][] = array('LOWER(Tag.name) LIKE' => $t);
if (!$strictTagNameOnly) {
foreach ($tag as $k => $t) {
$tag[$k] = strtolower($t);
$conditions['OR'][] = array('LOWER(GalaxyCluster.value)' => $tag[$k]);
}
foreach ($tag as $k => $t) {
$conditions['OR'][] = array('AND' => array('GalaxyElement.key' => 'synonyms', 'LOWER(GalaxyElement.value) LIKE' => $t));
}
$elements = $this->GalaxyCluster->GalaxyElement->find('all', array(
'recursive' => -1,
'conditions' => $conditions,
'contain' => array('GalaxyCluster.tag_name')
));
foreach ($elements as $element) {
$tag[] = strtolower($element['GalaxyCluster']['tag_name']);
}
foreach ($tag as $k => $t) {
$conditions['OR'][] = array('LOWER(Tag.name) LIKE' => $t);
}
} else {
foreach ($tag as $k => $t) {
$conditions['OR'][] = array('Tag.name' => $t);
}
}
$tags = $this->Tag->find('all', array(
'conditions' => $conditions,
'recursive' => -1
));
if (!$searchIfTagExists && empty($tags)) {
$tags = [];
foreach ($tag as $i => $tagName) {
$tags[] = ['Tag' => ['name' => $tagName], 'simulatedTag' => true];
}
}
$this->loadModel('Taxonomy');
foreach ($tags as $k => $t) {
$taxonomy = $this->Taxonomy->getTaxonomyForTag($t['Tag']['name'], true);
if (!empty($taxonomy)) {
$dataFound = false;
$taxonomy = $this->Taxonomy->getTaxonomyForTag($t['Tag']['name'], false);
if (!empty($taxonomy) && !empty($taxonomy['TaxonomyPredicate'][0])) {
$dataFound = true;
$tags[$k]['Taxonomy'] = $taxonomy['Taxonomy'];
$tags[$k]['TaxonomyPredicate'] = $taxonomy['TaxonomyPredicate'][0];
}
$cluster = $this->GalaxyCluster->getCluster($t['Tag']['name']);
if (!empty($cluster)) {
$dataFound = true;
$tags[$k]['GalaxyCluster'] = $cluster['GalaxyCluster'];
}
if (!$searchIfTagExists && !$dataFound && !empty($t['simulatedTag'])) {
unset($tags[$k]);
}
}
return $this->RestResponse->viewData($tags, $this->response->type());
}

View File

@ -53,7 +53,7 @@ class JSONConverterTool
public function convert($event, $isSiteAdmin=false, $raw = false)
{
$toRearrange = array('Org', 'Orgc', 'SharingGroup', 'Attribute', 'ShadowAttribute', 'RelatedAttribute', 'RelatedEvent', 'Galaxy', 'Object');
$toRearrange = array('Org', 'Orgc', 'SharingGroup', 'Attribute', 'ShadowAttribute', 'RelatedAttribute', 'RelatedEvent', 'Galaxy', 'Object', 'EventReport');
foreach ($toRearrange as $object) {
if (isset($event[$object])) {
$event['Event'][$object] = $event[$object];

View File

@ -86,7 +86,7 @@ class AppModel extends Model
39 => false, 40 => false, 41 => false, 42 => false, 43 => false, 44 => false,
45 => false, 46 => false, 47 => false, 48 => false, 49 => false, 50 => false,
51 => false, 52 => false, 53 => false, 54 => false, 55 => false, 56 => false,
57 => false, 58 => false
57 => false, 58 => false, 59 => false
);
public $advanced_updates_description = array(
@ -1420,6 +1420,22 @@ class AppModel extends Model
case 58:
$sqlArray[] = "ALTER TABLE `warninglists` MODIFY COLUMN `warninglist_entry_count` int(11) unsigned NOT NULL DEFAULT 0;";
break;
case 59:
$sqlArray[] = "CREATE TABLE IF NOT EXISTS event_reports (
`id` int(11) NOT NULL AUTO_INCREMENT,
`uuid` varchar(40) COLLATE utf8_bin NOT NULL ,
`event_id` int(11) NOT NULL,
`name` varchar(255) NOT NULL,
`content` text,
`distribution` tinyint(4) NOT NULL DEFAULT 0,
`sharing_group_id` int(11),
`timestamp` int(11) NOT NULL,
`deleted` tinyint(1) NOT NULL DEFAULT 0,
PRIMARY KEY (id),
CONSTRAINT u_uuid UNIQUE (uuid),
INDEX `name` (`name`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;";
break;
case 'fixNonEmptySharingGroupID':
$sqlArray[] = 'UPDATE `events` SET `sharing_group_id` = 0 WHERE `distribution` != 4;';
$sqlArray[] = 'UPDATE `attributes` SET `sharing_group_id` = 0 WHERE `distribution` != 4;';

View File

@ -378,6 +378,10 @@ class Event extends AppModel
'Sighting' => array(
'className' => 'Sighting',
'dependent' => true,
),
'EventReport' => array(
'className' => 'EventReport',
'dependent' => true,
)
);
@ -1210,7 +1214,7 @@ class Event extends AppModel
private function __rearrangeEventStructureForSync($event)
{
// rearrange things to be compatible with the Xml::fromArray()
$objectsToRearrange = array('Attribute', 'Object', 'Orgc', 'SharingGroup', 'EventTag', 'Org', 'ShadowAttribute');
$objectsToRearrange = array('Attribute', 'Object', 'Orgc', 'SharingGroup', 'EventTag', 'Org', 'ShadowAttribute', 'EventReport');
foreach ($objectsToRearrange as $o) {
if (isset($event[$o])) {
$event['Event'][$o] = $event[$o];
@ -1277,6 +1281,23 @@ class Event extends AppModel
return $data;
}
private function __prepareEventReportForSync($data, $server)
{
if (!empty($data['EventReport'])) {
foreach ($data['EventReport'] as $key => $report) {
$data['EventReport'][$key] = $this->__updateEventReportForSync($report, $server);
if (empty($data['EventReport'][$key])) {
unset($data['EventReport'][$key]);
}
}
$data['EventReport'] = array_values($data['EventReport']);
}
if (isset($data['EventReport']) && empty($data['EventReport'])) {
unset($data['EventReport']);
}
return $data;
}
private function __updateEventForSync($event, $server)
{
$event = $this->__rearrangeEventStructureForSync($event);
@ -1295,6 +1316,7 @@ class Event extends AppModel
}
$event['Event'] = $this->__prepareAttributesForSync($event['Event'], $server);
$event['Event'] = $this->__prepareObjectsForSync($event['Event'], $server);
$event['Event'] = $this->__prepareEventReportForSync($event['Event'], $server);
// Downgrade the event from connected communities to community only
if (!$server['Server']['internal'] && $event['Event']['distribution'] == 2) {
@ -1388,6 +1410,57 @@ class Event extends AppModel
return $attribute;
}
private function __updateEventReportForSync($report, $server)
{
if (!$server['Server']['internal'] && $report['distribution'] < 2) {
return false;
}
// check if remote version support event reports
$eventReportSupportedByRemote = false;
$uri = $server['Server']['url'] . '/eventReports/add';
$HttpSocket = $this->setupHttpSocket($server, null);
$request = $this->setupSyncRequest($server);
try {
$response = $HttpSocket->get($uri, false, $request);
if ($response->isOk()) {
$apiDescription = json_decode($response->body, true);
$eventReportSupportedByRemote = !empty($apiDescription['description']);
}
} catch (Exception $e) {
$this->Log = ClassRegistry::init('Log');
$message = __('Remote version does not support event report.');
$this->Log->createLogEntry('SYSTEM', $action, 'Server', $id, $message);
}
if (!$eventReportSupportedByRemote) {
return [];
}
// Downgrade the object from connected communities to community only
if (!$server['Server']['internal'] && $report['distribution'] == 2) {
$report['distribution'] = 1;
}
// If the object has a sharing group attached, make sure it can be transferred
if ($report['distribution'] == 4) {
if (!$server['Server']['internal'] && $this->checkDistributionForPush(array('EventReport' => $report), $server, 'EventReport') === false) {
return false;
}
// Add the local server to the list of instances in the SG
if (isset($object['SharingGroup']['SharingGroupServer'])) {
foreach ($object['SharingGroup']['SharingGroupServer'] as &$s) {
if ($s['server_id'] == 0) {
$s['Server'] = array(
'id' => 0,
'url' => $this->__getAnnounceBaseurl(),
'name' => $this->__getAnnounceBaseurl()
);
}
}
}
}
return $report;
}
/**
* Download event from remote server.
*
@ -1485,6 +1558,11 @@ class Event extends AppModel
'table' => 'object_references',
'foreign_key' => 'event_id',
'value' => $id
),
array(
'table' => 'event_reports',
'foreign_key' => 'event_id',
'value' => $id
)
);
if ($thread_id) {
@ -1942,6 +2020,8 @@ class Event extends AppModel
}
$conditionsAttributes = array();
$conditionsObjects = array();
$conditionsObjectReferences = array();
$conditionsEventReport = array();
if (isset($options['flatten']) && $options['flatten']) {
$flatten = true;
@ -1958,10 +2038,12 @@ class Event extends AppModel
}
$attributeCondSelect = '(SELECT events.org_id FROM events WHERE events.id = Attribute.event_id)';
$objectCondSelect = '(SELECT events.org_id FROM events WHERE events.id = Object.event_id)';
$eventReportCondSelect = '(SELECT events.org_id FROM events WHERE events.id = EventReport.event_id)';
if ($this->getDataSource()->config['datasource'] == 'Database/Postgres') {
$schemaName = $this->getDataSource()->config['schema'];
$attributeCondSelect = sprintf('(SELECT "%s"."events"."org_id" FROM "%s"."events" WHERE "%s"."events"."id" = "Attribute"."event_id")', $schemaName, $schemaName, $schemaName);
$objectCondSelect = sprintf('(SELECT "%s"."events"."org_id" FROM "%s"."events" WHERE "%s"."events"."id" = "Object"."event_id")', $schemaName, $schemaName, $schemaName);
$eventReportCondSelect = sprintf('(SELECT "%s"."events"."org_id" FROM "%s"."events" WHERE "%s"."events"."id" = "EventReport"."event_id")', $schemaName, $schemaName, $schemaName);
}
$conditionsAttributes['AND'][0]['OR'] = array(
array('AND' => array(
@ -1986,16 +2068,30 @@ class Event extends AppModel
)),
$objectCondSelect => $user['org_id']
);
$conditionsEventReport['AND'][0]['OR'] = array(
array('AND' => array(
'EventReport.distribution >' => 0,
'EventReport.distribution !=' => 4,
)),
array('AND' => array(
'EventReport.distribution' => 4,
'EventReport.sharing_group_id' => $sgids,
)),
$eventReportCondSelect => $user['org_id']
);
}
if ($options['distribution']) {
$conditions['AND'][] = array('Event.distribution' => $options['distribution']);
$conditionsAttributes['AND'][] = array('Attribute.distribution' => $options['distribution']);
$conditionsObjects['AND'][] = array('Object.distribution' => $options['distribution']);
$conditionsEventReport['AND'][] = array('EventReport.distribution' => $options['distribution']);
}
if ($options['sharing_group_id']) {
$conditions['AND'][] = array('Event.sharing_group_id' => $options['sharing_group_id']);
$conditionsAttributes['AND'][] = array('Attribute.sharing_group_id' => $options['sharing_group_id']);
$conditionsObjects['AND'][] = array('Object.sharing_group_id' => $options['sharing_group_id']);
$conditionsEventReport['AND'][] = array('EventReport.sharing_group_id' => $options['sharing_group_id']);
}
if ($options['from']) {
$conditions['AND'][] = array('Event.date >=' => $options['from']);
@ -2016,7 +2112,7 @@ class Event extends AppModel
$conditions['AND'][] = array('Event.published' => 1);
$conditionsAttributes['AND'][] = array('Attribute.to_ids' => 1);
}
$softDeletables = array('Attribute', 'Object', 'ObjectReference');
$softDeletables = array('Attribute', 'Object', 'ObjectReference', 'EventReport');
if (isset($options['deleted'])) {
if (!is_array($options['deleted'])) {
$options['deleted'] = array($options['deleted']);
@ -2093,6 +2189,7 @@ class Event extends AppModel
$fieldsObj = array('*');
$fieldsShadowAtt = array('ShadowAttribute.id', 'ShadowAttribute.type', 'ShadowAttribute.category', 'ShadowAttribute.value', 'ShadowAttribute.to_ids', 'ShadowAttribute.uuid', 'ShadowAttribute.event_uuid', 'ShadowAttribute.event_id', 'ShadowAttribute.old_id', 'ShadowAttribute.comment', 'ShadowAttribute.org_id', 'ShadowAttribute.proposal_to_delete', 'ShadowAttribute.timestamp', 'ShadowAttribute.first_seen', 'ShadowAttribute.last_seen');
$fieldsOrg = array('id', 'name', 'uuid', 'local');
$fieldsEventReport = array('*');
$sharingGroupData = $this->__cacheSharingGroupData($user, $useCache);
$params = array(
'conditions' => $conditions,
@ -2126,7 +2223,12 @@ class Event extends AppModel
),
'EventTag' => array(
'order' => false
)
),
'EventReport' => array(
'fields' => $fieldsEventReport,
'conditions' => $conditionsEventReport,
'order' => false
)
)
);
if (!empty($options['excludeLocalTags'])) {
@ -2144,6 +2246,7 @@ class Event extends AppModel
unset($params['contain']['Attribute']);
unset($params['contain']['ShadowAttribute']);
unset($params['contain']['Object']);
unset($params['contain']['EventReport']);
}
$results = $this->find('all', $params);
if (empty($results)) {
@ -2331,6 +2434,9 @@ class Event extends AppModel
}
unset($tempObjectAttributeContainer);
}
if (!empty($event['EventReport'])) {
$event['EventReport'] = $this->__attachSharingGroups(!$options['sgReferenceOnly'], $event['EventReport'], $sharingGroupData);
}
if (!empty($event['ShadowAttribute'])) {
if ($isSiteAdmin && $options['includeFeedCorrelations']) {
if (!empty($options['overrideLimit'])) {
@ -3361,7 +3467,7 @@ class Event extends AppModel
return array($bodyevent, $body);
}
private function __captureSGForElement($element, $user, $syncLocal=false)
public function __captureSGForElement($element, $user, $syncLocal=false)
{
if (isset($element['SharingGroup'])) {
$sg = $this->SharingGroup->captureSG($element['SharingGroup'], $user, $syncLocal);
@ -3631,12 +3737,8 @@ class Event extends AppModel
if (!$this->checkEventBlockRules($data)) {
return 'Blocked by event block rules';
}
if (!isset($this->Log)) {
$this->Log = ClassRegistry::init('Log'); // initialize Log class if not
}
if (empty($data['Event']['Attribute']) && empty($data['Event']['Object']) && !empty($data['Event']['published'])) {
$this->Log = ClassRegistry::init('Log');
if (empty($data['Event']['Attribute']) && empty($data['Event']['Object']) && !empty($data['Event']['published']) && empty($data['Event']['EventReport'])) {
$this->Log->create();
$validationErrors['Event'] = 'Received a published event that was empty. Event add process blocked.';
$this->Log->save(array(
@ -3798,7 +3900,8 @@ class Event extends AppModel
'comment',
'deleted'
),
'ObjectRelation' => array()
'ObjectRelation' => array(),
'EventReport' => $this->EventReport->captureFields,
);
$saveResult = $this->save(array('Event' => $data['Event']), array('fieldList' => $fieldList['Event']));
if ($saveResult) {
@ -3876,6 +3979,11 @@ class Event extends AppModel
$this->Log
);
}
if (!empty($data['Event']['EventReport'])) {
foreach ($data['Event']['EventReport'] as $report) {
$result = $this->EventReport->captureReport($user, $report, $this->id);
}
}
// zeroq: check if sightings are attached and add to event
if (isset($data['Sighting']) && !empty($data['Sighting'])) {
$this->Sighting = ClassRegistry::init('Sighting');
@ -4085,6 +4193,18 @@ class Event extends AppModel
}
}
}
if (isset($data['Event']['EventReport'])) {
foreach ($data['Event']['EventReport'] as $i => $report) {
$nothingToChange = false;
$result = $this->EventReport->editReport($user, $report, $this->id, true, $nothingToChange);
if (!empty($result)) {
$validationErrors['EventReport'][] = $result;
}
if (!$nothingToChange) {
$changed = true;
}
}
}
if (isset($data['Event']['Tag']) && $user['Role']['perm_tagger']) {
foreach ($data['Event']['Tag'] as $tag) {
$tag_id = $this->EventTag->Tag->captureTag($tag, $user);
@ -7220,4 +7340,16 @@ class Event extends AppModel
// default value if no match found
return Configure::read('MISP.email_subject_TLP_string') ?: "tlp:amber";
}
public function getExtendingEventIdsFromEvent($user, $eventID)
{
$event = $this->fetchSimpleEvent($user, $eventID);
if (!empty($event)) {
$extendingEventIds = $this->fetchSimpleEventIds($user, ['conditions' => [
'extends_uuid' => $event['Event']['uuid']
]]);
return $extendingEventIds;
}
return [];
}
}

502
app/Model/EventReport.php Normal file
View File

@ -0,0 +1,502 @@
<?php
App::uses('AppModel', 'Model');
class EventReport extends AppModel
{
public $actsAs = array(
'Containable',
'SysLogLogable.SysLogLogable' => array(
'userModel' => 'User',
'userKey' => 'user_id',
'change' => 'full'
),
);
public $validate = array(
'event_id' => array(
'numeric' => array(
'rule' => array('numeric')
)
),
'uuid' => array(
'uuid' => array(
'rule' => array('custom', '/^[a-fA-F0-9]{8}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{12}$/'),
'message' => 'Please provide a valid UUID'
),
'unique' => array(
'rule' => 'isUnique',
'message' => 'The UUID provided is not unique',
'required' => 'create'
)
),
'distribution' => array(
'rule' => array('inList', array('0', '1', '2', '3', '4', '5')),
'message' => 'Options: Your organisation only, This community only, Connected communities, All communities, Sharing group, Inherit event',
'required' => true
)
);
public $captureFields = array('uuid', 'name', 'content', 'distribution', 'sharing_group_id', 'timestamp', 'deleted', 'event_id');
public $defaultContain = array(
'SharingGroup' => array('fields' => array('id', 'name', 'uuid')),
'Event' => array(
'fields' => array('Event.id', 'Event.orgc_id', 'Event.org_id', 'Event.info', 'Event.user_id', 'Event.date'),
'Orgc' => array('fields' => array('Orgc.id', 'Orgc.name')),
'Org' => array('fields' => array('Org.id', 'Org.name'))
)
);
public $belongsTo = array(
'Event' => array(
'className' => 'Event',
'foreignKey' => 'event_id'
),
'SharingGroup' => array(
'className' => 'SharingGroup',
'foreignKey' => 'sharing_group_id'
),
);
public function beforeValidate($options = array())
{
parent::beforeValidate();
// generate UUID if it doesn't exist
if (empty($this->data['EventReport']['uuid'])) {
$this->data['EventReport']['uuid'] = CakeText::uuid();
}
// generate timestamp if it doesn't exist
if (empty($this->data['EventReport']['timestamp'])) {
$date = new DateTime();
$this->data['EventReport']['timestamp'] = $date->getTimestamp();
}
if ($this->data['EventReport']['distribution'] != 4) {
$this->data['EventReport']['sharing_group_id'] = 0;
}
// Set defaults for when some of the mandatory fields don't have defaults
// These fields all have sane defaults either based on another field, or due to server settings
if (!isset($this->data['EventReport']['distribution'])) {
$this->data['EventReport']['distribution'] = Configure::read('MISP.default_attribute_distribution');
if ($report['EventReport']['distribution'] == 'event') {
$report['EventReport']['distribution'] = 5;
}
}
return true;
}
/**
* captureReport Gets a report then save it
*
* @param array $user
* @param array $report
* @param int|string $eventId
* @return array Any errors preventing the capture
*/
public function captureReport(array $user, array $report, $eventId)
{
$this->Log = ClassRegistry::init('Log');
if (!isset($report['EventReport'])) {
$report = ['EventReport' => $report];
}
$report['EventReport']['event_id'] = $eventId;
$report = $this->captureSG($user, $report);
$this->create();
$errors = $this->saveAndReturnErrors($report, ['fieldList' => $this->captureFields]);
if (!empty($errors)) {
$this->Log->createLogEntry($user, 'add', 'EventReport', 0,
__('Event Report dropped due to validation for Event report %s failed: %s', $report['EventReport']['uuid'], ' failed: ' . $report['EventReport']['name']),
__('Validation errors: %s.%sFull report: %s', json_encode($errors), PHP_EOL, json_encode($report['EventReport']))
);
}
return $errors;
}
/**
* addReport Add a report
*
* @param array $user
* @param array $report
* @param int|string $eventId
* @return array Any errors preventing the addition
*/
public function addReport(array $user, array $report, $eventId)
{
$errors = $this->captureReport($user, $report, $eventId);
if (empty($errors)) {
$this->Event->unpublishEvent($eventId);
}
return $errors;
}
/**
* editReport Edit a report
*
* @param array $user
* @param array $report
* @param int|string $eventId
* @param bool $fromPull
* @param bool $nothingToChange
* @return array Any errors preventing the edition
*/
public function editReport(array $user, array $report, $eventId, $fromPull = false, &$nothingToChange = false)
{
$errors = array();
if (!isset($report['EventReport']['uuid'])) {
$errors[] = __('Event Report doesn\'t have an UUID');
return $errors;
}
$report['EventReport']['event_id'] = $eventId;
$existingReport = $this->find('first', array(
'conditions' => array('EventReport.uuid' => $report['EventReport']['uuid']),
'recursive' => -1,
));
if (empty($existingReport)) {
if ($fromPull) {
return $this->captureReport($user, $report, $eventId);
} else {
$errors[] = __('Event Report not found.');
return $errors;
}
}
if ($fromPull) {
if (isset($report['EventReport']['timestamp'])) {
if ($report['EventReport']['timestamp'] <= $existingReport['EventReport']['timestamp']) {
$nothingToChange = true;
return array();
}
}
} else {
unset($report['EventReport']['timestamp']);
}
$errors = $this->saveAndReturnErrors($report, ['fieldList' => $this->captureFields], $errors);
if (empty($errors)) {
$this->Event->unpublishEvent($eventId);
}
return $errors;
}
/**
* deleteReport ACL-aware method to delete the report.
*
* @param array $user
* @param int|string $id
* @param bool $hard
* @return array Any errors preventing the deletion
*/
public function deleteReport(array $user, $report, $hard=false)
{
$report = $this->fetchIfAuthorized($user, $report, 'delete', $throwErrors=true, $full=false);
$errors = [];
if ($hard) {
$deleted = $this->delete($report['EventReport']['id'], true);
if (!$deleted) {
$errors[] = __('Failed to delete report');
}
} else {
$report['EventReport']['deleted'] = true;
$errors = $this->saveAndReturnErrors($report, ['fieldList' => ['deleted']]);
}
if (empty($errors)) {
$this->Event->unpublishEvent($report['EventReport']['event_id']);
}
return $errors;
}
/**
* restoreReport ACL-aware method to restore a report.
*
* @param array $user
* @param int|string $id
* @return array Any errors preventing the restoration
*/
public function restoreReport(array $user, $id)
{
$report = $this->fetchIfAuthorized($user, $id, 'edit', $throwErrors=true, $full=false);
$report['EventReport']['deleted'] = false;
$errors = $this->saveAndReturnErrors($report, ['fieldList' => ['deleted']]);
if (empty($errors)) {
$this->Event->unpublishEvent($report['EventReport']['event_id']);
}
return $errors;
}
private function captureSG(array $user, array $report)
{
$this->Event = ClassRegistry::init('Event');
if (isset($report['EventReport']['distribution']) && $report['EventReport']['distribution'] == 4) {
$report['EventReport'] = $this->Event->__captureSGForElement($report['EventReport'], $user);
}
return $report;
}
/**
* buildACLConditions Generate ACL conditions for viewing the report
*
* @param array $user
* @return array
*/
public function buildACLConditions(array $user)
{
$this->Event = ClassRegistry::init('Event');
$conditions = array();
if (!$user['Role']['perm_site_admin']) {
$sgids = $this->Event->cacheSgids($user, true);
$eventConditions = $this->Event->createEventConditions($user);
$conditions = array(
'AND' => array(
$eventConditions['AND'],
array(
'OR' => array(
'Event.org_id' => $user['org_id'],
'EventReport.distribution' => array('1', '2', '3', '5'),
'AND '=> array(
'EventReport.distribution' => 4,
'EventReport.sharing_group_id' => $sgids,
)
)
)
)
);
}
return $conditions;
}
/**
* fetchById Simple ACL-aware method to fetch a report by Id or UUID
*
* @param array $user
* @param int|string $reportId
* @param bool $throwErrors
* @param bool $full
* @return array
*/
public function simpleFetchById(array $user, $reportId, $throwErrors=true, $full=false)
{
if (Validation::uuid($reportId)) {
$temp = $this->find('first', array(
'recursive' => -1,
'fields' => array("EventReport.id", "EventReport.uuid"),
'conditions' => array("EventReport.uuid" => $reportId)
));
if (empty($temp)) {
if ($throwErrors) {
throw new NotFoundException(__('Invalid report'));
}
return array();
}
$reportId = $temp['EventReport']['id'];
} elseif (!is_numeric($reportId)) {
if ($throwErrors) {
throw new NotFoundException(__('Invalid report'));
}
return array();
}
$options = array('conditions' => array("EventReport.id" => $reportId));
$report = $this->fetchReports($user, $options, $full=$full);
if (!empty($report)) {
return $report[0];
}
if ($throwErrors) {
throw new NotFoundException(__('Invalid report'));
}
return array();
}
/**
* fetchReports ACL-aware method. Basically find with ACL
*
* @param array $user
* @param array $options
* @param bool $full
* @return void
*/
public function fetchReports(array $user, array $options = array(), $full=false)
{
$params = array(
'conditions' => $this->buildACLConditions($user),
'contain' => $this->defaultContain,
'recursive' => -1
);
if ($full) {
$params['recursive'] = 1;
}
if (isset($options['fields'])) {
$params['fields'] = $options['fields'];
}
if (isset($options['conditions'])) {
$params['conditions']['AND'][] = $options['conditions'];
}
if (isset($options['group'])) {
$params['group'] = empty($options['group']) ? $options['group'] : false;
}
$reports = $this->find('all', $params);
return $reports;
}
/**
* fetchIfAuthorized Fetches a report and checks if the user has the authorization to perform the requested operation
*
* @param array $user
* @param int|string|array $report
* @param mixed $authorizations the requested actions to be performed on the report
* @param bool $throwErrors Should the function throws excpetion if users is not allowed to perform the action
* @param bool $full
* @return array The report or an error message
*/
public function fetchIfAuthorized(array $user, $report, $authorizations, $throwErrors=true, $full=false)
{
$authorizations = is_array($authorizations) ? $authorizations : array($authorizations);
$possibleAuthorizations = array('view', 'edit', 'delete');
if (!empty(array_diff($authorizations, $possibleAuthorizations))) {
throw new NotFoundException(__('Invalid authorization requested'));
}
if (isset($report['uuid'])) {
$report['EventReport'] = $report;
}
if (!isset($report['EventReport']['uuid'])) {
$report = $this->simpleFetchById($user, $report, $throwErrors=$throwErrors, $full=$full);
if (empty($report)) {
$message = __('Invalid report');
return array('authorized' => false, 'error' => $message);
}
}
if ($user['Role']['perm_site_admin']) {
return $report;
}
if (in_array('view', $authorizations) && count($authorizations) == 1) {
return $report;
} else {
if (in_array('edit', $authorizations) || in_array('delete', $authorizations)) {
$checkResult = $this->canEditReport($user, $report);
if ($checkResult !== true) {
if ($throwErrors) {
throw new UnauthorizedException($checkResult);
}
return array('authorized' => false, 'error' => $checkResult);
}
}
return $report;
}
}
public function canEditReport(array $user, array $report)
{
if ($user['Role']['perm_site_admin']) {
return true;
}
if (empty($report['Event'])) {
return __('Could not find associated event');
}
if ($report['Event']['orgc_id'] != $user['org_id']) {
return __('Only the creator organisation of the event can modify the report');
}
return true;
}
public function reArrangeReport(array $report)
{
$rearrangeObjects = array('Event', 'SharingGroup');
if (isset($report['EventReport'])) {
foreach ($rearrangeObjects as $ro) {
if (isset($report[$ro]) && !is_null($report[$ro]['id'])) {
$report['EventReport'][$ro] = $report[$ro];
}
unset($report[$ro]);
}
}
return $report;
}
/**
* getProxyMISPElements Extract MISP Elements from an event and make them accessible by their UUID
*
* @param array $user
* @param int|string $eventid
* @return array
*/
public function getProxyMISPElements(array $user, $eventid)
{
$event = $this->Event->fetchEvent($user, ['eventid' => $eventid]);
if (empty($event)) {
throw new NotFoundException(__('Invalid Event'));
}
$event = $event[0];
$parentEventId = $this->Event->fetchSimpleEventIds($user, ['conditions' => [
'uuid' => $event['Event']['extends_uuid']
]]);
$completeEvent = $event;
if (!empty($parentEventId)) {
$parentEvent = $this->Event->fetchEvent($user, ['eventid' => $parentEventId, 'extended' => true]);
if (!empty($parentEvent)) {
$parentEvent = $parentEvent[0];
$completeEvent = $parentEvent;
}
}
$attributes = Hash::combine($completeEvent, 'Attribute.{n}.uuid', 'Attribute.{n}');
$this->AttributeTag = ClassRegistry::init('AttributeTag');
$allTagNames = Hash::combine($event['EventTag'], '{n}.Tag.name', '{n}.Tag');
$attributeTags = Hash::combine($this->AttributeTag->getAttributesTags($completeEvent['Attribute'], true), '{n}.name', '{n}');
$parentEventTags = !empty($parentEvent) ? Hash::combine($parentEvent['EventTag'], '{n}.Tag.name', '{n}.Tag') : [];
$allTagNames = array_merge($allTagNames, $attributeTags, $parentEventTags);
$objects = [];
$templateConditions = [];
$recordedConditions = [];
foreach ($completeEvent['Object'] as $k => $object) {
$objects[$object['uuid']] = $object;
$objectAttributes = [];
foreach ($object['Attribute'] as $i => $objectAttribute) {
$objectAttributes[$objectAttribute['uuid']] = $object['Attribute'][$i];
$objectAttributes[$objectAttribute['uuid']]['object_uuid'] = $object['uuid'];
}
$attributes = array_merge($attributes, $objectAttributes);
$objectAttributeTags = Hash::combine($this->AttributeTag->getAttributesTags($object['Attribute'], true), '{n}.name', '{n}');
$allTagNames = array_merge($allTagNames, $objectAttributeTags);
$uniqueCondition = sprintf('%s.%s', $object['template_uuid'], $object['template_version']);
if (!isset($recordedConditions[$uniqueCondition])) {
$templateConditions['OR'][] = [
'ObjectTemplate.uuid' => $object['template_uuid'],
'ObjectTemplate.version' => $object['template_version']
];
$recordedConditions[$uniqueCondition] = true;
}
}
$templateConditions = empty($templateConditions) ? ['ObjectTemplate.id' => 0] : $templateConditions;
$this->ObjectTemplate = ClassRegistry::init('ObjectTemplate');
$templates = $this->ObjectTemplate->find('all', array(
'conditions' => $templateConditions,
'recursive' => -1,
'contain' => array(
'ObjectTemplateElement' => [
'order' => ['ui-priority' => 'DESC'],
'fields' => ['object_relation', 'type', 'ui-priority']
]
)
));
$objectTemplates = [];
foreach ($templates as $template) {
$objectTemplates[sprintf('%s.%s', $template['ObjectTemplate']['uuid'], $template['ObjectTemplate']['version'])] = $template;
}
$this->Galaxy = ClassRegistry::init('Galaxy');
$allowedGalaxies = $this->Galaxy->getAllowedMatrixGalaxies();
$allowedGalaxies = Hash::combine($allowedGalaxies, '{n}.Galaxy.uuid', '{n}.Galaxy');
$proxyMISPElements = [
'attribute' => $attributes,
'object' => $objects,
'objectTemplates' => $objectTemplates,
'galaxymatrix' => $allowedGalaxies,
'tagname' => $allTagNames
];
return $proxyMISPElements;
}
private function saveAndReturnErrors($data, $saveOptions = [], $errors = [])
{
$saveSuccess = $this->save($data, $saveOptions);
if (!$saveSuccess) {
foreach ($this->validationErrors as $validationError) {
$errors[] = $validationError[0];
}
}
return $errors;
}
}

View File

@ -2536,6 +2536,18 @@ class Server extends AppModel
}
}
}
if (isset($event['Event']['EventReport']) && !empty($event['Event']['EventReport'])) {
foreach ($event['Event']['EventReport'] as $key => $r) {
switch ($r['distribution']) {
case '1':
$event['Event']['EventReport'][$key]['distribution'] = '0';
break;
case '2':
$event['Event']['EventReport'][$key]['distribution'] = '1';
break;
}
}
}
}
// Distribution, set reporter of the event, being the admin that initiated the pull
@ -2565,6 +2577,13 @@ class Server extends AppModel
}
}
}
if (!empty($event['Event']['EventReport'])) {
foreach ($event['Event']['EventReport'] as $report) {
if (empty($report['deleted'])) {
return true;
}
}
}
return false;
}
@ -3002,11 +3021,16 @@ class Server extends AppModel
if (empty($sgIds)) {
$sgIds = array(-1);
}
$tableName = $this->Event->EventReport->table;
$eventReportQuery = sprintf('EXISTS (SELECT id, deleted FROM %s WHERE %s.event_id = Event.id and %s.deleted = 0)', $tableName, $tableName, $tableName);
$findParams = array(
'conditions' => array(
$eventid_conditions_key => $eventid_conditions_value,
'Event.published' => 1,
'Event.attribute_count >' => 0,
'OR' => array(
array('Event.attribute_count >' => 0),
array($eventReportQuery),
),
'OR' => array(
array(
'AND' => array(

View File

@ -0,0 +1,19 @@
<script>
'use strict';
var proxyMISPElements = null
var eventid = '<?= !isset($eventid) ? '' : h($eventid) ?>'
var reportid = '<?= h($reportid) ?>'
var invalidMessage = '<?= __('invalid scope or id') ?>'
</script>
<?php
echo $this->element('genericElements/assetLoader', [
'js' => [
'markdownEditor/event-report',
],
'css' => [
'markdownEditor/event-report',
]
]);
?>

View File

@ -0,0 +1,74 @@
<?php
$formatDifferences = [
__('No html support, typographer & autolinker'),
__('An additional syntax to reference MISP Elements'),
];
$allowedScopes = ['attribute', 'object', 'galaxymatrix' ,'tag'];
$allowedScopesHtml = '<code>' . implode('</code> <code>', $allowedScopes) . '</code>';
?>
<h2><?= __('Markdown format') ?></h2>
<p><?= __('The suported markdown format is similar to %s with some differences:', sprintf('<a href="%s" target="_blank">GFM</a>', 'https://github.github.com/gfm/')) ?></p>
<ul>
<?php foreach($formatDifferences as $formatDifference): ?>
<li><?= $formatDifference ?></li>
<?php endforeach; ?>
</ul>
<h2><?= __('Markdown extended format') ?></h2>
<p><?= __('In order to have a visually pleasant report but more importantly, avoid hardcoding element\'s value or ID, MISP elements such as attributes and objects can be referenced with the following special syntax') ?></p>
<h4 style="text-align: center;">
<code style="font-size: 14px;">@[scope](UUID)</code>
</h4>
<span><?= __('Where:') ?></span>
<ul>
<li><b>scope</b>: <?= __('Is the scope to which the UUID is related to.') ?></li>
<ul>
<li><?= __('Can be one of the following: %s', $allowedScopesHtml) ?></li>
</ul>
<li><b>UUID</b>: <?= __('Is the UUID of the MISP element with only one exception for the tag') ?></li>
</ul>
<span><?= __('Examples:') ?></span>
<ul>
<li><code>@[attribute](5f1accda-cde4-47fc-baf1-6ab8f331dc3b)</code></li>
<li><code>@[object](5f1accda-cde4-47fc-baf1-6ab8f331dc3b)</code></li>
<li><code>@[galaxymatrix](5f1accda-cde4-47fc-baf1-6ab8f331dc3b)</code></li>
</ul>
<h4><?= __('Pictures from attachment-type attributes') ?></h4>
<p><?= __('Syntax for pictures is like the syntax for referencing MISP elements but with two differences:') ?></p>
<ul>
<li><?= __('The addition of the %s character to indicate that the picture should be displayed and not the atttribute', '<code>!</code>') ?></li>
<li><?= __('The scope is fixed to %s as only attributes can contain a file', '<code>attribute</code>') ?></li>
</ul>
<h4 style="text-align: center;">
<code style="font-size: 14px;">@![attribute](UUID)</code>
</h4>
<span><?= __('Examples:') ?></span>
<ul>
<li><code>@![attribute](5f1accda-cde4-47fc-baf1-6ab8f331dc3b)</code></li>
</ul>
<h4><?= __('Tags') ?></h4>
<p><?= __('Syntax for representing tags is similar the syntax for referencing MISP elements but with two differences:') ?></p>
<ul>
<li><?= __('The scope is fixed to %s', '<code>tag</code>') ?></li>
<li><?= __('The UUID is replaced by the tag name sa tags don\'t have UUID') ?></li>
</ul>
<span><?= __('Examples:') ?></span>
<ul>
<li><code>@[tag](tlp:green)</code></li>
<li><code>@[tag](misp-galaxy:threat-actor="APT 29")</code></li>
</ul>
<h4><?= __('Event\'s Galaxy matrixes') ?></h4>
<p><?= __('Syntax for embedding the ATT&CK matrix or any other galaxy matrixes is similar to the syntax for referencing MISP elements:') ?></p>
<ul>
<li><?= __('The scope is fixed to %s', '<code>galaxymatrix</code>') ?></li>
<li><?= __('The matrix will be generated for the whole event for which the report is linked to') ?></li>
</ul>
<span><?= __('Examples:') ?></span>
<ul>
<li><code>@[galaxymatrix](5f1accda-cde4-47fc-baf1-6ab8f331dc3b)</code></li>
</ul>

View File

@ -25,6 +25,7 @@
}
}
?>
<br />
<div class="pagination">
<ul>
<?php

View File

@ -0,0 +1,56 @@
<?php
/*
* Form proposing different delete methods
*
* - title: Title of the form
* - modelName: The model of the deleted element
* - value: The value being deleted
* - id: the ID of the value being deleted
* - additionaMessage: array of message to be inserted
* - softDeleteURL: The optional soft delete URL to POST to
* - hardDeleteURL: The hard delete URL to POST to
* - doNotShowHelp: If help text for soft/hard deleting should not be shown
*
*/
?>
<div class="confirmation">
<? if (!empty($title)): ?>
<legend><?= h($title) ?></legend>
<? endif; ?>
<div style="padding-left:5px;padding-right:5px;padding-bottom:5px;">
<h6><?= __('Are you sure you want to delete %s %s (%s)?', h($modelName), sprintf('<i style="font-size: larger">%s</i>', h($value)), h($id)) ?></h6>
<ul>
<?php if (!isset($doNotShowHelp) || !$doNotShowHelp ): ?>
<li><?= sprintf(__('%s a %s propagates the deletion to other instances and lets you restore it in the future'), sprintf('<strong class="blue">%s</strong>', __('Soft-deleting')), h($modelName)) ?></li>
<li><?= sprintf(__('%s a %s permanentaly deletes it'), sprintf('<strong class="red">%s</strong>', __('Hard-deleting')), h($modelName)) ?></li>
<?php endif; ?>
</ul>
<?php if (!empty($additionalMessage)): ?>
<ul>
<li><?= implode('</li><li>', $additionalMessage) ?></li>
</ul>
<?php endif; ?>
<div style="display: flex">
<?php
if (!empty($softDeleteURL)) {
echo $this->Form->postButton(
'<i class="' . $this->FontAwesome->getClass('trash') . ' fa-trash"></i> ' . __('Soft-delete'),
$softDeleteURL,
array('class' => 'btn btn-primary')
);
echo '<span style="width: 0.5em";></span>';
}
$hardDeleteText = !empty($softDeleteURL) ? __('Hard-delete') : __('Delete');
echo $this->Form->postButton(
'<i class="' . $this->FontAwesome->getClass('ban') . ' fa-ban"></i> ' . $hardDeleteText,
$hardDeleteURL,
array('class' => 'btn btn-danger')
);
?>
<button type="button" tabindex="0" aria-label="<?php echo __('Cancel');?>" class="btn btn-inverse" style="margin-left: auto; height: fit-content;" id="PromptNoButton" onClick="cancelPrompt();"><?php echo __('No');?></span>
</div>
</div>
<?php
echo $this->Form->end();
?>
</div>

View File

@ -132,6 +132,11 @@ $divider = $this->element('/genericElements/SideMenu/side_menu_divider');
'url' => $baseurl . '/attributes/add_attachment/' . $eventId,
'text' => __('Add Attachment')
));
echo $this->element('/genericElements/SideMenu/side_menu_link', array(
'element_id' => 'add',
'url' => '/eventReports/add/' . h($event['Event']['id']),
'text' => __('Add Event Report')
));
echo $this->element('/genericElements/SideMenu/side_menu_link', array(
'element_id' => 'populateFrom',
'onClick' => array(
@ -407,6 +412,37 @@ $divider = $this->element('/genericElements/SideMenu/side_menu_divider');
}
break;
case 'eventReports':
echo $this->element('/genericElements/SideMenu/side_menu_link', array(
'element_id' => 'index',
'url' => '/eventReports/index',
'text' => __('List Event Reports')
));
if ($isAclAdd) {
echo $this->element('/genericElements/SideMenu/side_menu_link', array(
'element_id' => 'add',
'text' => __('Add Event Report'),
'title' => __('Add Event Report'),
'onClick' => array(
'function' => 'openIdSelection',
'params' => array('this', 'eventReports', 'add')
),
));
}
if ($menuItem === 'view' || $menuItem === 'edit') {
echo $this->element('/genericElements/SideMenu/side_menu_link', array(
'element_id' => 'view',
'url' => '/eventReports/view/' . h($id),
'text' => __('View Event Report')
));
echo $this->element('/genericElements/SideMenu/side_menu_link', array(
'element_id' => 'edit',
'url' => '/eventReports/edit/' . h($id),
'text' => __('Edit Event Report')
));
}
break;
case 'regexp':
echo $this->element('/genericElements/SideMenu/side_menu_link', array(
'url' => sprintf(

View File

@ -26,6 +26,9 @@ if (empty($url)) {
} else {
$a = 'href="' . $this->get('baseurl') . h($url) . '"';
}
if (!empty($title)) {
$a .= ' title="' . h($title) . '"';
}
if (!empty($onClick)) {
$params = '';
foreach ($onClick['params'] as $param) {

View File

@ -9,6 +9,8 @@
* - paragraph: Text to be displayed (optional)
* - html: HTML to be displayed directly (optional)
* - code: Code snipet to be displayed - copy pastable (optional)
* - type: Controls the size of the modal (`xl` or `lg`)
* - class: A class to be applied on the modal (For reference or customization)
*/
$contents = '';
foreach ($data['content'] as $content) {
@ -23,7 +25,9 @@
$action = $this->request->params['action'];
$controller = $this->request->params['controller'];
echo sprintf(
'<div id="genericModal" class="modal hide fade" tabindex="-1" role="dialog" aria-labelledby="genericModalLabel" aria-hidden="true">%s%s%s</div>',
'<div id="genericModal" class="modal %s hide fade %s" tabindex="-1" role="dialog" aria-labelledby="genericModalLabel" aria-hidden="true">%s%s%s</div>',
isset($type) ? sprintf('modal-%s', $type) : '',
isset($class) ? $class : '',
sprintf(
'<div class="modal-header"><button type="button" class="close" data-dismiss="modal" aria-hidden="true">×</button><h3 id="genericModalLabel">%s</h3></div>',
empty($data['title']) ?
@ -31,7 +35,8 @@
h($data['title'])
),
sprintf(
'<div class="modal-body modal-body-long">%s</div>',
'<div class="modal-body modal-body-%s">%s</div>',
isset($type) ? $type : 'long',
$contents
),
sprintf(

View File

@ -0,0 +1,219 @@
<?php
/**
* additionalMarkdownElements: List of custom elements linked to the markdown view to be injected
* [
* 'path' => 'The path of the element',
* 'variables' => 'List of variables to be passed on the element'
* ]
* additionalMarkdownHelpModalElements: List of custom elements linked to the help modal to be injected
* [
* 'path' => 'The path of the element',
* 'tab_name' => 'The name of the navigation tab'
* ]
*/
$insideModal = isset($insideModal) ? $insideModal : false;
if ($canEdit) {
if (!empty($additionalMarkdownHelpModalElements)) {
foreach ($additionalMarkdownHelpModalElements as $i => $additionalHelpModal) {
$additionalMarkdownHelpModalElements[$i]['tab_content'] = $this->element($additionalHelpModal['path']);
}
}
echo $this->element('markdownEditor/markdownEditorHelpModal', ['additionalMarkdownHelpModalElements' => $additionalMarkdownHelpModalElements]);
}
?>
<div id="mardown-viewer-toolbar" class="btn-toolbar">
<div class="btn-group">
<?php if ($canEdit && !$insideModal): ?>
<button type="button" class="btn" data-togglemode="editor" onclick="setMode('editor')">
<i class="<?= $this->FontAwesome->getClass('edit') ?> fa-edit"></i>
<?= __('Edit') ?>
</button>
<button type="button" class="btn" data-togglemode="splitscreen" onclick="setMode('splitscreen')">
<i class="<?= $this->FontAwesome->getClass('columns') ?> fa-columns"></i>
<?= __('Split Screen') ?>
</button>
<?php endif; ?>
<button type="button" class="btn btn-inverse" data-togglemode="viewer" onclick="setMode('viewer')">
<i class="<?= $this->FontAwesome->getClass('markdown') ?> fa-markdown"></i>
<?= __('Markdown') ?>
</button>
<button type="button" class="btn" data-togglemode="raw" onclick="setMode('raw')">
<i class="<?= $this->FontAwesome->getClass('code') ?> fa-code"></i>
<?= __('Raw') ?>
</button>
</div>
<div class="btn-group">
<?php if ($canEdit && !$insideModal): ?>
<button id="saveMarkdownButton" type="button" class="btn btn-primary" onclick="saveMarkdown()">
<i class="<?= $this->FontAwesome->getClass('save') ?>"></i>
<?= __('Save') ?>
</button>
<?php endif; ?>
<?php if (!$insideModal): ?>
<button class="btn btn-primary dropdown-toggle" data-toggle="dropdown">
<?= __('Menu') ?>
<span class="caret"></span>
</button>
<ul id="markdownDropdownGeneralMenu" class="dropdown-menu">
<li class="dropdown-submenu">
<a tabindex="-1" href="#">
<span class="icon"><i class="<?= $this->FontAwesome->getClass('download') ?>"></i></span>
<?= __('Download') ?>
</a>
<ul class="dropdown-menu">
<li><a tabindex="-1" href="#" onclick="downloadMarkdown('pdf')">
<span class="icon"><i class="<?= $this->FontAwesome->getClass('file-pdf') ?>"></i></span>
<?= __('Download PDF (via print)') ?>
</a></li>
<li><a tabindex="-1" href="#" onclick="downloadMarkdown('text')">
<span class="icon"><i class="<?= $this->FontAwesome->getClass('markdown') ?>"></i></span>
<?= __('Download Markdown') ?>
</a></li>
<li><a tabindex="-1" href="#" title="<?= __('Replace custom syntax by a valid one') ?>" onclick="downloadMarkdown('text-gfm')">
<span class="icon"><i class="<?= $this->FontAwesome->getClass('markdown') ?>"></i></span>
<?= __('Download GFM simplified format') ?>
</a></li>
</ul>
</li>
<li class="dropdown-submenu">
<a tabindex="-1" href="#">
<span class="icon"><i class="<?= $this->FontAwesome->getClass('markdown') ?>"></i></span>
<?= __('Markdown parsing rules') ?>
</a>
<ul id="markdown-dropdown-rules-menu" class="dropdown-menu">
<li><a tabindex="-1" href="#" style="min-width: 200px;" onclick="markdownItToggleRule('image', arguments[0]); return false;" >
<span class="icon"><i class="<?= $this->FontAwesome->getClass('image') ?>"></i></span>
<span class="ruleText"><?= __('Image parsing') ?></span>
<span id="markdownparsing-image-parsing-enabled" class="bold green hidden" style="float: right;"><?= __('enabled') ?></span>
<span id="markdownparsing-image-parsing-disabled" class="bold red" style="float: right;"><?= __('disabled') ?></span>
</a></li>
<li><a tabindex="-1" href="#" style="min-width: 200px;" onclick="markdownItToggleRule('link', arguments[0]); return false;" >
<span class="icon"><i class="<?= $this->FontAwesome->getClass('link') ?>"></i></span>
<span class="ruleText"><?= __('Link parsing') ?></span>
<span id="markdownparsing-link-parsing-enabled" class="bold green hidden" style="float: right;"><?= __('enabled') ?></span>
<span id="markdownparsing-link-parsing-disabled" class="bold red" style="float: right;"><?= __('disabled') ?></span>
</a></li>
</ul>
</li>
</ul>
<?php elseif($canEdit && !empty($editRedirect)): ?>
<a id="saveMarkdownButton" type="button" class="btn btn-primary" href="<?= h($editRedirect) ?>" target="_blank">
<i class="<?= $this->FontAwesome->getClass('edit') ?>"></i>
<?= __('Edit report') ?>
</a>
<?php endif; ?>
</div>
<?php if ($canEdit && !$insideModal): ?>
<button type="button" class="btn btn-primary" onclick="showHelp()">
<i class="<?= $this->FontAwesome->getClass('question-circle') ?> fa-question-circle"></i>
<?= __('Help') ?>
</button>
<?php endif; ?>
</div>
<div class="raw-container">
<pre id="raw"><?php echo h($markdown) ?></pre>
</div>
<div class="split-container">
<div id="editor-container">
<div id="editor-subcontainer">
<div id="top-bar" class="editor-action-bar">
<span class="<?= $this->FontAwesome->getClass('bold') ?> useCursorPointer icon" onclick="replacementAction('bold')" title="<?= __('Bold') ?>"></span>
<span class="<?= $this->FontAwesome->getClass('italic') ?> useCursorPointer icon" onclick="replacementAction('italic')" title="<?= __('Italic') ?>"></span>
<span class="<?= $this->FontAwesome->getClass('heading') ?> useCursorPointer icon" onclick="replacementAction('heading')" title="<?= __('Heading') ?>"></span>
<span class="<?= $this->FontAwesome->getClass('strikethrough') ?> useCursorPointer icon" onclick="replacementAction('strikethrough')" title="<?= __('Strikethrough') ?>"></span>
<i class="top-bar-separator"></i>
<span class="<?= $this->FontAwesome->getClass('list-ul') ?> useCursorPointer icon" onclick="replacementAction('list-ul')" title="<?= __('Unordered list') ?>"></span>
<span class="<?= $this->FontAwesome->getClass('list-ol') ?> useCursorPointer icon" onclick="replacementAction('list-ol')" title="<?= __('Ordered list') ?>"></span>
<i class="top-bar-separator"></i>
<span class="<?= $this->FontAwesome->getClass('quote-left') ?> useCursorPointer icon" onclick="replacementAction('quote')" title="<?= __('Quote') ?>"></span>
<span class="<?= $this->FontAwesome->getClass('code') ?> useCursorPointer icon" onclick="replacementAction('code')" title="<?= __('Code') ?>"></span>
<span class="<?= $this->FontAwesome->getClass('table') ?> useCursorPointer icon" onclick="replacementAction('table')" title="<?= __('Table') ?>"></span>
</div>
<textarea id="editor"></textarea>
<div id="bottom-bar" class="editor-action-bar">
<span id="lastModifiedField" title="<?= __('Last updated') ?>" class="label"></span>
<span>
<span title="<?= __('Toggle autocompletion while typing'); ?>">
<input type="checkbox" id="autocompletionCB" style="margin: 0 2px 0 0" checked="checked"></input>
<span class="<?= $this->FontAwesome->getClass('magic') ?> useCursorPointer icon" onclick="$autocompletionCB[0].checked = !$autocompletionCB[0].checked"></span>
</span>
</span>
<span>
<span title="<?= __('Synchronize scrolling'); ?>">
<input type="checkbox" id="syncScrollCB" style="margin: 0 2px 0 0" checked="checked"></input>
<span class="<?= $this->FontAwesome->getClass('link') ?> useCursorPointer icon" onclick="$syncScrollCB[0].checked = !$syncScrollCB[0].checked"></span>
</span>
</span>
<span>
<span title="<?= __('Automatically render markdown when typing'); ?>">
<input type="checkbox" id="autoRenderMarkdownCB" style="margin: 0 2px 0 0" checked="checked"></input>
<span class="<?= $this->FontAwesome->getClass('markdown') ?> useCursorPointer icon" onclick="$autoRenderMarkdownCB[0].checked = !$autoRenderMarkdownCB[0].checked"></span>
</span>
</span>
<span style="margin-left: auto">
<span title="<?= __('Toggle fullscreen mode'); ?>">
<span id="toggleFullScreenMode" class="<?= $this->FontAwesome->getClass('expand-arrows-alt') ?> useCursorPointer icon" onclick="toggleFullscreenMode()"></span>
</span>
</span>
</div>
</div>
<div id="resizable-handle" class="ui-resizable-handle ui-resizable-e"></div>
</div>
<div id="viewer-container">
<div id="viewer"></div>
</div>
</div>
<div id="loadingBackdrop" class="modal-backdrop" style="display: none"></div>
<script>
'use strict';
var md, cm;
var saveConfirmMessage = '<?= __('You are about to save the document. Do you wish to proceed?') ?>'
var saveSuccessMessage = '<?= 'Markdown saved' ?>'
var saveFailedMessage = '<?= 'Could not save markdown. Reason' ?>'
var savePDFConfirmMessage = '<?= __('In order to save the PDF, you have to set the print destination to `Save as PDF`.') ?>'
var confirmationMessageUnsavedChanges = '<?= __('You are about to leave the page with unsaved changes. Do you want to proceed?') ?>'
var changeDetectedMessage = '<?= __('Unsaved changes') ?>'
var canEdit = <?= $canEdit ? 'true' : 'false' ?>;
var originalRaw = <?= json_encode(is_array($markdown) ? $markdown : array($markdown), JSON_HEX_TAG); ?>[0];
var lastModified = '<?= h($lastModified) ?>' + '000'
</script>
<?php
echo $this->element('genericElements/assetLoader', array(
'js' => array(
'doT',
'markdown-it',
'highlight.min',
'FileSaver',
),
'css' => array(
'highlight.min',
)
));
if ($canEdit) {
echo $this->element('genericElements/assetLoader', array(
'js' => array(
'moment-with-locales',
'codemirror/codemirror',
'codemirror/modes/markdown',
'codemirror/addons/simplescrollbars',
'codemirror/addons/show-hint',
),
'css' => array(
'codemirror',
'codemirror/simplescrollbars',
'codemirror/show-hint',
)
));
}
echo $this->element('genericElements/assetLoader', array(
'js' => array('markdownEditor/markdownEditor'),
'css' => array('markdownEditor/markdownEditor')
));
if (!empty($additionalMarkdownElements)) {
echo $this->element($additionalMarkdownElements['path'], $additionalMarkdownElements['variables']);
}
?>

View File

@ -0,0 +1,105 @@
<?php
/**
* Accept `additionalMarkdownHelpModalElements` to add new tags. Format
* additionalMarkdownHelpModalElements = {
* 'tab_name' => 'The name of the tab',
* 'tab_content' => 'The content of the tab',
* }
*
*/
$additionalTabsHTML = '';
$additionalTabContentHTML = '';
if (!empty($additionalMarkdownHelpModalElements)) {
foreach ($additionalMarkdownHelpModalElements as $i => $tab) {
$additionalMarkdownHelpModalElements[$i]['tab_id'] = 'tab-' . preg_replace('/[^a-zA-Z0-9]/', '_', $tab['tab_name']);
$additionalTabsHTML .= sprintf('<li><a href="#%s">%s</a></li>', $additionalMarkdownHelpModalElements[$i]['tab_id'], $additionalMarkdownHelpModalElements[$i]['tab_name']);
$additionalTabContentHTML .= sprintf('<div class="tab-pane active" id="%s">', $additionalMarkdownHelpModalElements[$i]['tab_id']);
$additionalTabContentHTML .= $tab['tab_content'];
$additionalTabContentHTML .= '</div>';
}
}
$formatDifferences = [
__('No html support, typographer & autolinker'),
__('An additional syntax to reference MISP Elements'),
];
$shortcutsTableHeader = [__('Command'), __('Action')];
$shortcuts = [
['<kbd>' . implode('</kbd><kbd>', ['ctrl', ' + ', 'space']) . '</kbd>', __('Triggers autocomplete if applicable')],
['<kbd>' . implode('</kbd><kbd>', ['ctrl', ' + ', 'b']) . '</kbd>', __('Makes text bold')],
['<kbd>' . implode('</kbd><kbd>', ['ctrl', ' + ', 'i']) . '</kbd>', __('Makes text italic')],
['<kbd>' . implode('</kbd><kbd>', ['ctrl', ' + ', 'm']) . '</kbd>', __('Insert a MISP Element')],
['<kbd>' . implode('</kbd><kbd>', ['ctrl', ' + ', 'h']) . '</kbd>', __('Makes text as header')],
];
$helpHTML = '';
$helpHTML .= '<ul class="nav nav-tabs" id="tab-markdown-help">';
$helpHTML .= $additionalTabsHTML;
$helpHTML .= sprintf('<li class=""><a href="#tab-editor">%s</a></li>', __('Editor shortcuts'));
$helpHTML .= sprintf('<li class=""><a href="#tab-plugin">%s</a></li>', __('Markdown plugin'));
$helpHTML .= '</ul class="nav nav-tabs">';
$helpHTML .= '<div class="tab-content">';
$helpHTML .= $additionalTabContentHTML;
$helpHTML .= '<div class="tab-pane" id="tab-editor">';
$helpBodyHTML = '';
foreach ($shortcuts as $sc) {
$helpBodyHTML .= sprintf('<tr><td><kbd>%s</kbd></td><td>%s</td></tr>', $sc[0], $sc[1]);
}
$helpHTML .= sprintf('<h2>%s</h2>', __('Editor shortcuts'));
$helpHTML .= sprintf('<table class="table table-bordered table-condensed"><thead><tr>%s</tr></thead><tbody>%s</tbody></table>',
'<th>' . implode('</th><th>', $shortcutsTableHeader) . '</th>',
$helpBodyHTML
);
$helpHTML .= '</div>';
$helpHTML .= '<div class="tab-pane" id="tab-plugin">';
$helpHTML .= sprintf('<h2>%s</h2>', __('Markdown plugins'));
$helpHTML .= sprintf('<h3>%s</h3>', __('Highlighted language'));
$helpHTML .= sprintf('<p>%s</p>', __('Languages rendered in code block can be highlighted using the %s plugin. The list of supported languages can be found %s.',
sprintf('<a href="%s">%s</a>', 'https://highlightjs.org/', 'highlight.js'),
sprintf('<a href="%s">%s</a>', 'https://github.com/highlightjs/highlight.js/blob/master/SUPPORTED_LANGUAGES.md', 'here')
));
$helpHTML .= '</div>';
$helpHTML .= '</div>';
$data = array(
'title' => __('Markdown viewer help'),
'content' => array(
array(
'html' => $helpHTML
),
)
);
echo $this->element('genericElements/infoModal', array('data' => $data, 'type' => 'lg', 'class' => 'markdown-modal-helper'));
?>
<script>
$(document).ready(function() {
$('#tab-markdown-help a').click(function (e) {
e.preventDefault();
$(this).tab('show');
})
$('#tab-markdown-help > li:first').addClass('active')
})
</script>
<style>
/* borrowing from BS4 */
kbd {
padding: .2rem .4rem;
font-size: 87.5%;
color: #fff;
background-color: #212529;
border-radius: .2rem;
}
kbd kbd {
padding: 0;
font-size: 100%;
font-weight: 700;
}
</style>

View File

@ -0,0 +1,58 @@
<?php
$modelForForm = 'EventReport';
echo $this->element('genericElements/Form/genericForm', array(
'form' => $this->Form,
'data' => array(
'title' => $action == 'add' ? __('Add Event Report (event %s)', h($event_id)) : __('Edit Event Report %s (event %s)', h($id), h($event_id)),
'model' => 'EventReport',
'fields' => array(
array(
'field' => 'name',
'class' => 'input',
'stayInLine' => 1
),
array(
'field' => 'distribution',
'class' => 'input',
'options' => $distributionLevels,
'default' => isset($attribute['Attribute']['distribution']) ? $attribute['Attribute']['distribution'] : $initialDistribution,
'stayInLine' => 1
),
array(
'field' => 'sharing_group_id',
'class' => 'input',
'options' => $sharingGroups,
'label' => __("Sharing Group")
),
array(
'field' => 'content',
'class' => 'textarea input span6'
),
array(
'field' => 'event_id',
'default' => $event_id,
'type' => 'hidden'
)
),
'submit' => array(
'action' => $this->request->params['action'],
'ajaxSubmit' => sprintf('submitPopoverForm(\'%s\', \'addEventReport\', 0, 1)', h($event_id))
),
)
));
?>
</div>
<?php
if (empty($ajax)) {
echo $this->element('/genericElements/SideMenu/side_menu', array('menuList' => 'eventReports', 'menuItem' => $this->request->params['action']));
}
?>
<script type="text/javascript">
$(document).ready(function() {
$('#EventReportDistribution').change(function() {
checkSharingGroup('EventReport');
});
checkSharingGroup('EventReport');
});
</script>

View File

@ -0,0 +1,10 @@
<?php
echo $this->element('/genericElements/Form/hardSoftDeleteForm', [
'title' => __('Delete Event Report'),
'modelName' => __('report'),
'value' => $report['EventReport']['name'],
'id' => $report['EventReport']['id'],
'softDeleteURL' => sprintf('%s/eventReports/delete/%s', $baseurl, $report['EventReport']['id']),
'hardDeleteURL' => sprintf('%s/eventReports/delete/%s/1', $baseurl, $report['EventReport']['id']),
]);
?>

View File

@ -0,0 +1,185 @@
<div id="eventReportQuickIndex">
<?php if ($extendedEvent): ?>
<div class="alert alert-info"><?= __('Viewing reports in extended event view') ?></div>
<?php endif; ?>
<div style="margin-bottom: 10px;">
<button class="btn btn-small btn-primary" onclick="openGenericModal(baseurl + '/eventReports/add/<?= h($event_id) ?>')">
<i class="<?= $this->FontAwesome->getClass('plus') ?>"></i> <?= __('Add Event Report') ?>
</button>
</div>
<?php
echo $this->element('/genericElements/IndexTable/index_table', array(
'paginatorOptions' => array(
'update' => '#eventreport_index_div',
),
'data' => array(
'data' => $reports,
'top_bar' => array(
'children' => array(
array(
'type' => 'simple',
'children' => array(
array(
'active' => $context === 'all',
'url' => sprintf('%s/eventReports/index/event_id:%s/index_for_event:1/context:all', $baseurl, h($event_id)),
'text' => __('All'),
),
array(
'active' => $context === 'default',
'class' => 'defaultContext',
'url' => sprintf('%s/eventReports/index/event_id:%s/index_for_event:1/context:default', $baseurl, h($event_id)),
'text' => __('Default'),
),
array(
'active' => $context === 'deleted',
'url' => sprintf('%s/event_reports/index/event_id:%s/index_for_event:1/context:deleted', $baseurl, h($event_id)),
'text' => __('Deleted'),
),
)
)
)
),
'primary_id_path' => 'EventReport.id',
'skip_pagination' => count($reports) < 10,
'fields' => array(
array(
'name' => __('Id'),
'sort' => 'id',
'class' => 'short',
'data_path' => 'EventReport.id',
),
array(
'name' => __('Name'),
'class' => 'useCursorPointer',
'data_path' => 'EventReport.name',
),
array(
'name' => __('Event ID'),
'requirement' => $extendedEvent,
'data_path' => 'EventReport.event_id',
'class' => 'short',
'element' => 'links',
'data_path' => 'EventReport.event_id',
'url' => $baseurl . '/events/view/%s'
),
array(
'name' => __('Last update'),
'sort' => 'timestamp',
'class' => 'short',
'element' => 'datetime',
'data_path' => 'EventReport.timestamp',
),
array(
'name' => __('Distribution'),
'element' => 'distribution_levels',
'class' => 'short',
'data_path' => 'EventReport.distribution',
)
),
'actions' => array(
array(
'url' => '/eventReports/view',
'url_params_data_paths' => array(
'EventReport.id'
),
'icon' => 'eye',
'dbclickAction' => true
),
array(
'url' => '/eventReports/edit',
'url_params_data_paths' => array(
'EventReport.id'
),
'icon' => 'edit'
),
array(
'title' => __('Delete'),
'icon' => 'trash',
'onclick' => 'simplePopup(\'' . $baseurl . '/event_reports/delete/[onclick_params_data_path]\');',
'onclick_params_data_path' => 'EventReport.id',
'complex_requirement' => array(
'function' => function ($row, $options) {
return ($options['me']['Role']['perm_site_admin'] || $options['me']['org_id'] == $options['datapath']['orgc']) && !$options['datapath']['deleted'];
},
'options' => array(
'me' => $me,
'datapath' => array(
'orgc' => 'EventReport.orgc_id',
'deleted' => 'EventReport.deleted'
)
)
),
),
array(
'title' => __('Restore report'),
'url' => $baseurl . '/event_reports/restore',
'url_params_data_paths' => array('EventReport.id'),
'icon' => 'trash-restore',
'postLink' => true,
'postLinkConfirm' => __('Are you sure you want to restore the Report?'),
'complex_requirement' => array(
'function' => function ($row, $options) {
return ($options['me']['Role']['perm_site_admin'] || $options['me']['org_id'] == $options['datapath']['orgc']) && $options['datapath']['deleted'];
},
'options' => array(
'me' => $me,
'datapath' => array(
'orgc' => 'EventReport.orgc_id',
'deleted' => 'EventReport.deleted'
)
)
),
),
)
)
));
?>
</div>
<script>
var loadingSpanAnimation = '<span id="loadingSpan" class="fa fa-spin fa-spinner" style="margin-left: 5px;"></span>';
$(document).ready(function() {
$('#eventReportQuickIndex td[data-path="EventReport.name"]').click(function() {
var reportId = $(this).closest('tr').data('primary-id')
openGenericModal('/eventReports/viewSummary/' + reportId)
})
$('#eventReportQuickIndex .btn-toolbar a.btn').click(function(e) {
e.preventDefault()
$("#eventreport_index_div").empty()
.append(
$('<div></div>')
.css({'text-align': 'center', 'font-size': 'large', 'margin': '5px 0'})
.append(loadingSpanAnimation)
)
var url = $(this).attr('href')
$.get(url, function(data) {
$("#eventreport_index_div").html(data);
});
});
})
function reloadEventReportTable() {
var url = $("#eventReportQuickIndex a.defaultContext").attr('href')
$.ajax({
dataType:"html",
beforeSend: function() {
$("#eventreport_index_div").empty()
.append(
$('<div></div>')
.css({'text-align': 'center', 'font-size': 'large', 'margin': '5px 0'})
.append(loadingSpanAnimation)
)
},
success:function (data) {
$("#eventreport_index_div").html(data);
},
error: function (jqXHR, textStatus, errorThrown) {
$("#eventreport_index_div").empty().text('<?= __('Failed to load Event report table')?>')
showMessage('fail', textStatus + ": " + errorThrown);
},
url:url
});
}
</script>

View File

@ -0,0 +1,154 @@
<?php
if(!$embedded_view) {
echo '<div class="index">';
}
echo $this->element('/genericElements/IndexTable/index_table', array(
'data' => array(
'data' => $reports,
'top_bar' => array(
'children' => array(
array(
'type' => 'simple',
'children' => array(
array(
'active' => $context === 'all',
'url' => sprintf('%s/eventReports/index/context:all', $baseurl),
'text' => __('All'),
),
array(
'active' => $context === 'default',
'class' => 'defaultContext',
'url' => sprintf('%s/eventReports/index/context:default', $baseurl),
'text' => __('Default'),
),
array(
'active' => $context === 'deleted',
'url' => sprintf('%s/event_reports/index/context:deleted', $baseurl),
'text' => __('Deleted'),
),
)
),
array(
'type' => 'search',
'button' => __('Filter'),
'placeholder' => __('Enter value to search'),
'data' => '',
'searchKey' => 'value'
)
)
),
'title' => sprintf(__('Event Reports %s'), !empty($event_id) ? sprintf(__('for Event %s'), h($event_id)) : ''),
'primary_id_path' => 'EventReport.id',
'fields' => array(
array(
'name' => __('Id'),
'sort' => 'id',
'class' => 'short',
'data_path' => 'EventReport.id',
'element' => 'links',
'url' => $baseurl . '/eventReports/view/%s'
),
array(
'name' => __('Name'),
'data_path' => 'EventReport.name',
),
array(
'name' => __('Event ID'),
'class' => 'short',
'element' => 'links',
'data_path' => 'EventReport.event_id',
'url' => $baseurl . '/events/view/%s'
),
array(
'name' => __('Last update'),
'sort' => 'timestamp',
'class' => 'short',
'element' => 'datetime',
'data_path' => 'EventReport.timestamp',
),
array(
'name' => __('Distribution'),
'element' => 'distribution_levels',
'class' => 'short',
'data_path' => 'EventReport.distribution',
)
),
'actions' => array(
array(
'url' => '/eventReports/view',
'url_params_data_paths' => array(
'EventReport.id'
),
'icon' => 'eye',
'dbclickAction' => true
),
array(
'url' => '/eventReports/edit',
'url_params_data_paths' => array(
'EventReport.id'
),
'icon' => 'edit'
),
array(
'title' => __('Delete'),
'icon' => 'trash',
'onclick' => 'simplePopup(\'' . $baseurl . '/event_reports/delete/[onclick_params_data_path]\');',
'onclick_params_data_path' => 'EventReport.id',
'complex_requirement' => array(
'function' => function ($row, $options) {
return ($options['me']['Role']['perm_site_admin'] || $options['me']['org_id'] == $options['datapath']['orgc']) && !$options['datapath']['deleted'];
},
'options' => array(
'me' => $me,
'datapath' => array(
'orgc' => 'Event.orgc_id',
'deleted' => 'EventReport.deleted'
)
)
),
),
array(
'title' => __('Restore report'),
'url' => $baseurl . '/event_reports/restore',
'url_params_data_paths' => array('EventReport.id'),
'icon' => 'trash-restore',
'postLink' => true,
'postLinkConfirm' => __('Are you sure you want to restore the Report?'),
'complex_requirement' => array(
'function' => function ($row, $options) {
return ($options['me']['Role']['perm_site_admin'] || $options['me']['org_id'] == $options['datapath']['orgc']) && $options['datapath']['deleted'];
},
'options' => array(
'me' => $me,
'datapath' => array(
'orgc' => 'Event.orgc_id',
'deleted' => 'EventReport.deleted'
)
)
),
),
)
)
));
if(!$embedded_view) {
echo '</div>';
echo $this->element('/genericElements/SideMenu/side_menu', array('menuList' => 'eventReports', 'menuItem' => 'index'));
}
?>
<script type="text/javascript">
var passedArgsArray = <?php echo $passedArgs; ?>;
if (passedArgsArray['context'] === undefined) {
passedArgsArray['context'] = '';
}
$(document).ready(function() {
$('#quickFilterButton').click(function() {
runIndexQuickFilter('/context:' + passedArgsArray['context']);
});
$('#quickFilterField').on('keypress', function (e) {
if(e.which === 13) {
runIndexQuickFilter('/context:' + passedArgsArray['context']);
}
});
});
</script>

View File

@ -0,0 +1,72 @@
<?php
$table_data = array();
$table_data[] = array('key' => __('ID'), 'value' => $report['EventReport']['id']);
$table_data[] = array('key' => __('UUID'), 'value' => $report['EventReport']['uuid']);
$table_data[] = array('key' => __('Name'), 'value' => $report['EventReport']['name']);
$table_data[] = array(
'key' => __('Event ID'),
'html' => sprintf('%s', sprintf(
'<a href="%s">%s</a>',
sprintf('%s%s%s', $baseurl, '/events/view/', h($report['EventReport']['event_id'])),
h($report['EventReport']['event_id'])
))
);
$table_data[] = array(
'key' => __('Distribution'),
'value_class' => ($report['EventReport']['distribution'] == 0) ? 'privateRedText' : '',
'html' => sprintf('%s',
($report['EventReport']['distribution'] == 4) ?
sprintf('<a href="%s%s">%s</a>', $baseurl . '/sharing_groups/view/', h($report['SharingGroup']['id']), h($report['SharingGroup']['name'])) :
h($distributionLevels[$report['EventReport']['distribution']])
)
);
$table_data[] = array('key' => __('Timestamp'), 'value' => date('Y-m-d H:i:s', $report['EventReport']['timestamp']));
$table_data[] = array('key' => __('Deleted'), 'boolean' => $report['EventReport']['deleted'], 'value_class' => $report['EventReport']['deleted'] ? 'red' : 'green');
?>
<div class='<?= !isset($ajax) || !$ajax ? 'view' : '' ?>'>
<div class="row-fluid">
<h2><?= h($report['EventReport']['name']) ?></h2>
<div class="span8" style="margin-bottom: 10px;">
<?php echo $this->element('genericElements/viewMetaTable', array('table_data' => $table_data)); ?>
</div>
<div class="clear">
<h4><?= __('Event Report content') ?></h4>
<div class="markdownEditor-full-container">
<?php
echo $this->element('markdownEditor/markdownEditor', [
'canEdit' => $canEdit,
'markdown' => $report['EventReport']['content'],
'modelName' => 'EventReport',
'mardownModelFieldName' => 'content',
'lastModified' => $report['EventReport']['timestamp'],
'additionalMarkdownElements' => [
'path' => 'EventReports/reportEditor',
'variables' => [
'reportid' => $report['EventReport']['id'],
'eventid' => $report['EventReport']['event_id'],
]
],
'additionalMarkdownHelpModalElements' => [[
'path' => 'EventReports/reportHelpModal',
'tab_name' => __('Markdown format'),
]]
]);
?>
</div>
</div>
</div>
<div style="margin-bottom: 15px;"></div>
</div>
<script type="text/javascript">
$(document).ready(function () {
});
</script>
<?php
if (!isset($ajax) || !$ajax) {
echo $this->element('/genericElements/SideMenu/side_menu', array('menuList' => 'eventReports', 'menuItem' => 'view'));
}
?>

View File

@ -0,0 +1,31 @@
<?php
$data = array(
'title' => __('Event report: %s', h($report['EventReport']['name'])),
'content' => array(
array(
'html' => $this->element('markdownEditor/markdownEditor', [
'insideModal' => true,
'canEdit' => $canEdit,
'markdown' => $report['EventReport']['content'],
'modelName' => 'EventReport',
'mardownModelFieldName' => 'content',
'lastModified' => $report['EventReport']['timestamp'],
'additionalMarkdownElements' => [
'path' => 'EventReports/reportEditor',
'variables' => [
'reportid' => $report['EventReport']['id'],
'eventid' => $report['EventReport']['event_id'],
'proxyMISPElements' => $proxyMISPElements,
]
],
'additionalMarkdownHelpModalElements' => [[
'path' => 'EventReports/reportHelpModal',
'tab_name' => __('Markdown format'),
]],
'editRedirect' => sprintf('%s/eventReports/view/%s', $baseurl, $report['EventReport']['id']),
])
),
)
);
echo $this->element('genericElements/infoModal', array('data' => $data, 'type' => 'xl'));
?>

View File

@ -334,7 +334,6 @@
<?php echo '</ul>' ?>
</div>
<?php endif; ?>
<?php
if (!empty($event['RelatedEvent'])):
?>
@ -490,6 +489,9 @@
<button class="btn btn-inverse toggle qet galaxy-toggle-button" id="attackmatrix_toggle" data-toggle-type="attackmatrix" onclick="enable_attack_matrix();">
<span class="icon-plus icon-white" title="<?php echo __('Toggle ATT&CK matrix');?>" role="button" tabindex="0" aria-label="<?php echo __('Toggle ATT&CK matrix');?>" style="vertical-align:top;"></span><?php echo __('ATT&CK matrix');?>
</button>
<button class="btn btn-inverse toggle qet galaxy-toggle-button" id="eventreport_toggle" data-toggle-type="eventreport">
<span class="icon-plus icon-white" title="<?php echo __('Toggle reports');?>" role="button" tabindex="0" aria-label="<?php echo __('Toggle reports');?>" style="vertical-align:top;"></span><?php echo __('Event reports');?>
</button>
<button class="btn btn-inverse toggle qet galaxy-toggle-button" id="attributes_toggle" data-toggle-type="attributes">
<span class="icon-minus icon-white" title="<?php echo __('Toggle attributes');?>" role="button" tabindex="0" aria-label="<?php echo __('Toggle attributes');?>" style="vertical-align:top;"></span><?php echo __('Attributes');?>
</button>
@ -516,6 +518,10 @@
</div>
<div id="attackmatrix_div" class="info_container_eventgraph_network" style="display: none;" data-fullscreen="false" data-mitre-attack-galaxy-id="<?php echo h($mitreAttackGalaxyId)?>">
</div>
<div id="eventreport_div" style="display: none;">
<span class="report-title-section"><?php echo __('Event Reports');?></span>
<div id="eventreport_index_div"></div>
</div>
<div id="attributes_div">
<?php echo $this->element('eventattribute'); ?>
</div>
@ -538,6 +544,12 @@ $(document).ready(function () {
$("#discussions_div").html(data);
});
$.get("<?php echo $baseurl; ?>/eventReports/index/event_id:<?= h($event['Event']['id']); ?>/index_for_event:1<?= $extended ? '/extended_event:1' : ''?>", function(data) {
$("#eventreport_index_div").html(data);
if ($('#eventreport_index_div table tbody > tr').length) { // open if contain a report
$('#eventreport_toggle').click()
}
});
});
function enable_correlation_graph() {
@ -551,5 +563,6 @@ function enable_attack_matrix() {
$("#attackmatrix_div").html(data);
});
}
</script>
<input type="hidden" value="/shortcuts/event_view.json" class="keyboardShortcutsConfig" />

View File

@ -0,0 +1,349 @@
/* BASICS */
.CodeMirror {
/* Set height, width, borders, and global font properties here */
font-family: monospace;
height: 300px;
color: black;
direction: ltr;
}
/* PADDING */
.CodeMirror-lines {
padding: 4px 0; /* Vertical padding around content */
}
.CodeMirror pre.CodeMirror-line,
.CodeMirror pre.CodeMirror-line-like {
padding: 0 4px; /* Horizontal padding of content */
}
.CodeMirror-scrollbar-filler, .CodeMirror-gutter-filler {
background-color: white; /* The little square between H and V scrollbars */
}
/* GUTTER */
.CodeMirror-gutters {
border-right: 1px solid #ddd;
background-color: #f7f7f7;
white-space: nowrap;
}
.CodeMirror-linenumbers {}
.CodeMirror-linenumber {
padding: 0 3px 0 5px;
min-width: 20px;
text-align: right;
color: #999;
white-space: nowrap;
}
.CodeMirror-guttermarker { color: black; }
.CodeMirror-guttermarker-subtle { color: #999; }
/* CURSOR */
.CodeMirror-cursor {
border-left: 1px solid black;
border-right: none;
width: 0;
}
/* Shown when moving in bi-directional text */
.CodeMirror div.CodeMirror-secondarycursor {
border-left: 1px solid silver;
}
.cm-fat-cursor .CodeMirror-cursor {
width: auto;
border: 0 !important;
background: #7e7;
}
.cm-fat-cursor div.CodeMirror-cursors {
z-index: 1;
}
.cm-fat-cursor-mark {
background-color: rgba(20, 255, 20, 0.5);
-webkit-animation: blink 1.06s steps(1) infinite;
-moz-animation: blink 1.06s steps(1) infinite;
animation: blink 1.06s steps(1) infinite;
}
.cm-animate-fat-cursor {
width: auto;
border: 0;
-webkit-animation: blink 1.06s steps(1) infinite;
-moz-animation: blink 1.06s steps(1) infinite;
animation: blink 1.06s steps(1) infinite;
background-color: #7e7;
}
@-moz-keyframes blink {
0% {}
50% { background-color: transparent; }
100% {}
}
@-webkit-keyframes blink {
0% {}
50% { background-color: transparent; }
100% {}
}
@keyframes blink {
0% {}
50% { background-color: transparent; }
100% {}
}
/* Can style cursor different in overwrite (non-insert) mode */
.CodeMirror-overwrite .CodeMirror-cursor {}
.cm-tab { display: inline-block; text-decoration: inherit; }
.CodeMirror-rulers {
position: absolute;
left: 0; right: 0; top: -50px; bottom: 0;
overflow: hidden;
}
.CodeMirror-ruler {
border-left: 1px solid #ccc;
top: 0; bottom: 0;
position: absolute;
}
/* DEFAULT THEME */
.cm-s-default .cm-header {color: blue;}
.cm-s-default .cm-quote {color: #090;}
.cm-negative {color: #d44;}
.cm-positive {color: #292;}
.cm-header, .cm-strong {font-weight: bold;}
.cm-em {font-style: italic;}
.cm-link {text-decoration: underline;}
.cm-strikethrough {text-decoration: line-through;}
.cm-s-default .cm-keyword {color: #708;}
.cm-s-default .cm-atom {color: #219;}
.cm-s-default .cm-number {color: #164;}
.cm-s-default .cm-def {color: #00f;}
.cm-s-default .cm-variable,
.cm-s-default .cm-punctuation,
.cm-s-default .cm-property,
.cm-s-default .cm-operator {}
.cm-s-default .cm-variable-2 {color: #05a;}
.cm-s-default .cm-variable-3, .cm-s-default .cm-type {color: #085;}
.cm-s-default .cm-comment {color: #a50;}
.cm-s-default .cm-string {color: #a11;}
.cm-s-default .cm-string-2 {color: #f50;}
.cm-s-default .cm-meta {color: #555;}
.cm-s-default .cm-qualifier {color: #555;}
.cm-s-default .cm-builtin {color: #30a;}
.cm-s-default .cm-bracket {color: #997;}
.cm-s-default .cm-tag {color: #170;}
.cm-s-default .cm-attribute {color: #00c;}
.cm-s-default .cm-hr {color: #999;}
.cm-s-default .cm-link {color: #00c;}
.cm-s-default .cm-error {color: #f00;}
.cm-invalidchar {color: #f00;}
.CodeMirror-composing { border-bottom: 2px solid; }
/* Default styles for common addons */
div.CodeMirror span.CodeMirror-matchingbracket {color: #0b0;}
div.CodeMirror span.CodeMirror-nonmatchingbracket {color: #a22;}
.CodeMirror-matchingtag { background: rgba(255, 150, 0, .3); }
.CodeMirror-activeline-background {background: #e8f2ff;}
/* STOP */
/* The rest of this file contains styles related to the mechanics of
the editor. You probably shouldn't touch them. */
.CodeMirror {
position: relative;
overflow: hidden;
background: white;
}
.CodeMirror-scroll {
overflow: scroll !important; /* Things will break if this is overridden */
/* 50px is the magic margin used to hide the element's real scrollbars */
/* See overflow: hidden in .CodeMirror */
margin-bottom: -50px; margin-right: -50px;
padding-bottom: 50px;
height: 100%;
outline: none; /* Prevent dragging from highlighting the element */
position: relative;
}
.CodeMirror-sizer {
position: relative;
border-right: 50px solid transparent;
}
/* The fake, visible scrollbars. Used to force redraw during scrolling
before actual scrolling happens, thus preventing shaking and
flickering artifacts. */
.CodeMirror-vscrollbar, .CodeMirror-hscrollbar, .CodeMirror-scrollbar-filler, .CodeMirror-gutter-filler {
position: absolute;
z-index: 6;
display: none;
}
.CodeMirror-vscrollbar {
right: 0; top: 0;
overflow-x: hidden;
overflow-y: scroll;
}
.CodeMirror-hscrollbar {
bottom: 0; left: 0;
overflow-y: hidden;
overflow-x: scroll;
}
.CodeMirror-scrollbar-filler {
right: 0; bottom: 0;
}
.CodeMirror-gutter-filler {
left: 0; bottom: 0;
}
.CodeMirror-gutters {
position: absolute; left: 0; top: 0;
min-height: 100%;
z-index: 3;
}
.CodeMirror-gutter {
white-space: normal;
height: 100%;
display: inline-block;
vertical-align: top;
margin-bottom: -50px;
}
.CodeMirror-gutter-wrapper {
position: absolute;
z-index: 4;
background: none !important;
border: none !important;
}
.CodeMirror-gutter-background {
position: absolute;
top: 0; bottom: 0;
z-index: 4;
}
.CodeMirror-gutter-elt {
position: absolute;
cursor: default;
z-index: 4;
}
.CodeMirror-gutter-wrapper ::selection { background-color: transparent }
.CodeMirror-gutter-wrapper ::-moz-selection { background-color: transparent }
.CodeMirror-lines {
cursor: text;
min-height: 1px; /* prevents collapsing before first draw */
}
.CodeMirror pre.CodeMirror-line,
.CodeMirror pre.CodeMirror-line-like {
/* Reset some styles that the rest of the page might have set */
-moz-border-radius: 0; -webkit-border-radius: 0; border-radius: 0;
border-width: 0;
background: transparent;
font-family: inherit;
font-size: inherit;
margin: 0;
white-space: pre;
word-wrap: normal;
line-height: inherit;
color: inherit;
z-index: 2;
position: relative;
overflow: visible;
-webkit-tap-highlight-color: transparent;
-webkit-font-variant-ligatures: contextual;
font-variant-ligatures: contextual;
}
.CodeMirror-wrap pre.CodeMirror-line,
.CodeMirror-wrap pre.CodeMirror-line-like {
word-wrap: break-word;
white-space: pre-wrap;
word-break: normal;
}
.CodeMirror-linebackground {
position: absolute;
left: 0; right: 0; top: 0; bottom: 0;
z-index: 0;
}
.CodeMirror-linewidget {
position: relative;
z-index: 2;
padding: 0.1px; /* Force widget margins to stay inside of the container */
}
.CodeMirror-widget {}
.CodeMirror-rtl pre { direction: rtl; }
.CodeMirror-code {
outline: none;
}
/* Force content-box sizing for the elements where we expect it */
.CodeMirror-scroll,
.CodeMirror-sizer,
.CodeMirror-gutter,
.CodeMirror-gutters,
.CodeMirror-linenumber {
-moz-box-sizing: content-box;
box-sizing: content-box;
}
.CodeMirror-measure {
position: absolute;
width: 100%;
height: 0;
overflow: hidden;
visibility: hidden;
}
.CodeMirror-cursor {
position: absolute;
pointer-events: none;
}
.CodeMirror-measure pre { position: static; }
div.CodeMirror-cursors {
visibility: hidden;
position: relative;
z-index: 3;
}
div.CodeMirror-dragcursors {
visibility: visible;
}
.CodeMirror-focused div.CodeMirror-cursors {
visibility: visible;
}
.CodeMirror-selected { background: #d9d9d9; }
.CodeMirror-focused .CodeMirror-selected { background: #d7d4f0; }
.CodeMirror-crosshair { cursor: crosshair; }
.CodeMirror-line::selection, .CodeMirror-line > span::selection, .CodeMirror-line > span > span::selection { background: #d7d4f0; }
.CodeMirror-line::-moz-selection, .CodeMirror-line > span::-moz-selection, .CodeMirror-line > span > span::-moz-selection { background: #d7d4f0; }
.cm-searching {
background-color: #ffa;
background-color: rgba(255, 255, 0, .4);
}
/* Used to force a border model for a node */
.cm-force-border { padding-right: .1px; }
@media print {
/* Hide the cursor when printing */
.CodeMirror div.CodeMirror-cursors {
visibility: hidden;
}
}
/* See issue #2901 */
.cm-tab-wrap-hack:after { content: ''; }
/* Help users use markselection to safely style text background */
span.CodeMirror-selectedtext { background: none; }

View File

@ -0,0 +1,37 @@
.CodeMirror-hints {
position: absolute;
z-index: 10;
overflow: hidden;
list-style: none;
margin: 0;
padding: 2px;
-webkit-box-shadow: 2px 3px 5px rgba(0,0,0,.2);
-moz-box-shadow: 2px 3px 5px rgba(0,0,0,.2);
box-shadow: 2px 3px 5px rgba(0,0,0,.2);
border-radius: 3px;
border: 1px solid silver;
background: white;
font-size: 90%;
font-family: monospace;
max-height: 20em;
overflow-y: auto;
}
.CodeMirror-hint {
margin: 0;
padding: 0 4px;
border-radius: 2px;
white-space: pre;
color: black;
cursor: pointer;
}
li.CodeMirror-hint-active {
background: #08f;
color: white;
}

View File

@ -0,0 +1,72 @@
.CodeMirror-simplescroll-horizontal div, .CodeMirror-simplescroll-vertical div {
position: absolute;
background: #ccc;
-moz-box-sizing: border-box;
box-sizing: border-box;
border: 1px solid #bbb;
border-radius: 2px;
}
.CodeMirror-simplescroll-horizontal, .CodeMirror-simplescroll-vertical {
position: absolute;
z-index: 6;
background: #eee;
}
.CodeMirror-simplescroll-horizontal {
bottom: 0; left: 0;
height: 8px;
}
.CodeMirror-simplescroll-horizontal div {
bottom: 0;
height: 100%;
}
.CodeMirror-simplescroll-vertical {
right: 0; top: 0;
width: 8px;
}
.CodeMirror-simplescroll-vertical div {
right: 0;
width: 100%;
}
.CodeMirror-overlayscroll .CodeMirror-scrollbar-filler, .CodeMirror-overlayscroll .CodeMirror-gutter-filler {
display: none;
}
.CodeMirror-overlayscroll-horizontal div, .CodeMirror-overlayscroll-vertical div {
position: absolute;
background: #e6e6e6;
border-radius: 3px;
}
.CodeMirror-overlayscroll-horizontal, .CodeMirror-overlayscroll-vertical {
position: absolute;
z-index: 2;
}
.modal-body-xl .CodeMirror-overlayscroll-horizontal, .modal-body-xl .CodeMirror-overlayscroll-vertical {
position: absolute;
z-index: 6;
}
.CodeMirror-overlayscroll-horizontal {
bottom: 0; left: 0;
height: 6px;
}
.CodeMirror-overlayscroll-horizontal div {
bottom: 0;
height: 100%;
}
.CodeMirror-overlayscroll-vertical {
right: 0; top: 0;
width: 6px;
}
.CodeMirror-overlayscroll-vertical div {
right: 0;
width: 100%;
}

1
app/webroot/css/highlight.min.css vendored Normal file
View File

@ -0,0 +1 @@
.hljs{display:block;overflow-x:auto;padding:.5em;background:#f0f0f0}.hljs,.hljs-subst{color:#444}.hljs-comment{color:#888}.hljs-attribute,.hljs-doctag,.hljs-keyword,.hljs-meta-keyword,.hljs-name,.hljs-selector-tag{font-weight:700}.hljs-deletion,.hljs-number,.hljs-quote,.hljs-selector-class,.hljs-selector-id,.hljs-string,.hljs-template-tag,.hljs-type{color:#800}.hljs-section,.hljs-title{color:#800;font-weight:700}.hljs-link,.hljs-regexp,.hljs-selector-attr,.hljs-selector-pseudo,.hljs-symbol,.hljs-template-variable,.hljs-variable{color:#bc6060}.hljs-literal{color:#78a960}.hljs-addition,.hljs-built_in,.hljs-bullet,.hljs-code{color:#397300}.hljs-meta{color:#1f7199}.hljs-meta-string{color:#4d99bf}.hljs-emphasis{font-style:italic}.hljs-strong{font-weight:700}

View File

@ -939,10 +939,39 @@ a.proposal_link_red:hover {
float: left;
}
#eventreport_div {
position: relative;
margin-top: 20px;
padding: 35px 15px 0px 15px;
background-color: #fff;
border: 1px solid #ddd;
border-radius: 3px;
}
#eventreport_div > .report-title-section {
position: absolute;
top: -1px;
left: -1px;
padding: 3px 7px;
font-weight: bold;
background-color: #f5f5f5;
border: 1px solid #ddd;
color: #6b6b6b;
border-radius: 3px 0 3px 0;
}
.modal-body-long {
max-height: 600px;
}
.modal-body-xl {
max-height: 70vh;
}
.modal-body-lg {
max-height: 60vh;
}
.ajax_popover_form {
display:none;
width: 700px;
@ -955,6 +984,18 @@ a.proposal_link_red:hover {
z-index:5;
}
.modal.modal-lg {
width: 60%;
left: 20%;
margin-left: 0px;
}
.modal.modal-xl {
width: 90%;
left: 5%;
margin-left: 0px;
}
.ajax_popover_form_large {
width: 1400px;
left: calc(50% - 700px);

View File

@ -0,0 +1,102 @@
@media print {
.misp-element-wrapper.object .obj-type {
color: white !important;
}
.misp-element-wrapper.object .obj-value {
color: black !important;
}
}
span.misp-element-wrapper {
margin: 3px 3px;
border: 1px solid #ddd !important;
border-radius: 3px;
white-space: nowrap;
display: inline-block;
padding: 0;
}
.misp-element-wrapper, .misp-tag-wrapper, .misp-picture-wrapper {
cursor: help;
}
.misp-element-wrapper.invalid {
cursor: not-allowed;
}
.misp-element-wrapper.invalid > span {
margin: 0 3px;
}
.misp-element-wrapper.attribute .attr-type {
background-color: #f5f5f5 !important;
border-right: 1px solid #ddd !important;
display: inline-block;
}
.misp-element-wrapper.attribute .attr-type > span {
margin: 2px 3px;
}
.misp-element-wrapper.attribute .attr-value {
display: inline-table;
margin: 0px 3px;
}
.misp-element-wrapper.attribute .attr-value > span {
max-width: 300px;
text-overflow: ellipsis;
overflow: hidden;
white-space: nowrap;
display: table-cell;
}
span.misp-element-wrapper.object {
border: 1px solid #3465a4 !important;
}
.misp-element-wrapper.object .obj-type {
display: inline-block;
background-color: #3465a4 !important;
color: #ffffff !important;
}
.misp-element-wrapper.object .obj-type .object-attribute-type {
margin-left: 0;
background-color: #f5f5f5;
color: black;
padding: 1px 3px;
border-radius: 7px;
}
.misp-element-wrapper.object .obj-type > span {
margin: 2px 3px;
}
.misp-element-wrapper.object .obj-value {
display: inline-table;
margin: 0px 3px;
}
.misp-element-wrapper.object .obj-value > span {
max-width: 300px;
text-overflow: ellipsis;
overflow: hidden;
white-space: nowrap;
display: table-cell;
}
.attributePicture > img {
display: block;
margin: 0 auto;
max-width: 90%;
}
.CodeMirror-hint-active .blue {
color: white !important;
}
.popover {
z-index: 1060;
}

View File

@ -0,0 +1,211 @@
@media print {
body {
-webkit-print-color-adjust: exact !important;
}
body * {
visibility: hidden;
}
#viewer, #viewer * {
visibility: visible;
}
#viewer {
position: absolute !important;
left: 0;
top: 0;
}
}
.markdownEditor-full-container {
position: relative;
}
.markdownEditor-full-container:fullscreen {
background-color: white;
}
.markdownEditor-full-container:fullscreen .raw-container,
.markdownEditor-full-container:fullscreen .split-container,
.markdownEditor-full-container:fullscreen #editor-container,
.markdownEditor-full-container:fullscreen #viewer-container {
max-height: calc(100vh - 50px) !important;
}
.markdownEditor-full-container:fullscreen #editor-container {
border: 0;
}
.markdownEditor-full-container:fullscreen .btn-toolbar {
margin-left: 15px;
}
.split-container {
overflow: hidden;
min-height: 500px;
}
.modal-body-xl .split-container {
max-height: calc(70vh - 50px)
}
.split-container.split-actif > #editor-container, .split-container.split-actif > #viewer-container {
max-height: calc(100vh - 120px)
}
#viewer-container {
margin: 0 0;
justify-content: center;
padding: 7px;
overflow-y: auto;
padding: 0 15px;
box-shadow: inset 0px 2px 6px #eee;
}
#editor-container {
border: 1px solid #ccc;
width: 50%;
min-width: 300px;
position: relative;
float: left;
}
.split-container:not(.split-actif) #editor-container {
float: unset;
width: unset;
}
#editor {
min-height: 500px;
width: 100%;
border-radius: 0;
resize: vertical;
}
#viewer {
position: relative;
width: 100%;
max-width: 1200px;
margin: auto;
}
#editor-container > div:not(.ui-resizable-handle) {
width: 100%;
}
.split-container .ui-resizable-handle {
box-shadow: 4px 0 6px #eee;
width: 7px;
position: absolute;
right: -7px;
z-index: 1;
cursor: col-resize;
}
.ui-resizable-helper {
border-right: 2px dotted #ccc;
cursor: col-resize;
z-index: 1060 !important;
}
#editor-subcontainer {
display: flex;
flex-direction: column;
}
#editor-subcontainer > div:not(#bottom-bar) {
flex-grow: 1;
}
#bottom-bar {
height: 1.5em;
}
#top-bar {
height: 34px;
}
.editor-action-bar {
display: flex;
align-items: center;
background-color: #3c3c3c;
color: white;
padding: 0 20px;
}
.editor-action-bar > span {
margin-left: 15px;
}
#top-bar .icon {
padding: 2px 2px;
border-radius: 2px;
vertical-align: middle;
font-size: 16px;
margin: 0px 5px
}
#top-bar .icon:hover {
background-color: #f3f3f3;
color: black;
}
.top-bar-separator {
display: inline-block;
margin: auto 8px;
width: 1px;
height: 15px;
background-color: #d0d0d0;
}
#loadingBackdrop {
display: flex;
justify-content: center;
align-items: center;
position: absolute;
left: 0;
right: 0;
top: 0;
bottom: 0;
}
#lastModifiedField {
border-radius: 0;
}
.dropdown-menu li .icon {
width: 20px;
display: inline-block;
}
.cm-s-default {
width: 100%;
height: calc(100vh - 120px - 1.5em - 34px) !important;
}
.markdownEditor-full-container:fullscreen .cm-s-default {
height: calc(100vh - 50px - 1.5em - 34px) !important;
}
.modal-body-xl .split-container .cm-s-default{
max-height: calc(70vh - 55px) !important;
}
.modal-body-xl #viewer {
max-height: calc(70vh - 75px)
}
.popover {
max-width: 66%;
}
.cm-s-default .CodeMirror-gutter-wrapper {
z-index: 1;
}
.cm-s-default .CodeMirror-gutters {
z-index: 0;
}
.link-not-active {
pointer-events: none;
cursor: default;
text-decoration: none;
color: black;
}
.dropdown-menu > li > a:hover .green,
.dropdown-menu > li > a:hover .red {
color: white !important;
}

View File

@ -0,0 +1,130 @@
// CodeMirror, copyright (c) by Marijn Haverbeke and others
// Distributed under an MIT license: https://codemirror.net/LICENSE
(function (mod) {
if (typeof exports == "object" && typeof module == "object") // CommonJS
mod(require("../../lib/codemirror"));
else if (typeof define == "function" && define.amd) // AMD
define(["../../lib/codemirror"], mod);
else // Plain browser env
mod(CodeMirror);
})(function (CodeMirror) {
CodeMirror.defineExtension("addPanel", function (node, options) {
options = options || {};
if (!this.state.panels) initPanels(this);
var info = this.state.panels;
var wrapper = info.wrapper;
var cmWrapper = this.getWrapperElement();
var replace = options.replace instanceof Panel && !options.replace.cleared;
if (options.after instanceof Panel && !options.after.cleared) {
wrapper.insertBefore(node, options.before.node.nextSibling);
} else if (options.before instanceof Panel && !options.before.cleared) {
wrapper.insertBefore(node, options.before.node);
} else if (replace) {
wrapper.insertBefore(node, options.replace.node);
options.replace.clear(true);
} else if (options.position == "bottom") {
wrapper.appendChild(node);
} else if (options.position == "before-bottom") {
wrapper.insertBefore(node, cmWrapper.nextSibling);
} else if (options.position == "after-top") {
wrapper.insertBefore(node, cmWrapper);
} else {
wrapper.insertBefore(node, wrapper.firstChild);
}
var height = (options && options.height) || node.offsetHeight;
var panel = new Panel(this, node, options, height);
info.panels.push(panel);
this.setSize();
if (options.stable && isAtTop(this, node))
this.scrollTo(null, this.getScrollInfo().top + height);
return panel;
});
function Panel(cm, node, options, height) {
this.cm = cm;
this.node = node;
this.options = options;
this.height = height;
this.cleared = false;
}
/* when skipRemove is true, clear() was called from addPanel().
* Thus removePanels() should not be called (issue 5518) */
Panel.prototype.clear = function (skipRemove) {
if (this.cleared) return;
this.cleared = true;
var info = this.cm.state.panels;
info.panels.splice(info.panels.indexOf(this), 1);
this.cm.setSize();
if (this.options.stable && isAtTop(this.cm, this.node))
this.cm.scrollTo(null, this.cm.getScrollInfo().top - this.height)
info.wrapper.removeChild(this.node);
if (info.panels.length == 0 && !skipRemove) removePanels(this.cm);
};
Panel.prototype.changed = function () {
this.height = this.node.getBoundingClientRect().height;
this.cm.setSize();
};
function initPanels(cm) {
var wrap = cm.getWrapperElement();
var style = window.getComputedStyle ? window.getComputedStyle(wrap) : wrap.currentStyle;
var height = parseInt(style.height);
var info = cm.state.panels = {
setHeight: wrap.style.height,
panels: [],
wrapper: document.createElement("div")
};
wrap.parentNode.insertBefore(info.wrapper, wrap);
var hasFocus = cm.hasFocus();
info.wrapper.appendChild(wrap);
if (hasFocus) cm.focus();
cm._setSize = cm.setSize;
if (height != null) cm.setSize = function (width, newHeight) {
if (!newHeight) newHeight = info.wrapper.offsetHeight;
info.setHeight = newHeight;
if (typeof newHeight != "number") {
var px = /^(\d+\.?\d*)px$/.exec(newHeight);
if (px) {
newHeight = Number(px[1]);
} else {
info.wrapper.style.height = newHeight;
newHeight = info.wrapper.offsetHeight;
}
}
var editorheight = newHeight - info.panels
.map(function (p) { return p.node.getBoundingClientRect().height; })
.reduce(function (a, b) { return a + b; }, 0);
cm._setSize(width, editorheight);
height = newHeight;
};
}
function removePanels(cm) {
var info = cm.state.panels;
cm.state.panels = null;
var wrap = cm.getWrapperElement();
info.wrapper.parentNode.replaceChild(wrap, info.wrapper);
wrap.style.height = info.setHeight;
cm.setSize = cm._setSize;
cm.setSize();
}
function isAtTop(cm, dom) {
for (var sibling = dom.nextSibling; sibling; sibling = sibling.nextSibling)
if (sibling == cm.getWrapperElement()) return true
return false
}
});

View File

@ -0,0 +1,480 @@
// CodeMirror, copyright (c) by Marijn Haverbeke and others
// Distributed under an MIT license: https://codemirror.net/LICENSE
(function(mod) {
if (typeof exports == "object" && typeof module == "object") // CommonJS
mod(require("../../lib/codemirror"));
else if (typeof define == "function" && define.amd) // AMD
define(["../../lib/codemirror"], mod);
else // Plain browser env
mod(CodeMirror);
})(function(CodeMirror) {
"use strict";
var HINT_ELEMENT_CLASS = "CodeMirror-hint";
var ACTIVE_HINT_ELEMENT_CLASS = "CodeMirror-hint-active";
// This is the old interface, kept around for now to stay
// backwards-compatible.
CodeMirror.showHint = function(cm, getHints, options) {
if (!getHints) return cm.showHint(options);
if (options && options.async) getHints.async = true;
var newOpts = {hint: getHints};
if (options) for (var prop in options) newOpts[prop] = options[prop];
return cm.showHint(newOpts);
};
CodeMirror.defineExtension("showHint", function(options) {
options = parseOptions(this, this.getCursor("start"), options);
var selections = this.listSelections()
if (selections.length > 1) return;
// By default, don't allow completion when something is selected.
// A hint function can have a `supportsSelection` property to
// indicate that it can handle selections.
if (this.somethingSelected()) {
if (!options.hint.supportsSelection) return;
// Don't try with cross-line selections
for (var i = 0; i < selections.length; i++)
if (selections[i].head.line != selections[i].anchor.line) return;
}
if (this.state.completionActive) this.state.completionActive.close();
var completion = this.state.completionActive = new Completion(this, options);
if (!completion.options.hint) return;
CodeMirror.signal(this, "startCompletion", this);
completion.update(true);
});
CodeMirror.defineExtension("closeHint", function() {
if (this.state.completionActive) this.state.completionActive.close()
})
function Completion(cm, options) {
this.cm = cm;
this.options = options;
this.widget = null;
this.debounce = 0;
this.tick = 0;
this.startPos = this.cm.getCursor("start");
this.startLen = this.cm.getLine(this.startPos.line).length - this.cm.getSelection().length;
var self = this;
cm.on("cursorActivity", this.activityFunc = function() { self.cursorActivity(); });
}
var requestAnimationFrame = window.requestAnimationFrame || function(fn) {
return setTimeout(fn, 1000/60);
};
var cancelAnimationFrame = window.cancelAnimationFrame || clearTimeout;
Completion.prototype = {
close: function() {
if (!this.active()) return;
this.cm.state.completionActive = null;
this.tick = null;
this.cm.off("cursorActivity", this.activityFunc);
if (this.widget && this.data) CodeMirror.signal(this.data, "close");
if (this.widget) this.widget.close();
CodeMirror.signal(this.cm, "endCompletion", this.cm);
},
active: function() {
return this.cm.state.completionActive == this;
},
pick: function(data, i) {
var completion = data.list[i], self = this;
this.cm.operation(function() {
if (completion.hint)
completion.hint(self.cm, data, completion);
else
self.cm.replaceRange(getText(completion), completion.from || data.from,
completion.to || data.to, "complete");
CodeMirror.signal(data, "pick", completion);
self.cm.scrollIntoView();
})
this.close();
},
cursorActivity: function() {
if (this.debounce) {
cancelAnimationFrame(this.debounce);
this.debounce = 0;
}
var identStart = this.startPos;
if(this.data) {
identStart = this.data.from;
}
var pos = this.cm.getCursor(), line = this.cm.getLine(pos.line);
if (pos.line != this.startPos.line || line.length - pos.ch != this.startLen - this.startPos.ch ||
pos.ch < identStart.ch || this.cm.somethingSelected() ||
(!pos.ch || this.options.closeCharacters.test(line.charAt(pos.ch - 1)))) {
this.close();
} else {
var self = this;
this.debounce = requestAnimationFrame(function() {self.update();});
if (this.widget) this.widget.disable();
}
},
update: function(first) {
if (this.tick == null) return
var self = this, myTick = ++this.tick
fetchHints(this.options.hint, this.cm, this.options, function(data) {
if (self.tick == myTick) self.finishUpdate(data, first)
})
},
finishUpdate: function(data, first) {
if (this.data) CodeMirror.signal(this.data, "update");
var picked = (this.widget && this.widget.picked) || (first && this.options.completeSingle);
if (this.widget) this.widget.close();
this.data = data;
if (data && data.list.length) {
if (picked && data.list.length == 1) {
this.pick(data, 0);
} else {
this.widget = new Widget(this, data);
CodeMirror.signal(data, "shown");
}
}
}
};
function parseOptions(cm, pos, options) {
var editor = cm.options.hintOptions;
var out = {};
for (var prop in defaultOptions) out[prop] = defaultOptions[prop];
if (editor) for (var prop in editor)
if (editor[prop] !== undefined) out[prop] = editor[prop];
if (options) for (var prop in options)
if (options[prop] !== undefined) out[prop] = options[prop];
if (out.hint.resolve) out.hint = out.hint.resolve(cm, pos)
return out;
}
function getText(completion) {
if (typeof completion == "string") return completion;
else return completion.text;
}
function buildKeyMap(completion, handle) {
var baseMap = {
Up: function() {handle.moveFocus(-1);},
Down: function() {handle.moveFocus(1);},
PageUp: function() {handle.moveFocus(-handle.menuSize() + 1, true);},
PageDown: function() {handle.moveFocus(handle.menuSize() - 1, true);},
Home: function() {handle.setFocus(0);},
End: function() {handle.setFocus(handle.length - 1);},
Enter: handle.pick,
Tab: handle.pick,
Esc: handle.close
};
var mac = /Mac/.test(navigator.platform);
if (mac) {
baseMap["Ctrl-P"] = function() {handle.moveFocus(-1);};
baseMap["Ctrl-N"] = function() {handle.moveFocus(1);};
}
var custom = completion.options.customKeys;
var ourMap = custom ? {} : baseMap;
function addBinding(key, val) {
var bound;
if (typeof val != "string")
bound = function(cm) { return val(cm, handle); };
// This mechanism is deprecated
else if (baseMap.hasOwnProperty(val))
bound = baseMap[val];
else
bound = val;
ourMap[key] = bound;
}
if (custom)
for (var key in custom) if (custom.hasOwnProperty(key))
addBinding(key, custom[key]);
var extra = completion.options.extraKeys;
if (extra)
for (var key in extra) if (extra.hasOwnProperty(key))
addBinding(key, extra[key]);
return ourMap;
}
function getHintElement(hintsElement, el) {
while (el && el != hintsElement) {
if (el.nodeName.toUpperCase() === "LI" && el.parentNode == hintsElement) return el;
el = el.parentNode;
}
}
function Widget(completion, data) {
this.completion = completion;
this.data = data;
this.picked = false;
var widget = this, cm = completion.cm;
var ownerDocument = cm.getInputField().ownerDocument;
var parentWindow = ownerDocument.defaultView || ownerDocument.parentWindow;
var hints = this.hints = ownerDocument.createElement("ul");
var theme = completion.cm.options.theme;
hints.className = "CodeMirror-hints " + theme;
this.selectedHint = data.selectedHint || 0;
var completions = data.list;
for (var i = 0; i < completions.length; ++i) {
var elt = hints.appendChild(ownerDocument.createElement("li")), cur = completions[i];
var className = HINT_ELEMENT_CLASS + (i != this.selectedHint ? "" : " " + ACTIVE_HINT_ELEMENT_CLASS);
if (cur.className != null) className = cur.className + " " + className;
elt.className = className;
if (cur.render) cur.render(elt, data, cur);
else elt.appendChild(ownerDocument.createTextNode(cur.displayText || getText(cur)));
elt.hintId = i;
}
var container = completion.options.container || ownerDocument.body;
var pos = cm.cursorCoords(completion.options.alignWithWord ? data.from : null);
var left = pos.left, top = pos.bottom, below = true;
var offsetLeft = 0, offsetTop = 0;
if (container !== ownerDocument.body) {
// We offset the cursor position because left and top are relative to the offsetParent's top left corner.
var isContainerPositioned = ['absolute', 'relative', 'fixed'].indexOf(parentWindow.getComputedStyle(container).position) !== -1;
var offsetParent = isContainerPositioned ? container : container.offsetParent;
var offsetParentPosition = offsetParent.getBoundingClientRect();
var bodyPosition = ownerDocument.body.getBoundingClientRect();
offsetLeft = (offsetParentPosition.left - bodyPosition.left - offsetParent.scrollLeft);
offsetTop = (offsetParentPosition.top - bodyPosition.top - offsetParent.scrollTop);
}
hints.style.left = (left - offsetLeft) + "px";
hints.style.top = (top - offsetTop) + "px";
// If we're at the edge of the screen, then we want the menu to appear on the left of the cursor.
var winW = parentWindow.innerWidth || Math.max(ownerDocument.body.offsetWidth, ownerDocument.documentElement.offsetWidth);
var winH = parentWindow.innerHeight || Math.max(ownerDocument.body.offsetHeight, ownerDocument.documentElement.offsetHeight);
container.appendChild(hints);
var box = hints.getBoundingClientRect(), overlapY = box.bottom - winH;
var scrolls = hints.scrollHeight > hints.clientHeight + 1
var startScroll = cm.getScrollInfo();
if (overlapY > 0) {
var height = box.bottom - box.top, curTop = pos.top - (pos.bottom - box.top);
if (curTop - height > 0) { // Fits above cursor
hints.style.top = (top = pos.top - height - offsetTop) + "px";
below = false;
} else if (height > winH) {
hints.style.height = (winH - 5) + "px";
hints.style.top = (top = pos.bottom - box.top - offsetTop) + "px";
var cursor = cm.getCursor();
if (data.from.ch != cursor.ch) {
pos = cm.cursorCoords(cursor);
hints.style.left = (left = pos.left - offsetLeft) + "px";
box = hints.getBoundingClientRect();
}
}
}
var overlapX = box.right - winW;
if (overlapX > 0) {
if (box.right - box.left > winW) {
hints.style.width = (winW - 5) + "px";
overlapX -= (box.right - box.left) - winW;
}
hints.style.left = (left = pos.left - overlapX - offsetLeft) + "px";
}
if (scrolls) for (var node = hints.firstChild; node; node = node.nextSibling)
node.style.paddingRight = cm.display.nativeBarWidth + "px"
cm.addKeyMap(this.keyMap = buildKeyMap(completion, {
moveFocus: function(n, avoidWrap) { widget.changeActive(widget.selectedHint + n, avoidWrap); },
setFocus: function(n) { widget.changeActive(n); },
menuSize: function() { return widget.screenAmount(); },
length: completions.length,
close: function() { completion.close(); },
pick: function() { widget.pick(); },
data: data
}));
if (completion.options.closeOnUnfocus) {
var closingOnBlur;
cm.on("blur", this.onBlur = function() { closingOnBlur = setTimeout(function() { completion.close(); }, 100); });
cm.on("focus", this.onFocus = function() { clearTimeout(closingOnBlur); });
}
cm.on("scroll", this.onScroll = function() {
var curScroll = cm.getScrollInfo(), editor = cm.getWrapperElement().getBoundingClientRect();
var newTop = top + startScroll.top - curScroll.top;
var point = newTop - (parentWindow.pageYOffset || (ownerDocument.documentElement || ownerDocument.body).scrollTop);
if (!below) point += hints.offsetHeight;
if (point <= editor.top || point >= editor.bottom) return completion.close();
hints.style.top = newTop + "px";
hints.style.left = (left + startScroll.left - curScroll.left) + "px";
});
CodeMirror.on(hints, "dblclick", function(e) {
var t = getHintElement(hints, e.target || e.srcElement);
if (t && t.hintId != null) {widget.changeActive(t.hintId); widget.pick();}
});
CodeMirror.on(hints, "click", function(e) {
var t = getHintElement(hints, e.target || e.srcElement);
if (t && t.hintId != null) {
widget.changeActive(t.hintId);
if (completion.options.completeOnSingleClick) widget.pick();
}
});
CodeMirror.on(hints, "mousedown", function() {
setTimeout(function(){cm.focus();}, 20);
});
this.scrollToActive()
CodeMirror.signal(data, "select", completions[this.selectedHint], hints.childNodes[this.selectedHint]);
return true;
}
Widget.prototype = {
close: function() {
if (this.completion.widget != this) return;
this.completion.widget = null;
this.hints.parentNode.removeChild(this.hints);
this.completion.cm.removeKeyMap(this.keyMap);
var cm = this.completion.cm;
if (this.completion.options.closeOnUnfocus) {
cm.off("blur", this.onBlur);
cm.off("focus", this.onFocus);
}
cm.off("scroll", this.onScroll);
},
disable: function() {
this.completion.cm.removeKeyMap(this.keyMap);
var widget = this;
this.keyMap = {Enter: function() { widget.picked = true; }};
this.completion.cm.addKeyMap(this.keyMap);
},
pick: function() {
this.completion.pick(this.data, this.selectedHint);
},
changeActive: function(i, avoidWrap) {
if (i >= this.data.list.length)
i = avoidWrap ? this.data.list.length - 1 : 0;
else if (i < 0)
i = avoidWrap ? 0 : this.data.list.length - 1;
if (this.selectedHint == i) return;
var node = this.hints.childNodes[this.selectedHint];
if (node) node.className = node.className.replace(" " + ACTIVE_HINT_ELEMENT_CLASS, "");
node = this.hints.childNodes[this.selectedHint = i];
node.className += " " + ACTIVE_HINT_ELEMENT_CLASS;
this.scrollToActive()
CodeMirror.signal(this.data, "select", this.data.list[this.selectedHint], node);
},
scrollToActive: function() {
var margin = this.completion.options.scrollMargin || 0;
var node1 = this.hints.childNodes[Math.max(0, this.selectedHint - margin)];
var node2 = this.hints.childNodes[Math.min(this.data.list.length - 1, this.selectedHint + margin)];
var firstNode = this.hints.firstChild;
if (node1.offsetTop < this.hints.scrollTop)
this.hints.scrollTop = node1.offsetTop - firstNode.offsetTop;
else if (node2.offsetTop + node2.offsetHeight > this.hints.scrollTop + this.hints.clientHeight)
this.hints.scrollTop = node2.offsetTop + node2.offsetHeight - this.hints.clientHeight + firstNode.offsetTop;
},
screenAmount: function() {
return Math.floor(this.hints.clientHeight / this.hints.firstChild.offsetHeight) || 1;
}
};
function applicableHelpers(cm, helpers) {
if (!cm.somethingSelected()) return helpers
var result = []
for (var i = 0; i < helpers.length; i++)
if (helpers[i].supportsSelection) result.push(helpers[i])
return result
}
function fetchHints(hint, cm, options, callback) {
if (hint.async) {
hint(cm, callback, options)
} else {
var result = hint(cm, options)
if (result && result.then) result.then(callback)
else callback(result)
}
}
function resolveAutoHints(cm, pos) {
var helpers = cm.getHelpers(pos, "hint"), words
if (helpers.length) {
var resolved = function(cm, callback, options) {
var app = applicableHelpers(cm, helpers);
function run(i) {
if (i == app.length) return callback(null)
fetchHints(app[i], cm, options, function(result) {
if (result && result.list.length > 0) callback(result)
else run(i + 1)
})
}
run(0)
}
resolved.async = true
resolved.supportsSelection = true
return resolved
} else if (words = cm.getHelper(cm.getCursor(), "hintWords")) {
return function(cm) { return CodeMirror.hint.fromList(cm, {words: words}) }
} else if (CodeMirror.hint.anyword) {
return function(cm, options) { return CodeMirror.hint.anyword(cm, options) }
} else {
return function() {}
}
}
CodeMirror.registerHelper("hint", "auto", {
resolve: resolveAutoHints
});
CodeMirror.registerHelper("hint", "fromList", function(cm, options) {
var cur = cm.getCursor(), token = cm.getTokenAt(cur)
var term, from = CodeMirror.Pos(cur.line, token.start), to = cur
if (token.start < cur.ch && /\w/.test(token.string.charAt(cur.ch - token.start - 1))) {
term = token.string.substr(0, cur.ch - token.start)
} else {
term = ""
from = cur
}
var found = [];
for (var i = 0; i < options.words.length; i++) {
var word = options.words[i];
if (word.slice(0, term.length) == term)
found.push(word);
}
if (found.length) return {list: found, from: from, to: to};
});
CodeMirror.commands.autocomplete = CodeMirror.showHint;
var defaultOptions = {
hint: CodeMirror.hint.auto,
completeSingle: true,
alignWithWord: true,
closeCharacters: /[\s()\[\]{};:>,]/,
closeOnUnfocus: true,
completeOnSingleClick: true,
container: null,
customKeys: null,
extraKeys: null
};
CodeMirror.defineOption("hintOptions", null);
});

View File

@ -0,0 +1,153 @@
// CodeMirror, copyright (c) by Marijn Haverbeke and others
// Distributed under an MIT license: https://codemirror.net/LICENSE
(function(mod) {
if (typeof exports == "object" && typeof module == "object") // CommonJS
mod(require("../../lib/codemirror"));
else if (typeof define == "function" && define.amd) // AMD
define(["../../lib/codemirror"], mod);
else // Plain browser env
mod(CodeMirror);
})(function(CodeMirror) {
"use strict";
function Bar(cls, orientation, scroll) {
this.orientation = orientation;
this.scroll = scroll;
this.screen = this.total = this.size = 1;
this.pos = 0;
this.node = document.createElement("div");
this.node.className = cls + "-" + orientation;
this.inner = this.node.appendChild(document.createElement("div"));
var self = this;
CodeMirror.on(this.inner, "mousedown", function(e) {
if (e.which != 1) return;
CodeMirror.e_preventDefault(e);
var axis = self.orientation == "horizontal" ? "pageX" : "pageY";
var start = e[axis], startpos = self.pos;
function done() {
CodeMirror.off(document, "mousemove", move);
CodeMirror.off(document, "mouseup", done);
}
function move(e) {
if (e.which != 1) return done();
self.moveTo(startpos + (e[axis] - start) * (self.total / self.size));
}
CodeMirror.on(document, "mousemove", move);
CodeMirror.on(document, "mouseup", done);
});
CodeMirror.on(this.node, "click", function(e) {
CodeMirror.e_preventDefault(e);
var innerBox = self.inner.getBoundingClientRect(), where;
if (self.orientation == "horizontal")
where = e.clientX < innerBox.left ? -1 : e.clientX > innerBox.right ? 1 : 0;
else
where = e.clientY < innerBox.top ? -1 : e.clientY > innerBox.bottom ? 1 : 0;
self.moveTo(self.pos + where * self.screen);
});
function onWheel(e) {
var moved = CodeMirror.wheelEventPixels(e)[self.orientation == "horizontal" ? "x" : "y"];
var oldPos = self.pos;
self.moveTo(self.pos + moved);
if (self.pos != oldPos) CodeMirror.e_preventDefault(e);
}
CodeMirror.on(this.node, "mousewheel", onWheel);
CodeMirror.on(this.node, "DOMMouseScroll", onWheel);
}
Bar.prototype.setPos = function(pos, force) {
if (pos < 0) pos = 0;
if (pos > this.total - this.screen) pos = this.total - this.screen;
if (!force && pos == this.pos) return false;
this.pos = pos;
this.inner.style[this.orientation == "horizontal" ? "left" : "top"] =
(pos * (this.size / this.total)) + "px";
return true
};
Bar.prototype.moveTo = function(pos) {
if (this.setPos(pos)) this.scroll(pos, this.orientation);
}
var minButtonSize = 10;
Bar.prototype.update = function(scrollSize, clientSize, barSize) {
var sizeChanged = this.screen != clientSize || this.total != scrollSize || this.size != barSize
if (sizeChanged) {
this.screen = clientSize;
this.total = scrollSize;
this.size = barSize;
}
var buttonSize = this.screen * (this.size / this.total);
if (buttonSize < minButtonSize) {
this.size -= minButtonSize - buttonSize;
buttonSize = minButtonSize;
}
this.inner.style[this.orientation == "horizontal" ? "width" : "height"] =
buttonSize + "px";
this.setPos(this.pos, sizeChanged);
};
function SimpleScrollbars(cls, place, scroll) {
this.addClass = cls;
this.horiz = new Bar(cls, "horizontal", scroll);
place(this.horiz.node);
this.vert = new Bar(cls, "vertical", scroll);
place(this.vert.node);
this.width = null;
}
SimpleScrollbars.prototype.update = function(measure) {
if (this.width == null) {
var style = window.getComputedStyle ? window.getComputedStyle(this.horiz.node) : this.horiz.node.currentStyle;
if (style) this.width = parseInt(style.height);
}
var width = this.width || 0;
var needsH = measure.scrollWidth > measure.clientWidth + 1;
var needsV = measure.scrollHeight > measure.clientHeight + 1;
this.vert.node.style.display = needsV ? "block" : "none";
this.horiz.node.style.display = needsH ? "block" : "none";
if (needsV) {
this.vert.update(measure.scrollHeight, measure.clientHeight,
measure.viewHeight - (needsH ? width : 0));
this.vert.node.style.bottom = needsH ? width + "px" : "0";
}
if (needsH) {
this.horiz.update(measure.scrollWidth, measure.clientWidth,
measure.viewWidth - (needsV ? width : 0) - measure.barLeft);
this.horiz.node.style.right = needsV ? width + "px" : "0";
this.horiz.node.style.left = measure.barLeft + "px";
}
return {right: needsV ? width : 0, bottom: needsH ? width : 0};
};
SimpleScrollbars.prototype.setScrollTop = function(pos) {
this.vert.setPos(pos);
};
SimpleScrollbars.prototype.setScrollLeft = function(pos) {
this.horiz.setPos(pos);
};
SimpleScrollbars.prototype.clear = function() {
var parent = this.horiz.node.parentNode;
parent.removeChild(this.horiz.node);
parent.removeChild(this.vert.node);
};
CodeMirror.scrollbarModel.simple = function(place, scroll) {
return new SimpleScrollbars("CodeMirror-simplescroll", place, scroll);
};
CodeMirror.scrollbarModel.overlay = function(place, scroll) {
return new SimpleScrollbars("CodeMirror-overlayscroll", place, scroll);
};
});

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,934 @@
// CodeMirror, copyright (c) by Marijn Haverbeke and others
// Distributed under an MIT license: https://codemirror.net/LICENSE
(function(mod) {
if (typeof exports == "object" && typeof module == "object") // CommonJS
mod(require("../../lib/codemirror"));
else if (typeof define == "function" && define.amd) // AMD
define(["../../lib/codemirror"], mod);
else // Plain browser env
mod(CodeMirror);
})(function(CodeMirror) {
"use strict";
CodeMirror.defineMode("javascript", function(config, parserConfig) {
var indentUnit = config.indentUnit;
var statementIndent = parserConfig.statementIndent;
var jsonldMode = parserConfig.jsonld;
var jsonMode = parserConfig.json || jsonldMode;
var isTS = parserConfig.typescript;
var wordRE = parserConfig.wordCharacters || /[\w$\xa1-\uffff]/;
// Tokenizer
var keywords = function(){
function kw(type) {return {type: type, style: "keyword"};}
var A = kw("keyword a"), B = kw("keyword b"), C = kw("keyword c"), D = kw("keyword d");
var operator = kw("operator"), atom = {type: "atom", style: "atom"};
return {
"if": kw("if"), "while": A, "with": A, "else": B, "do": B, "try": B, "finally": B,
"return": D, "break": D, "continue": D, "new": kw("new"), "delete": C, "void": C, "throw": C,
"debugger": kw("debugger"), "var": kw("var"), "const": kw("var"), "let": kw("var"),
"function": kw("function"), "catch": kw("catch"),
"for": kw("for"), "switch": kw("switch"), "case": kw("case"), "default": kw("default"),
"in": operator, "typeof": operator, "instanceof": operator,
"true": atom, "false": atom, "null": atom, "undefined": atom, "NaN": atom, "Infinity": atom,
"this": kw("this"), "class": kw("class"), "super": kw("atom"),
"yield": C, "export": kw("export"), "import": kw("import"), "extends": C,
"await": C
};
}();
var isOperatorChar = /[+\-*&%=<>!?|~^@]/;
var isJsonldKeyword = /^@(context|id|value|language|type|container|list|set|reverse|index|base|vocab|graph)"/;
function readRegexp(stream) {
var escaped = false, next, inSet = false;
while ((next = stream.next()) != null) {
if (!escaped) {
if (next == "/" && !inSet) return;
if (next == "[") inSet = true;
else if (inSet && next == "]") inSet = false;
}
escaped = !escaped && next == "\\";
}
}
// Used as scratch variables to communicate multiple values without
// consing up tons of objects.
var type, content;
function ret(tp, style, cont) {
type = tp; content = cont;
return style;
}
function tokenBase(stream, state) {
var ch = stream.next();
if (ch == '"' || ch == "'") {
state.tokenize = tokenString(ch);
return state.tokenize(stream, state);
} else if (ch == "." && stream.match(/^\d[\d_]*(?:[eE][+\-]?[\d_]+)?/)) {
return ret("number", "number");
} else if (ch == "." && stream.match("..")) {
return ret("spread", "meta");
} else if (/[\[\]{}\(\),;\:\.]/.test(ch)) {
return ret(ch);
} else if (ch == "=" && stream.eat(">")) {
return ret("=>", "operator");
} else if (ch == "0" && stream.match(/^(?:x[\dA-Fa-f_]+|o[0-7_]+|b[01_]+)n?/)) {
return ret("number", "number");
} else if (/\d/.test(ch)) {
stream.match(/^[\d_]*(?:n|(?:\.[\d_]*)?(?:[eE][+\-]?[\d_]+)?)?/);
return ret("number", "number");
} else if (ch == "/") {
if (stream.eat("*")) {
state.tokenize = tokenComment;
return tokenComment(stream, state);
} else if (stream.eat("/")) {
stream.skipToEnd();
return ret("comment", "comment");
} else if (expressionAllowed(stream, state, 1)) {
readRegexp(stream);
stream.match(/^\b(([gimyus])(?![gimyus]*\2))+\b/);
return ret("regexp", "string-2");
} else {
stream.eat("=");
return ret("operator", "operator", stream.current());
}
} else if (ch == "`") {
state.tokenize = tokenQuasi;
return tokenQuasi(stream, state);
} else if (ch == "#" && stream.peek() == "!") {
stream.skipToEnd();
return ret("meta", "meta");
} else if (ch == "#" && stream.eatWhile(wordRE)) {
return ret("variable", "property")
} else if (ch == "<" && stream.match("!--") ||
(ch == "-" && stream.match("->") && !/\S/.test(stream.string.slice(0, stream.start)))) {
stream.skipToEnd()
return ret("comment", "comment")
} else if (isOperatorChar.test(ch)) {
if (ch != ">" || !state.lexical || state.lexical.type != ">") {
if (stream.eat("=")) {
if (ch == "!" || ch == "=") stream.eat("=")
} else if (/[<>*+\-]/.test(ch)) {
stream.eat(ch)
if (ch == ">") stream.eat(ch)
}
}
if (ch == "?" && stream.eat(".")) return ret(".")
return ret("operator", "operator", stream.current());
} else if (wordRE.test(ch)) {
stream.eatWhile(wordRE);
var word = stream.current()
if (state.lastType != ".") {
if (keywords.propertyIsEnumerable(word)) {
var kw = keywords[word]
return ret(kw.type, kw.style, word)
}
if (word == "async" && stream.match(/^(\s|\/\*.*?\*\/)*[\[\(\w]/, false))
return ret("async", "keyword", word)
}
return ret("variable", "variable", word)
}
}
function tokenString(quote) {
return function(stream, state) {
var escaped = false, next;
if (jsonldMode && stream.peek() == "@" && stream.match(isJsonldKeyword)){
state.tokenize = tokenBase;
return ret("jsonld-keyword", "meta");
}
while ((next = stream.next()) != null) {
if (next == quote && !escaped) break;
escaped = !escaped && next == "\\";
}
if (!escaped) state.tokenize = tokenBase;
return ret("string", "string");
};
}
function tokenComment(stream, state) {
var maybeEnd = false, ch;
while (ch = stream.next()) {
if (ch == "/" && maybeEnd) {
state.tokenize = tokenBase;
break;
}
maybeEnd = (ch == "*");
}
return ret("comment", "comment");
}
function tokenQuasi(stream, state) {
var escaped = false, next;
while ((next = stream.next()) != null) {
if (!escaped && (next == "`" || next == "$" && stream.eat("{"))) {
state.tokenize = tokenBase;
break;
}
escaped = !escaped && next == "\\";
}
return ret("quasi", "string-2", stream.current());
}
var brackets = "([{}])";
// This is a crude lookahead trick to try and notice that we're
// parsing the argument patterns for a fat-arrow function before we
// actually hit the arrow token. It only works if the arrow is on
// the same line as the arguments and there's no strange noise
// (comments) in between. Fallback is to only notice when we hit the
// arrow, and not declare the arguments as locals for the arrow
// body.
function findFatArrow(stream, state) {
if (state.fatArrowAt) state.fatArrowAt = null;
var arrow = stream.string.indexOf("=>", stream.start);
if (arrow < 0) return;
if (isTS) { // Try to skip TypeScript return type declarations after the arguments
var m = /:\s*(?:\w+(?:<[^>]*>|\[\])?|\{[^}]*\})\s*$/.exec(stream.string.slice(stream.start, arrow))
if (m) arrow = m.index
}
var depth = 0, sawSomething = false;
for (var pos = arrow - 1; pos >= 0; --pos) {
var ch = stream.string.charAt(pos);
var bracket = brackets.indexOf(ch);
if (bracket >= 0 && bracket < 3) {
if (!depth) { ++pos; break; }
if (--depth == 0) { if (ch == "(") sawSomething = true; break; }
} else if (bracket >= 3 && bracket < 6) {
++depth;
} else if (wordRE.test(ch)) {
sawSomething = true;
} else if (/["'\/`]/.test(ch)) {
for (;; --pos) {
if (pos == 0) return
var next = stream.string.charAt(pos - 1)
if (next == ch && stream.string.charAt(pos - 2) != "\\") { pos--; break }
}
} else if (sawSomething && !depth) {
++pos;
break;
}
}
if (sawSomething && !depth) state.fatArrowAt = pos;
}
// Parser
var atomicTypes = {"atom": true, "number": true, "variable": true, "string": true, "regexp": true, "this": true, "jsonld-keyword": true};
function JSLexical(indented, column, type, align, prev, info) {
this.indented = indented;
this.column = column;
this.type = type;
this.prev = prev;
this.info = info;
if (align != null) this.align = align;
}
function inScope(state, varname) {
for (var v = state.localVars; v; v = v.next)
if (v.name == varname) return true;
for (var cx = state.context; cx; cx = cx.prev) {
for (var v = cx.vars; v; v = v.next)
if (v.name == varname) return true;
}
}
function parseJS(state, style, type, content, stream) {
var cc = state.cc;
// Communicate our context to the combinators.
// (Less wasteful than consing up a hundred closures on every call.)
cx.state = state; cx.stream = stream; cx.marked = null, cx.cc = cc; cx.style = style;
if (!state.lexical.hasOwnProperty("align"))
state.lexical.align = true;
while(true) {
var combinator = cc.length ? cc.pop() : jsonMode ? expression : statement;
if (combinator(type, content)) {
while(cc.length && cc[cc.length - 1].lex)
cc.pop()();
if (cx.marked) return cx.marked;
if (type == "variable" && inScope(state, content)) return "variable-2";
return style;
}
}
}
// Combinator utils
var cx = {state: null, column: null, marked: null, cc: null};
function pass() {
for (var i = arguments.length - 1; i >= 0; i--) cx.cc.push(arguments[i]);
}
function cont() {
pass.apply(null, arguments);
return true;
}
function inList(name, list) {
for (var v = list; v; v = v.next) if (v.name == name) return true
return false;
}
function register(varname) {
var state = cx.state;
cx.marked = "def";
if (state.context) {
if (state.lexical.info == "var" && state.context && state.context.block) {
// FIXME function decls are also not block scoped
var newContext = registerVarScoped(varname, state.context)
if (newContext != null) {
state.context = newContext
return
}
} else if (!inList(varname, state.localVars)) {
state.localVars = new Var(varname, state.localVars)
return
}
}
// Fall through means this is global
if (parserConfig.globalVars && !inList(varname, state.globalVars))
state.globalVars = new Var(varname, state.globalVars)
}
function registerVarScoped(varname, context) {
if (!context) {
return null
} else if (context.block) {
var inner = registerVarScoped(varname, context.prev)
if (!inner) return null
if (inner == context.prev) return context
return new Context(inner, context.vars, true)
} else if (inList(varname, context.vars)) {
return context
} else {
return new Context(context.prev, new Var(varname, context.vars), false)
}
}
function isModifier(name) {
return name == "public" || name == "private" || name == "protected" || name == "abstract" || name == "readonly"
}
// Combinators
function Context(prev, vars, block) { this.prev = prev; this.vars = vars; this.block = block }
function Var(name, next) { this.name = name; this.next = next }
var defaultVars = new Var("this", new Var("arguments", null))
function pushcontext() {
cx.state.context = new Context(cx.state.context, cx.state.localVars, false)
cx.state.localVars = defaultVars
}
function pushblockcontext() {
cx.state.context = new Context(cx.state.context, cx.state.localVars, true)
cx.state.localVars = null
}
function popcontext() {
cx.state.localVars = cx.state.context.vars
cx.state.context = cx.state.context.prev
}
popcontext.lex = true
function pushlex(type, info) {
var result = function() {
var state = cx.state, indent = state.indented;
if (state.lexical.type == "stat") indent = state.lexical.indented;
else for (var outer = state.lexical; outer && outer.type == ")" && outer.align; outer = outer.prev)
indent = outer.indented;
state.lexical = new JSLexical(indent, cx.stream.column(), type, null, state.lexical, info);
};
result.lex = true;
return result;
}
function poplex() {
var state = cx.state;
if (state.lexical.prev) {
if (state.lexical.type == ")")
state.indented = state.lexical.indented;
state.lexical = state.lexical.prev;
}
}
poplex.lex = true;
function expect(wanted) {
function exp(type) {
if (type == wanted) return cont();
else if (wanted == ";" || type == "}" || type == ")" || type == "]") return pass();
else return cont(exp);
};
return exp;
}
function statement(type, value) {
if (type == "var") return cont(pushlex("vardef", value), vardef, expect(";"), poplex);
if (type == "keyword a") return cont(pushlex("form"), parenExpr, statement, poplex);
if (type == "keyword b") return cont(pushlex("form"), statement, poplex);
if (type == "keyword d") return cx.stream.match(/^\s*$/, false) ? cont() : cont(pushlex("stat"), maybeexpression, expect(";"), poplex);
if (type == "debugger") return cont(expect(";"));
if (type == "{") return cont(pushlex("}"), pushblockcontext, block, poplex, popcontext);
if (type == ";") return cont();
if (type == "if") {
if (cx.state.lexical.info == "else" && cx.state.cc[cx.state.cc.length - 1] == poplex)
cx.state.cc.pop()();
return cont(pushlex("form"), parenExpr, statement, poplex, maybeelse);
}
if (type == "function") return cont(functiondef);
if (type == "for") return cont(pushlex("form"), forspec, statement, poplex);
if (type == "class" || (isTS && value == "interface")) {
cx.marked = "keyword"
return cont(pushlex("form", type == "class" ? type : value), className, poplex)
}
if (type == "variable") {
if (isTS && value == "declare") {
cx.marked = "keyword"
return cont(statement)
} else if (isTS && (value == "module" || value == "enum" || value == "type") && cx.stream.match(/^\s*\w/, false)) {
cx.marked = "keyword"
if (value == "enum") return cont(enumdef);
else if (value == "type") return cont(typename, expect("operator"), typeexpr, expect(";"));
else return cont(pushlex("form"), pattern, expect("{"), pushlex("}"), block, poplex, poplex)
} else if (isTS && value == "namespace") {
cx.marked = "keyword"
return cont(pushlex("form"), expression, statement, poplex)
} else if (isTS && value == "abstract") {
cx.marked = "keyword"
return cont(statement)
} else {
return cont(pushlex("stat"), maybelabel);
}
}
if (type == "switch") return cont(pushlex("form"), parenExpr, expect("{"), pushlex("}", "switch"), pushblockcontext,
block, poplex, poplex, popcontext);
if (type == "case") return cont(expression, expect(":"));
if (type == "default") return cont(expect(":"));
if (type == "catch") return cont(pushlex("form"), pushcontext, maybeCatchBinding, statement, poplex, popcontext);
if (type == "export") return cont(pushlex("stat"), afterExport, poplex);
if (type == "import") return cont(pushlex("stat"), afterImport, poplex);
if (type == "async") return cont(statement)
if (value == "@") return cont(expression, statement)
return pass(pushlex("stat"), expression, expect(";"), poplex);
}
function maybeCatchBinding(type) {
if (type == "(") return cont(funarg, expect(")"))
}
function expression(type, value) {
return expressionInner(type, value, false);
}
function expressionNoComma(type, value) {
return expressionInner(type, value, true);
}
function parenExpr(type) {
if (type != "(") return pass()
return cont(pushlex(")"), maybeexpression, expect(")"), poplex)
}
function expressionInner(type, value, noComma) {
if (cx.state.fatArrowAt == cx.stream.start) {
var body = noComma ? arrowBodyNoComma : arrowBody;
if (type == "(") return cont(pushcontext, pushlex(")"), commasep(funarg, ")"), poplex, expect("=>"), body, popcontext);
else if (type == "variable") return pass(pushcontext, pattern, expect("=>"), body, popcontext);
}
var maybeop = noComma ? maybeoperatorNoComma : maybeoperatorComma;
if (atomicTypes.hasOwnProperty(type)) return cont(maybeop);
if (type == "function") return cont(functiondef, maybeop);
if (type == "class" || (isTS && value == "interface")) { cx.marked = "keyword"; return cont(pushlex("form"), classExpression, poplex); }
if (type == "keyword c" || type == "async") return cont(noComma ? expressionNoComma : expression);
if (type == "(") return cont(pushlex(")"), maybeexpression, expect(")"), poplex, maybeop);
if (type == "operator" || type == "spread") return cont(noComma ? expressionNoComma : expression);
if (type == "[") return cont(pushlex("]"), arrayLiteral, poplex, maybeop);
if (type == "{") return contCommasep(objprop, "}", null, maybeop);
if (type == "quasi") return pass(quasi, maybeop);
if (type == "new") return cont(maybeTarget(noComma));
if (type == "import") return cont(expression);
return cont();
}
function maybeexpression(type) {
if (type.match(/[;\}\)\],]/)) return pass();
return pass(expression);
}
function maybeoperatorComma(type, value) {
if (type == ",") return cont(maybeexpression);
return maybeoperatorNoComma(type, value, false);
}
function maybeoperatorNoComma(type, value, noComma) {
var me = noComma == false ? maybeoperatorComma : maybeoperatorNoComma;
var expr = noComma == false ? expression : expressionNoComma;
if (type == "=>") return cont(pushcontext, noComma ? arrowBodyNoComma : arrowBody, popcontext);
if (type == "operator") {
if (/\+\+|--/.test(value) || isTS && value == "!") return cont(me);
if (isTS && value == "<" && cx.stream.match(/^([^<>]|<[^<>]*>)*>\s*\(/, false))
return cont(pushlex(">"), commasep(typeexpr, ">"), poplex, me);
if (value == "?") return cont(expression, expect(":"), expr);
return cont(expr);
}
if (type == "quasi") { return pass(quasi, me); }
if (type == ";") return;
if (type == "(") return contCommasep(expressionNoComma, ")", "call", me);
if (type == ".") return cont(property, me);
if (type == "[") return cont(pushlex("]"), maybeexpression, expect("]"), poplex, me);
if (isTS && value == "as") { cx.marked = "keyword"; return cont(typeexpr, me) }
if (type == "regexp") {
cx.state.lastType = cx.marked = "operator"
cx.stream.backUp(cx.stream.pos - cx.stream.start - 1)
return cont(expr)
}
}
function quasi(type, value) {
if (type != "quasi") return pass();
if (value.slice(value.length - 2) != "${") return cont(quasi);
return cont(expression, continueQuasi);
}
function continueQuasi(type) {
if (type == "}") {
cx.marked = "string-2";
cx.state.tokenize = tokenQuasi;
return cont(quasi);
}
}
function arrowBody(type) {
findFatArrow(cx.stream, cx.state);
return pass(type == "{" ? statement : expression);
}
function arrowBodyNoComma(type) {
findFatArrow(cx.stream, cx.state);
return pass(type == "{" ? statement : expressionNoComma);
}
function maybeTarget(noComma) {
return function(type) {
if (type == ".") return cont(noComma ? targetNoComma : target);
else if (type == "variable" && isTS) return cont(maybeTypeArgs, noComma ? maybeoperatorNoComma : maybeoperatorComma)
else return pass(noComma ? expressionNoComma : expression);
};
}
function target(_, value) {
if (value == "target") { cx.marked = "keyword"; return cont(maybeoperatorComma); }
}
function targetNoComma(_, value) {
if (value == "target") { cx.marked = "keyword"; return cont(maybeoperatorNoComma); }
}
function maybelabel(type) {
if (type == ":") return cont(poplex, statement);
return pass(maybeoperatorComma, expect(";"), poplex);
}
function property(type) {
if (type == "variable") {cx.marked = "property"; return cont();}
}
function objprop(type, value) {
if (type == "async") {
cx.marked = "property";
return cont(objprop);
} else if (type == "variable" || cx.style == "keyword") {
cx.marked = "property";
if (value == "get" || value == "set") return cont(getterSetter);
var m // Work around fat-arrow-detection complication for detecting typescript typed arrow params
if (isTS && cx.state.fatArrowAt == cx.stream.start && (m = cx.stream.match(/^\s*:\s*/, false)))
cx.state.fatArrowAt = cx.stream.pos + m[0].length
return cont(afterprop);
} else if (type == "number" || type == "string") {
cx.marked = jsonldMode ? "property" : (cx.style + " property");
return cont(afterprop);
} else if (type == "jsonld-keyword") {
return cont(afterprop);
} else if (isTS && isModifier(value)) {
cx.marked = "keyword"
return cont(objprop)
} else if (type == "[") {
return cont(expression, maybetype, expect("]"), afterprop);
} else if (type == "spread") {
return cont(expressionNoComma, afterprop);
} else if (value == "*") {
cx.marked = "keyword";
return cont(objprop);
} else if (type == ":") {
return pass(afterprop)
}
}
function getterSetter(type) {
if (type != "variable") return pass(afterprop);
cx.marked = "property";
return cont(functiondef);
}
function afterprop(type) {
if (type == ":") return cont(expressionNoComma);
if (type == "(") return pass(functiondef);
}
function commasep(what, end, sep) {
function proceed(type, value) {
if (sep ? sep.indexOf(type) > -1 : type == ",") {
var lex = cx.state.lexical;
if (lex.info == "call") lex.pos = (lex.pos || 0) + 1;
return cont(function(type, value) {
if (type == end || value == end) return pass()
return pass(what)
}, proceed);
}
if (type == end || value == end) return cont();
if (sep && sep.indexOf(";") > -1) return pass(what)
return cont(expect(end));
}
return function(type, value) {
if (type == end || value == end) return cont();
return pass(what, proceed);
};
}
function contCommasep(what, end, info) {
for (var i = 3; i < arguments.length; i++)
cx.cc.push(arguments[i]);
return cont(pushlex(end, info), commasep(what, end), poplex);
}
function block(type) {
if (type == "}") return cont();
return pass(statement, block);
}
function maybetype(type, value) {
if (isTS) {
if (type == ":") return cont(typeexpr);
if (value == "?") return cont(maybetype);
}
}
function maybetypeOrIn(type, value) {
if (isTS && (type == ":" || value == "in")) return cont(typeexpr)
}
function mayberettype(type) {
if (isTS && type == ":") {
if (cx.stream.match(/^\s*\w+\s+is\b/, false)) return cont(expression, isKW, typeexpr)
else return cont(typeexpr)
}
}
function isKW(_, value) {
if (value == "is") {
cx.marked = "keyword"
return cont()
}
}
function typeexpr(type, value) {
if (value == "keyof" || value == "typeof" || value == "infer") {
cx.marked = "keyword"
return cont(value == "typeof" ? expressionNoComma : typeexpr)
}
if (type == "variable" || value == "void") {
cx.marked = "type"
return cont(afterType)
}
if (value == "|" || value == "&") return cont(typeexpr)
if (type == "string" || type == "number" || type == "atom") return cont(afterType);
if (type == "[") return cont(pushlex("]"), commasep(typeexpr, "]", ","), poplex, afterType)
if (type == "{") return cont(pushlex("}"), commasep(typeprop, "}", ",;"), poplex, afterType)
if (type == "(") return cont(commasep(typearg, ")"), maybeReturnType, afterType)
if (type == "<") return cont(commasep(typeexpr, ">"), typeexpr)
}
function maybeReturnType(type) {
if (type == "=>") return cont(typeexpr)
}
function typeprop(type, value) {
if (type == "variable" || cx.style == "keyword") {
cx.marked = "property"
return cont(typeprop)
} else if (value == "?" || type == "number" || type == "string") {
return cont(typeprop)
} else if (type == ":") {
return cont(typeexpr)
} else if (type == "[") {
return cont(expect("variable"), maybetypeOrIn, expect("]"), typeprop)
} else if (type == "(") {
return pass(functiondecl, typeprop)
}
}
function typearg(type, value) {
if (type == "variable" && cx.stream.match(/^\s*[?:]/, false) || value == "?") return cont(typearg)
if (type == ":") return cont(typeexpr)
if (type == "spread") return cont(typearg)
return pass(typeexpr)
}
function afterType(type, value) {
if (value == "<") return cont(pushlex(">"), commasep(typeexpr, ">"), poplex, afterType)
if (value == "|" || type == "." || value == "&") return cont(typeexpr)
if (type == "[") return cont(typeexpr, expect("]"), afterType)
if (value == "extends" || value == "implements") { cx.marked = "keyword"; return cont(typeexpr) }
if (value == "?") return cont(typeexpr, expect(":"), typeexpr)
}
function maybeTypeArgs(_, value) {
if (value == "<") return cont(pushlex(">"), commasep(typeexpr, ">"), poplex, afterType)
}
function typeparam() {
return pass(typeexpr, maybeTypeDefault)
}
function maybeTypeDefault(_, value) {
if (value == "=") return cont(typeexpr)
}
function vardef(_, value) {
if (value == "enum") {cx.marked = "keyword"; return cont(enumdef)}
return pass(pattern, maybetype, maybeAssign, vardefCont);
}
function pattern(type, value) {
if (isTS && isModifier(value)) { cx.marked = "keyword"; return cont(pattern) }
if (type == "variable") { register(value); return cont(); }
if (type == "spread") return cont(pattern);
if (type == "[") return contCommasep(eltpattern, "]");
if (type == "{") return contCommasep(proppattern, "}");
}
function proppattern(type, value) {
if (type == "variable" && !cx.stream.match(/^\s*:/, false)) {
register(value);
return cont(maybeAssign);
}
if (type == "variable") cx.marked = "property";
if (type == "spread") return cont(pattern);
if (type == "}") return pass();
if (type == "[") return cont(expression, expect(']'), expect(':'), proppattern);
return cont(expect(":"), pattern, maybeAssign);
}
function eltpattern() {
return pass(pattern, maybeAssign)
}
function maybeAssign(_type, value) {
if (value == "=") return cont(expressionNoComma);
}
function vardefCont(type) {
if (type == ",") return cont(vardef);
}
function maybeelse(type, value) {
if (type == "keyword b" && value == "else") return cont(pushlex("form", "else"), statement, poplex);
}
function forspec(type, value) {
if (value == "await") return cont(forspec);
if (type == "(") return cont(pushlex(")"), forspec1, poplex);
}
function forspec1(type) {
if (type == "var") return cont(vardef, forspec2);
if (type == "variable") return cont(forspec2);
return pass(forspec2)
}
function forspec2(type, value) {
if (type == ")") return cont()
if (type == ";") return cont(forspec2)
if (value == "in" || value == "of") { cx.marked = "keyword"; return cont(expression, forspec2) }
return pass(expression, forspec2)
}
function functiondef(type, value) {
if (value == "*") {cx.marked = "keyword"; return cont(functiondef);}
if (type == "variable") {register(value); return cont(functiondef);}
if (type == "(") return cont(pushcontext, pushlex(")"), commasep(funarg, ")"), poplex, mayberettype, statement, popcontext);
if (isTS && value == "<") return cont(pushlex(">"), commasep(typeparam, ">"), poplex, functiondef)
}
function functiondecl(type, value) {
if (value == "*") {cx.marked = "keyword"; return cont(functiondecl);}
if (type == "variable") {register(value); return cont(functiondecl);}
if (type == "(") return cont(pushcontext, pushlex(")"), commasep(funarg, ")"), poplex, mayberettype, popcontext);
if (isTS && value == "<") return cont(pushlex(">"), commasep(typeparam, ">"), poplex, functiondecl)
}
function typename(type, value) {
if (type == "keyword" || type == "variable") {
cx.marked = "type"
return cont(typename)
} else if (value == "<") {
return cont(pushlex(">"), commasep(typeparam, ">"), poplex)
}
}
function funarg(type, value) {
if (value == "@") cont(expression, funarg)
if (type == "spread") return cont(funarg);
if (isTS && isModifier(value)) { cx.marked = "keyword"; return cont(funarg); }
if (isTS && type == "this") return cont(maybetype, maybeAssign)
return pass(pattern, maybetype, maybeAssign);
}
function classExpression(type, value) {
// Class expressions may have an optional name.
if (type == "variable") return className(type, value);
return classNameAfter(type, value);
}
function className(type, value) {
if (type == "variable") {register(value); return cont(classNameAfter);}
}
function classNameAfter(type, value) {
if (value == "<") return cont(pushlex(">"), commasep(typeparam, ">"), poplex, classNameAfter)
if (value == "extends" || value == "implements" || (isTS && type == ",")) {
if (value == "implements") cx.marked = "keyword";
return cont(isTS ? typeexpr : expression, classNameAfter);
}
if (type == "{") return cont(pushlex("}"), classBody, poplex);
}
function classBody(type, value) {
if (type == "async" ||
(type == "variable" &&
(value == "static" || value == "get" || value == "set" || (isTS && isModifier(value))) &&
cx.stream.match(/^\s+[\w$\xa1-\uffff]/, false))) {
cx.marked = "keyword";
return cont(classBody);
}
if (type == "variable" || cx.style == "keyword") {
cx.marked = "property";
return cont(classfield, classBody);
}
if (type == "number" || type == "string") return cont(classfield, classBody);
if (type == "[")
return cont(expression, maybetype, expect("]"), classfield, classBody)
if (value == "*") {
cx.marked = "keyword";
return cont(classBody);
}
if (isTS && type == "(") return pass(functiondecl, classBody)
if (type == ";" || type == ",") return cont(classBody);
if (type == "}") return cont();
if (value == "@") return cont(expression, classBody)
}
function classfield(type, value) {
if (value == "?") return cont(classfield)
if (type == ":") return cont(typeexpr, maybeAssign)
if (value == "=") return cont(expressionNoComma)
var context = cx.state.lexical.prev, isInterface = context && context.info == "interface"
return pass(isInterface ? functiondecl : functiondef)
}
function afterExport(type, value) {
if (value == "*") { cx.marked = "keyword"; return cont(maybeFrom, expect(";")); }
if (value == "default") { cx.marked = "keyword"; return cont(expression, expect(";")); }
if (type == "{") return cont(commasep(exportField, "}"), maybeFrom, expect(";"));
return pass(statement);
}
function exportField(type, value) {
if (value == "as") { cx.marked = "keyword"; return cont(expect("variable")); }
if (type == "variable") return pass(expressionNoComma, exportField);
}
function afterImport(type) {
if (type == "string") return cont();
if (type == "(") return pass(expression);
return pass(importSpec, maybeMoreImports, maybeFrom);
}
function importSpec(type, value) {
if (type == "{") return contCommasep(importSpec, "}");
if (type == "variable") register(value);
if (value == "*") cx.marked = "keyword";
return cont(maybeAs);
}
function maybeMoreImports(type) {
if (type == ",") return cont(importSpec, maybeMoreImports)
}
function maybeAs(_type, value) {
if (value == "as") { cx.marked = "keyword"; return cont(importSpec); }
}
function maybeFrom(_type, value) {
if (value == "from") { cx.marked = "keyword"; return cont(expression); }
}
function arrayLiteral(type) {
if (type == "]") return cont();
return pass(commasep(expressionNoComma, "]"));
}
function enumdef() {
return pass(pushlex("form"), pattern, expect("{"), pushlex("}"), commasep(enummember, "}"), poplex, poplex)
}
function enummember() {
return pass(pattern, maybeAssign);
}
function isContinuedStatement(state, textAfter) {
return state.lastType == "operator" || state.lastType == "," ||
isOperatorChar.test(textAfter.charAt(0)) ||
/[,.]/.test(textAfter.charAt(0));
}
function expressionAllowed(stream, state, backUp) {
return state.tokenize == tokenBase &&
/^(?:operator|sof|keyword [bcd]|case|new|export|default|spread|[\[{}\(,;:]|=>)$/.test(state.lastType) ||
(state.lastType == "quasi" && /\{\s*$/.test(stream.string.slice(0, stream.pos - (backUp || 0))))
}
// Interface
return {
startState: function(basecolumn) {
var state = {
tokenize: tokenBase,
lastType: "sof",
cc: [],
lexical: new JSLexical((basecolumn || 0) - indentUnit, 0, "block", false),
localVars: parserConfig.localVars,
context: parserConfig.localVars && new Context(null, null, false),
indented: basecolumn || 0
};
if (parserConfig.globalVars && typeof parserConfig.globalVars == "object")
state.globalVars = parserConfig.globalVars;
return state;
},
token: function(stream, state) {
if (stream.sol()) {
if (!state.lexical.hasOwnProperty("align"))
state.lexical.align = false;
state.indented = stream.indentation();
findFatArrow(stream, state);
}
if (state.tokenize != tokenComment && stream.eatSpace()) return null;
var style = state.tokenize(stream, state);
if (type == "comment") return style;
state.lastType = type == "operator" && (content == "++" || content == "--") ? "incdec" : type;
return parseJS(state, style, type, content, stream);
},
indent: function(state, textAfter) {
if (state.tokenize == tokenComment) return CodeMirror.Pass;
if (state.tokenize != tokenBase) return 0;
var firstChar = textAfter && textAfter.charAt(0), lexical = state.lexical, top
// Kludge to prevent 'maybelse' from blocking lexical scope pops
if (!/^\s*else\b/.test(textAfter)) for (var i = state.cc.length - 1; i >= 0; --i) {
var c = state.cc[i];
if (c == poplex) lexical = lexical.prev;
else if (c != maybeelse) break;
}
while ((lexical.type == "stat" || lexical.type == "form") &&
(firstChar == "}" || ((top = state.cc[state.cc.length - 1]) &&
(top == maybeoperatorComma || top == maybeoperatorNoComma) &&
!/^[,\.=+\-*:?[\(]/.test(textAfter))))
lexical = lexical.prev;
if (statementIndent && lexical.type == ")" && lexical.prev.type == "stat")
lexical = lexical.prev;
var type = lexical.type, closing = firstChar == type;
if (type == "vardef") return lexical.indented + (state.lastType == "operator" || state.lastType == "," ? lexical.info.length + 1 : 0);
else if (type == "form" && firstChar == "{") return lexical.indented;
else if (type == "form") return lexical.indented + indentUnit;
else if (type == "stat")
return lexical.indented + (isContinuedStatement(state, textAfter) ? statementIndent || indentUnit : 0);
else if (lexical.info == "switch" && !closing && parserConfig.doubleIndentSwitch != false)
return lexical.indented + (/^(?:case|default)\b/.test(textAfter) ? indentUnit : 2 * indentUnit);
else if (lexical.align) return lexical.column + (closing ? 0 : 1);
else return lexical.indented + (closing ? 0 : indentUnit);
},
electricInput: /^\s*(?:case .*?:|default:|\{|\})$/,
blockCommentStart: jsonMode ? null : "/*",
blockCommentEnd: jsonMode ? null : "*/",
blockCommentContinue: jsonMode ? null : " * ",
lineComment: jsonMode ? null : "//",
fold: "brace",
closeBrackets: "()[]{}''\"\"``",
helperType: jsonMode ? "json" : "javascript",
jsonldMode: jsonldMode,
jsonMode: jsonMode,
expressionAllowed: expressionAllowed,
skipExpression: function(state) {
var top = state.cc[state.cc.length - 1]
if (top == expression || top == expressionNoComma) state.cc.pop()
}
};
});
CodeMirror.registerHelper("wordChars", "javascript", /[\w$]/);
CodeMirror.defineMIME("text/javascript", "javascript");
CodeMirror.defineMIME("text/ecmascript", "javascript");
CodeMirror.defineMIME("application/javascript", "javascript");
CodeMirror.defineMIME("application/x-javascript", "javascript");
CodeMirror.defineMIME("application/ecmascript", "javascript");
CodeMirror.defineMIME("application/json", {name: "javascript", json: true});
CodeMirror.defineMIME("application/x-json", {name: "javascript", json: true});
CodeMirror.defineMIME("application/ld+json", {name: "javascript", jsonld: true});
CodeMirror.defineMIME("text/typescript", { name: "javascript", typescript: true });
CodeMirror.defineMIME("application/typescript", { name: "javascript", typescript: true });
});

View File

@ -0,0 +1,886 @@
// CodeMirror, copyright (c) by Marijn Haverbeke and others
// Distributed under an MIT license: https://codemirror.net/LICENSE
(function(mod) {
if (typeof exports == "object" && typeof module == "object") // CommonJS
mod(require("../../lib/codemirror"), require("../xml/xml"), require("../meta"));
else if (typeof define == "function" && define.amd) // AMD
define(["../../lib/codemirror", "../xml/xml", "../meta"], mod);
else // Plain browser env
mod(CodeMirror);
})(function(CodeMirror) {
"use strict";
CodeMirror.defineMode("markdown", function(cmCfg, modeCfg) {
var htmlMode = CodeMirror.getMode(cmCfg, "text/html");
var htmlModeMissing = htmlMode.name == "null"
function getMode(name) {
if (CodeMirror.findModeByName) {
var found = CodeMirror.findModeByName(name);
if (found) name = found.mime || found.mimes[0];
}
var mode = CodeMirror.getMode(cmCfg, name);
return mode.name == "null" ? null : mode;
}
// Should characters that affect highlighting be highlighted separate?
// Does not include characters that will be output (such as `1.` and `-` for lists)
if (modeCfg.highlightFormatting === undefined)
modeCfg.highlightFormatting = false;
// Maximum number of nested blockquotes. Set to 0 for infinite nesting.
// Excess `>` will emit `error` token.
if (modeCfg.maxBlockquoteDepth === undefined)
modeCfg.maxBlockquoteDepth = 0;
// Turn on task lists? ("- [ ] " and "- [x] ")
if (modeCfg.taskLists === undefined) modeCfg.taskLists = false;
// Turn on strikethrough syntax
if (modeCfg.strikethrough === undefined)
modeCfg.strikethrough = false;
if (modeCfg.emoji === undefined)
modeCfg.emoji = false;
if (modeCfg.fencedCodeBlockHighlighting === undefined)
modeCfg.fencedCodeBlockHighlighting = true;
if (modeCfg.fencedCodeBlockDefaultMode === undefined)
modeCfg.fencedCodeBlockDefaultMode = 'text/plain';
if (modeCfg.xml === undefined)
modeCfg.xml = true;
// Allow token types to be overridden by user-provided token types.
if (modeCfg.tokenTypeOverrides === undefined)
modeCfg.tokenTypeOverrides = {};
var tokenTypes = {
header: "header",
code: "comment",
quote: "quote",
list1: "variable-2",
list2: "variable-3",
list3: "keyword",
hr: "hr",
image: "image",
imageAltText: "image-alt-text",
imageMarker: "image-marker",
formatting: "formatting",
linkInline: "link",
linkEmail: "link",
linkText: "link",
linkHref: "string",
em: "em",
strong: "strong",
strikethrough: "strikethrough",
emoji: "builtin"
};
for (var tokenType in tokenTypes) {
if (tokenTypes.hasOwnProperty(tokenType) && modeCfg.tokenTypeOverrides[tokenType]) {
tokenTypes[tokenType] = modeCfg.tokenTypeOverrides[tokenType];
}
}
var hrRE = /^([*\-_])(?:\s*\1){2,}\s*$/
, listRE = /^(?:[*\-+]|^[0-9]+([.)]))\s+/
, taskListRE = /^\[(x| )\](?=\s)/i // Must follow listRE
, atxHeaderRE = modeCfg.allowAtxHeaderWithoutSpace ? /^(#+)/ : /^(#+)(?: |$)/
, setextHeaderRE = /^ {0,3}(?:\={1,}|-{2,})\s*$/
, textRE = /^[^#!\[\]*_\\<>` "'(~:]+/
, fencedCodeRE = /^(~~~+|```+)[ \t]*([\w\/+#-]*)[^\n`]*$/
, linkDefRE = /^\s*\[[^\]]+?\]:.*$/ // naive link-definition
, punctuation = /[!"#$%&'()*+,\-.\/:;<=>?@\[\\\]^_`{|}~\xA1\xA7\xAB\xB6\xB7\xBB\xBF\u037E\u0387\u055A-\u055F\u0589\u058A\u05BE\u05C0\u05C3\u05C6\u05F3\u05F4\u0609\u060A\u060C\u060D\u061B\u061E\u061F\u066A-\u066D\u06D4\u0700-\u070D\u07F7-\u07F9\u0830-\u083E\u085E\u0964\u0965\u0970\u0AF0\u0DF4\u0E4F\u0E5A\u0E5B\u0F04-\u0F12\u0F14\u0F3A-\u0F3D\u0F85\u0FD0-\u0FD4\u0FD9\u0FDA\u104A-\u104F\u10FB\u1360-\u1368\u1400\u166D\u166E\u169B\u169C\u16EB-\u16ED\u1735\u1736\u17D4-\u17D6\u17D8-\u17DA\u1800-\u180A\u1944\u1945\u1A1E\u1A1F\u1AA0-\u1AA6\u1AA8-\u1AAD\u1B5A-\u1B60\u1BFC-\u1BFF\u1C3B-\u1C3F\u1C7E\u1C7F\u1CC0-\u1CC7\u1CD3\u2010-\u2027\u2030-\u2043\u2045-\u2051\u2053-\u205E\u207D\u207E\u208D\u208E\u2308-\u230B\u2329\u232A\u2768-\u2775\u27C5\u27C6\u27E6-\u27EF\u2983-\u2998\u29D8-\u29DB\u29FC\u29FD\u2CF9-\u2CFC\u2CFE\u2CFF\u2D70\u2E00-\u2E2E\u2E30-\u2E42\u3001-\u3003\u3008-\u3011\u3014-\u301F\u3030\u303D\u30A0\u30FB\uA4FE\uA4FF\uA60D-\uA60F\uA673\uA67E\uA6F2-\uA6F7\uA874-\uA877\uA8CE\uA8CF\uA8F8-\uA8FA\uA8FC\uA92E\uA92F\uA95F\uA9C1-\uA9CD\uA9DE\uA9DF\uAA5C-\uAA5F\uAADE\uAADF\uAAF0\uAAF1\uABEB\uFD3E\uFD3F\uFE10-\uFE19\uFE30-\uFE52\uFE54-\uFE61\uFE63\uFE68\uFE6A\uFE6B\uFF01-\uFF03\uFF05-\uFF0A\uFF0C-\uFF0F\uFF1A\uFF1B\uFF1F\uFF20\uFF3B-\uFF3D\uFF3F\uFF5B\uFF5D\uFF5F-\uFF65]|\uD800[\uDD00-\uDD02\uDF9F\uDFD0]|\uD801\uDD6F|\uD802[\uDC57\uDD1F\uDD3F\uDE50-\uDE58\uDE7F\uDEF0-\uDEF6\uDF39-\uDF3F\uDF99-\uDF9C]|\uD804[\uDC47-\uDC4D\uDCBB\uDCBC\uDCBE-\uDCC1\uDD40-\uDD43\uDD74\uDD75\uDDC5-\uDDC9\uDDCD\uDDDB\uDDDD-\uDDDF\uDE38-\uDE3D\uDEA9]|\uD805[\uDCC6\uDDC1-\uDDD7\uDE41-\uDE43\uDF3C-\uDF3E]|\uD809[\uDC70-\uDC74]|\uD81A[\uDE6E\uDE6F\uDEF5\uDF37-\uDF3B\uDF44]|\uD82F\uDC9F|\uD836[\uDE87-\uDE8B]/
, expandedTab = " " // CommonMark specifies tab as 4 spaces
function switchInline(stream, state, f) {
state.f = state.inline = f;
return f(stream, state);
}
function switchBlock(stream, state, f) {
state.f = state.block = f;
return f(stream, state);
}
function lineIsEmpty(line) {
return !line || !/\S/.test(line.string)
}
// Blocks
function blankLine(state) {
// Reset linkTitle state
state.linkTitle = false;
state.linkHref = false;
state.linkText = false;
// Reset EM state
state.em = false;
// Reset STRONG state
state.strong = false;
// Reset strikethrough state
state.strikethrough = false;
// Reset state.quote
state.quote = 0;
// Reset state.indentedCode
state.indentedCode = false;
if (state.f == htmlBlock) {
var exit = htmlModeMissing
if (!exit) {
var inner = CodeMirror.innerMode(htmlMode, state.htmlState)
exit = inner.mode.name == "xml" && inner.state.tagStart === null &&
(!inner.state.context && inner.state.tokenize.isInText)
}
if (exit) {
state.f = inlineNormal;
state.block = blockNormal;
state.htmlState = null;
}
}
// Reset state.trailingSpace
state.trailingSpace = 0;
state.trailingSpaceNewLine = false;
// Mark this line as blank
state.prevLine = state.thisLine
state.thisLine = {stream: null}
return null;
}
function blockNormal(stream, state) {
var firstTokenOnLine = stream.column() === state.indentation;
var prevLineLineIsEmpty = lineIsEmpty(state.prevLine.stream);
var prevLineIsIndentedCode = state.indentedCode;
var prevLineIsHr = state.prevLine.hr;
var prevLineIsList = state.list !== false;
var maxNonCodeIndentation = (state.listStack[state.listStack.length - 1] || 0) + 3;
state.indentedCode = false;
var lineIndentation = state.indentation;
// compute once per line (on first token)
if (state.indentationDiff === null) {
state.indentationDiff = state.indentation;
if (prevLineIsList) {
state.list = null;
// While this list item's marker's indentation is less than the deepest
// list item's content's indentation,pop the deepest list item
// indentation off the stack, and update block indentation state
while (lineIndentation < state.listStack[state.listStack.length - 1]) {
state.listStack.pop();
if (state.listStack.length) {
state.indentation = state.listStack[state.listStack.length - 1];
// less than the first list's indent -> the line is no longer a list
} else {
state.list = false;
}
}
if (state.list !== false) {
state.indentationDiff = lineIndentation - state.listStack[state.listStack.length - 1]
}
}
}
// not comprehensive (currently only for setext detection purposes)
var allowsInlineContinuation = (
!prevLineLineIsEmpty && !prevLineIsHr && !state.prevLine.header &&
(!prevLineIsList || !prevLineIsIndentedCode) &&
!state.prevLine.fencedCodeEnd
);
var isHr = (state.list === false || prevLineIsHr || prevLineLineIsEmpty) &&
state.indentation <= maxNonCodeIndentation && stream.match(hrRE);
var match = null;
if (state.indentationDiff >= 4 && (prevLineIsIndentedCode || state.prevLine.fencedCodeEnd ||
state.prevLine.header || prevLineLineIsEmpty)) {
stream.skipToEnd();
state.indentedCode = true;
return tokenTypes.code;
} else if (stream.eatSpace()) {
return null;
} else if (firstTokenOnLine && state.indentation <= maxNonCodeIndentation && (match = stream.match(atxHeaderRE)) && match[1].length <= 6) {
state.quote = 0;
state.header = match[1].length;
state.thisLine.header = true;
if (modeCfg.highlightFormatting) state.formatting = "header";
state.f = state.inline;
return getType(state);
} else if (state.indentation <= maxNonCodeIndentation && stream.eat('>')) {
state.quote = firstTokenOnLine ? 1 : state.quote + 1;
if (modeCfg.highlightFormatting) state.formatting = "quote";
stream.eatSpace();
return getType(state);
} else if (!isHr && !state.setext && firstTokenOnLine && state.indentation <= maxNonCodeIndentation && (match = stream.match(listRE))) {
var listType = match[1] ? "ol" : "ul";
state.indentation = lineIndentation + stream.current().length;
state.list = true;
state.quote = 0;
// Add this list item's content's indentation to the stack
state.listStack.push(state.indentation);
// Reset inline styles which shouldn't propagate aross list items
state.em = false;
state.strong = false;
state.code = false;
state.strikethrough = false;
if (modeCfg.taskLists && stream.match(taskListRE, false)) {
state.taskList = true;
}
state.f = state.inline;
if (modeCfg.highlightFormatting) state.formatting = ["list", "list-" + listType];
return getType(state);
} else if (firstTokenOnLine && state.indentation <= maxNonCodeIndentation && (match = stream.match(fencedCodeRE, true))) {
state.quote = 0;
state.fencedEndRE = new RegExp(match[1] + "+ *$");
// try switching mode
state.localMode = modeCfg.fencedCodeBlockHighlighting && getMode(match[2] || modeCfg.fencedCodeBlockDefaultMode );
if (state.localMode) state.localState = CodeMirror.startState(state.localMode);
state.f = state.block = local;
if (modeCfg.highlightFormatting) state.formatting = "code-block";
state.code = -1
return getType(state);
// SETEXT has lowest block-scope precedence after HR, so check it after
// the others (code, blockquote, list...)
} else if (
// if setext set, indicates line after ---/===
state.setext || (
// line before ---/===
(!allowsInlineContinuation || !prevLineIsList) && !state.quote && state.list === false &&
!state.code && !isHr && !linkDefRE.test(stream.string) &&
(match = stream.lookAhead(1)) && (match = match.match(setextHeaderRE))
)
) {
if ( !state.setext ) {
state.header = match[0].charAt(0) == '=' ? 1 : 2;
state.setext = state.header;
} else {
state.header = state.setext;
// has no effect on type so we can reset it now
state.setext = 0;
stream.skipToEnd();
if (modeCfg.highlightFormatting) state.formatting = "header";
}
state.thisLine.header = true;
state.f = state.inline;
return getType(state);
} else if (isHr) {
stream.skipToEnd();
state.hr = true;
state.thisLine.hr = true;
return tokenTypes.hr;
} else if (stream.peek() === '[') {
return switchInline(stream, state, footnoteLink);
}
return switchInline(stream, state, state.inline);
}
function htmlBlock(stream, state) {
var style = htmlMode.token(stream, state.htmlState);
if (!htmlModeMissing) {
var inner = CodeMirror.innerMode(htmlMode, state.htmlState)
if ((inner.mode.name == "xml" && inner.state.tagStart === null &&
(!inner.state.context && inner.state.tokenize.isInText)) ||
(state.md_inside && stream.current().indexOf(">") > -1)) {
state.f = inlineNormal;
state.block = blockNormal;
state.htmlState = null;
}
}
return style;
}
function local(stream, state) {
var currListInd = state.listStack[state.listStack.length - 1] || 0;
var hasExitedList = state.indentation < currListInd;
var maxFencedEndInd = currListInd + 3;
if (state.fencedEndRE && state.indentation <= maxFencedEndInd && (hasExitedList || stream.match(state.fencedEndRE))) {
if (modeCfg.highlightFormatting) state.formatting = "code-block";
var returnType;
if (!hasExitedList) returnType = getType(state)
state.localMode = state.localState = null;
state.block = blockNormal;
state.f = inlineNormal;
state.fencedEndRE = null;
state.code = 0
state.thisLine.fencedCodeEnd = true;
if (hasExitedList) return switchBlock(stream, state, state.block);
return returnType;
} else if (state.localMode) {
return state.localMode.token(stream, state.localState);
} else {
stream.skipToEnd();
return tokenTypes.code;
}
}
// Inline
function getType(state) {
var styles = [];
if (state.formatting) {
styles.push(tokenTypes.formatting);
if (typeof state.formatting === "string") state.formatting = [state.formatting];
for (var i = 0; i < state.formatting.length; i++) {
styles.push(tokenTypes.formatting + "-" + state.formatting[i]);
if (state.formatting[i] === "header") {
styles.push(tokenTypes.formatting + "-" + state.formatting[i] + "-" + state.header);
}
// Add `formatting-quote` and `formatting-quote-#` for blockquotes
// Add `error` instead if the maximum blockquote nesting depth is passed
if (state.formatting[i] === "quote") {
if (!modeCfg.maxBlockquoteDepth || modeCfg.maxBlockquoteDepth >= state.quote) {
styles.push(tokenTypes.formatting + "-" + state.formatting[i] + "-" + state.quote);
} else {
styles.push("error");
}
}
}
}
if (state.taskOpen) {
styles.push("meta");
return styles.length ? styles.join(' ') : null;
}
if (state.taskClosed) {
styles.push("property");
return styles.length ? styles.join(' ') : null;
}
if (state.linkHref) {
styles.push(tokenTypes.linkHref, "url");
} else { // Only apply inline styles to non-url text
if (state.strong) { styles.push(tokenTypes.strong); }
if (state.em) { styles.push(tokenTypes.em); }
if (state.strikethrough) { styles.push(tokenTypes.strikethrough); }
if (state.emoji) { styles.push(tokenTypes.emoji); }
if (state.linkText) { styles.push(tokenTypes.linkText); }
if (state.code) { styles.push(tokenTypes.code); }
if (state.image) { styles.push(tokenTypes.image); }
if (state.imageAltText) { styles.push(tokenTypes.imageAltText, "link"); }
if (state.imageMarker) { styles.push(tokenTypes.imageMarker); }
}
if (state.header) { styles.push(tokenTypes.header, tokenTypes.header + "-" + state.header); }
if (state.quote) {
styles.push(tokenTypes.quote);
// Add `quote-#` where the maximum for `#` is modeCfg.maxBlockquoteDepth
if (!modeCfg.maxBlockquoteDepth || modeCfg.maxBlockquoteDepth >= state.quote) {
styles.push(tokenTypes.quote + "-" + state.quote);
} else {
styles.push(tokenTypes.quote + "-" + modeCfg.maxBlockquoteDepth);
}
}
if (state.list !== false) {
var listMod = (state.listStack.length - 1) % 3;
if (!listMod) {
styles.push(tokenTypes.list1);
} else if (listMod === 1) {
styles.push(tokenTypes.list2);
} else {
styles.push(tokenTypes.list3);
}
}
if (state.trailingSpaceNewLine) {
styles.push("trailing-space-new-line");
} else if (state.trailingSpace) {
styles.push("trailing-space-" + (state.trailingSpace % 2 ? "a" : "b"));
}
return styles.length ? styles.join(' ') : null;
}
function handleText(stream, state) {
if (stream.match(textRE, true)) {
return getType(state);
}
return undefined;
}
function inlineNormal(stream, state) {
var style = state.text(stream, state);
if (typeof style !== 'undefined')
return style;
if (state.list) { // List marker (*, +, -, 1., etc)
state.list = null;
return getType(state);
}
if (state.taskList) {
var taskOpen = stream.match(taskListRE, true)[1] === " ";
if (taskOpen) state.taskOpen = true;
else state.taskClosed = true;
if (modeCfg.highlightFormatting) state.formatting = "task";
state.taskList = false;
return getType(state);
}
state.taskOpen = false;
state.taskClosed = false;
if (state.header && stream.match(/^#+$/, true)) {
if (modeCfg.highlightFormatting) state.formatting = "header";
return getType(state);
}
var ch = stream.next();
// Matches link titles present on next line
if (state.linkTitle) {
state.linkTitle = false;
var matchCh = ch;
if (ch === '(') {
matchCh = ')';
}
matchCh = (matchCh+'').replace(/([.?*+^\[\]\\(){}|-])/g, "\\$1");
var regex = '^\\s*(?:[^' + matchCh + '\\\\]+|\\\\\\\\|\\\\.)' + matchCh;
if (stream.match(new RegExp(regex), true)) {
return tokenTypes.linkHref;
}
}
// If this block is changed, it may need to be updated in GFM mode
if (ch === '`') {
var previousFormatting = state.formatting;
if (modeCfg.highlightFormatting) state.formatting = "code";
stream.eatWhile('`');
var count = stream.current().length
if (state.code == 0 && (!state.quote || count == 1)) {
state.code = count
return getType(state)
} else if (count == state.code) { // Must be exact
var t = getType(state)
state.code = 0
return t
} else {
state.formatting = previousFormatting
return getType(state)
}
} else if (state.code) {
return getType(state);
}
if (ch === '\\') {
stream.next();
if (modeCfg.highlightFormatting) {
var type = getType(state);
var formattingEscape = tokenTypes.formatting + "-escape";
return type ? type + " " + formattingEscape : formattingEscape;
}
}
if (ch === '!' && stream.match(/\[[^\]]*\] ?(?:\(|\[)/, false)) {
state.imageMarker = true;
state.image = true;
if (modeCfg.highlightFormatting) state.formatting = "image";
return getType(state);
}
if (ch === '[' && state.imageMarker && stream.match(/[^\]]*\](\(.*?\)| ?\[.*?\])/, false)) {
state.imageMarker = false;
state.imageAltText = true
if (modeCfg.highlightFormatting) state.formatting = "image";
return getType(state);
}
if (ch === ']' && state.imageAltText) {
if (modeCfg.highlightFormatting) state.formatting = "image";
var type = getType(state);
state.imageAltText = false;
state.image = false;
state.inline = state.f = linkHref;
return type;
}
if (ch === '[' && !state.image) {
if (state.linkText && stream.match(/^.*?\]/)) return getType(state)
state.linkText = true;
if (modeCfg.highlightFormatting) state.formatting = "link";
return getType(state);
}
if (ch === ']' && state.linkText) {
if (modeCfg.highlightFormatting) state.formatting = "link";
var type = getType(state);
state.linkText = false;
state.inline = state.f = stream.match(/\(.*?\)| ?\[.*?\]/, false) ? linkHref : inlineNormal
return type;
}
if (ch === '<' && stream.match(/^(https?|ftps?):\/\/(?:[^\\>]|\\.)+>/, false)) {
state.f = state.inline = linkInline;
if (modeCfg.highlightFormatting) state.formatting = "link";
var type = getType(state);
if (type){
type += " ";
} else {
type = "";
}
return type + tokenTypes.linkInline;
}
if (ch === '<' && stream.match(/^[^> \\]+@(?:[^\\>]|\\.)+>/, false)) {
state.f = state.inline = linkInline;
if (modeCfg.highlightFormatting) state.formatting = "link";
var type = getType(state);
if (type){
type += " ";
} else {
type = "";
}
return type + tokenTypes.linkEmail;
}
if (modeCfg.xml && ch === '<' && stream.match(/^(!--|\?|!\[CDATA\[|[a-z][a-z0-9-]*(?:\s+[a-z_:.\-]+(?:\s*=\s*[^>]+)?)*\s*(?:>|$))/i, false)) {
var end = stream.string.indexOf(">", stream.pos);
if (end != -1) {
var atts = stream.string.substring(stream.start, end);
if (/markdown\s*=\s*('|"){0,1}1('|"){0,1}/.test(atts)) state.md_inside = true;
}
stream.backUp(1);
state.htmlState = CodeMirror.startState(htmlMode);
return switchBlock(stream, state, htmlBlock);
}
if (modeCfg.xml && ch === '<' && stream.match(/^\/\w*?>/)) {
state.md_inside = false;
return "tag";
} else if (ch === "*" || ch === "_") {
var len = 1, before = stream.pos == 1 ? " " : stream.string.charAt(stream.pos - 2)
while (len < 3 && stream.eat(ch)) len++
var after = stream.peek() || " "
// See http://spec.commonmark.org/0.27/#emphasis-and-strong-emphasis
var leftFlanking = !/\s/.test(after) && (!punctuation.test(after) || /\s/.test(before) || punctuation.test(before))
var rightFlanking = !/\s/.test(before) && (!punctuation.test(before) || /\s/.test(after) || punctuation.test(after))
var setEm = null, setStrong = null
if (len % 2) { // Em
if (!state.em && leftFlanking && (ch === "*" || !rightFlanking || punctuation.test(before)))
setEm = true
else if (state.em == ch && rightFlanking && (ch === "*" || !leftFlanking || punctuation.test(after)))
setEm = false
}
if (len > 1) { // Strong
if (!state.strong && leftFlanking && (ch === "*" || !rightFlanking || punctuation.test(before)))
setStrong = true
else if (state.strong == ch && rightFlanking && (ch === "*" || !leftFlanking || punctuation.test(after)))
setStrong = false
}
if (setStrong != null || setEm != null) {
if (modeCfg.highlightFormatting) state.formatting = setEm == null ? "strong" : setStrong == null ? "em" : "strong em"
if (setEm === true) state.em = ch
if (setStrong === true) state.strong = ch
var t = getType(state)
if (setEm === false) state.em = false
if (setStrong === false) state.strong = false
return t
}
} else if (ch === ' ') {
if (stream.eat('*') || stream.eat('_')) { // Probably surrounded by spaces
if (stream.peek() === ' ') { // Surrounded by spaces, ignore
return getType(state);
} else { // Not surrounded by spaces, back up pointer
stream.backUp(1);
}
}
}
if (modeCfg.strikethrough) {
if (ch === '~' && stream.eatWhile(ch)) {
if (state.strikethrough) {// Remove strikethrough
if (modeCfg.highlightFormatting) state.formatting = "strikethrough";
var t = getType(state);
state.strikethrough = false;
return t;
} else if (stream.match(/^[^\s]/, false)) {// Add strikethrough
state.strikethrough = true;
if (modeCfg.highlightFormatting) state.formatting = "strikethrough";
return getType(state);
}
} else if (ch === ' ') {
if (stream.match(/^~~/, true)) { // Probably surrounded by space
if (stream.peek() === ' ') { // Surrounded by spaces, ignore
return getType(state);
} else { // Not surrounded by spaces, back up pointer
stream.backUp(2);
}
}
}
}
if (modeCfg.emoji && ch === ":" && stream.match(/^(?:[a-z_\d+][a-z_\d+-]*|\-[a-z_\d+][a-z_\d+-]*):/)) {
state.emoji = true;
if (modeCfg.highlightFormatting) state.formatting = "emoji";
var retType = getType(state);
state.emoji = false;
return retType;
}
if (ch === ' ') {
if (stream.match(/^ +$/, false)) {
state.trailingSpace++;
} else if (state.trailingSpace) {
state.trailingSpaceNewLine = true;
}
}
return getType(state);
}
function linkInline(stream, state) {
var ch = stream.next();
if (ch === ">") {
state.f = state.inline = inlineNormal;
if (modeCfg.highlightFormatting) state.formatting = "link";
var type = getType(state);
if (type){
type += " ";
} else {
type = "";
}
return type + tokenTypes.linkInline;
}
stream.match(/^[^>]+/, true);
return tokenTypes.linkInline;
}
function linkHref(stream, state) {
// Check if space, and return NULL if so (to avoid marking the space)
if(stream.eatSpace()){
return null;
}
var ch = stream.next();
if (ch === '(' || ch === '[') {
state.f = state.inline = getLinkHrefInside(ch === "(" ? ")" : "]");
if (modeCfg.highlightFormatting) state.formatting = "link-string";
state.linkHref = true;
return getType(state);
}
return 'error';
}
var linkRE = {
")": /^(?:[^\\\(\)]|\\.|\((?:[^\\\(\)]|\\.)*\))*?(?=\))/,
"]": /^(?:[^\\\[\]]|\\.|\[(?:[^\\\[\]]|\\.)*\])*?(?=\])/
}
function getLinkHrefInside(endChar) {
return function(stream, state) {
var ch = stream.next();
if (ch === endChar) {
state.f = state.inline = inlineNormal;
if (modeCfg.highlightFormatting) state.formatting = "link-string";
var returnState = getType(state);
state.linkHref = false;
return returnState;
}
stream.match(linkRE[endChar])
state.linkHref = true;
return getType(state);
};
}
function footnoteLink(stream, state) {
if (stream.match(/^([^\]\\]|\\.)*\]:/, false)) {
state.f = footnoteLinkInside;
stream.next(); // Consume [
if (modeCfg.highlightFormatting) state.formatting = "link";
state.linkText = true;
return getType(state);
}
return switchInline(stream, state, inlineNormal);
}
function footnoteLinkInside(stream, state) {
if (stream.match(/^\]:/, true)) {
state.f = state.inline = footnoteUrl;
if (modeCfg.highlightFormatting) state.formatting = "link";
var returnType = getType(state);
state.linkText = false;
return returnType;
}
stream.match(/^([^\]\\]|\\.)+/, true);
return tokenTypes.linkText;
}
function footnoteUrl(stream, state) {
// Check if space, and return NULL if so (to avoid marking the space)
if(stream.eatSpace()){
return null;
}
// Match URL
stream.match(/^[^\s]+/, true);
// Check for link title
if (stream.peek() === undefined) { // End of line, set flag to check next line
state.linkTitle = true;
} else { // More content on line, check if link title
stream.match(/^(?:\s+(?:"(?:[^"\\]|\\\\|\\.)+"|'(?:[^'\\]|\\\\|\\.)+'|\((?:[^)\\]|\\\\|\\.)+\)))?/, true);
}
state.f = state.inline = inlineNormal;
return tokenTypes.linkHref + " url";
}
var mode = {
startState: function() {
return {
f: blockNormal,
prevLine: {stream: null},
thisLine: {stream: null},
block: blockNormal,
htmlState: null,
indentation: 0,
inline: inlineNormal,
text: handleText,
formatting: false,
linkText: false,
linkHref: false,
linkTitle: false,
code: 0,
em: false,
strong: false,
header: 0,
setext: 0,
hr: false,
taskList: false,
list: false,
listStack: [],
quote: 0,
trailingSpace: 0,
trailingSpaceNewLine: false,
strikethrough: false,
emoji: false,
fencedEndRE: null
};
},
copyState: function(s) {
return {
f: s.f,
prevLine: s.prevLine,
thisLine: s.thisLine,
block: s.block,
htmlState: s.htmlState && CodeMirror.copyState(htmlMode, s.htmlState),
indentation: s.indentation,
localMode: s.localMode,
localState: s.localMode ? CodeMirror.copyState(s.localMode, s.localState) : null,
inline: s.inline,
text: s.text,
formatting: false,
linkText: s.linkText,
linkTitle: s.linkTitle,
linkHref: s.linkHref,
code: s.code,
em: s.em,
strong: s.strong,
strikethrough: s.strikethrough,
emoji: s.emoji,
header: s.header,
setext: s.setext,
hr: s.hr,
taskList: s.taskList,
list: s.list,
listStack: s.listStack.slice(0),
quote: s.quote,
indentedCode: s.indentedCode,
trailingSpace: s.trailingSpace,
trailingSpaceNewLine: s.trailingSpaceNewLine,
md_inside: s.md_inside,
fencedEndRE: s.fencedEndRE
};
},
token: function(stream, state) {
// Reset state.formatting
state.formatting = false;
if (stream != state.thisLine.stream) {
state.header = 0;
state.hr = false;
if (stream.match(/^\s*$/, true)) {
blankLine(state);
return null;
}
state.prevLine = state.thisLine
state.thisLine = {stream: stream}
// Reset state.taskList
state.taskList = false;
// Reset state.trailingSpace
state.trailingSpace = 0;
state.trailingSpaceNewLine = false;
if (!state.localState) {
state.f = state.block;
if (state.f != htmlBlock) {
var indentation = stream.match(/^\s*/, true)[0].replace(/\t/g, expandedTab).length;
state.indentation = indentation;
state.indentationDiff = null;
if (indentation > 0) return null;
}
}
}
return state.f(stream, state);
},
innerMode: function(state) {
if (state.block == htmlBlock) return {state: state.htmlState, mode: htmlMode};
if (state.localState) return {state: state.localState, mode: state.localMode};
return {state: state, mode: mode};
},
indent: function(state, textAfter, line) {
if (state.block == htmlBlock && htmlMode.indent) return htmlMode.indent(state.htmlState, textAfter, line)
if (state.localState && state.localMode.indent) return state.localMode.indent(state.localState, textAfter, line)
return CodeMirror.Pass
},
blankLine: blankLine,
getType: getType,
blockCommentStart: "<!--",
blockCommentEnd: "-->",
closeBrackets: "()[]{}''\"\"``",
fold: "markdown"
};
return mode;
}, "xml");
CodeMirror.defineMIME("text/markdown", "markdown");
CodeMirror.defineMIME("text/x-markdown", "markdown");
});

44
app/webroot/js/highlight.min.js vendored Normal file

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,788 @@
'use strict';
var debounceDelay = 150, slowDebounceDelay = 3000;
var renderTimer, scrollTimer, attackMatrixTimer, eventgraphTimer;
var scrollMap;
var $splitContainer, $editorContainer, $rawContainer, $viewerContainer, $fullContainer, $resizableHandle, $autocompletionCB, $syncScrollCB, $autoRenderMarkdownCB, $topBar, $lastModifiedField, $markdownDropdownRulesMenu, $markdownDropdownGeneralMenu, $toggleFullScreenMode, $loadingBackdrop
var $editor, $viewer, $raw
var $saveMarkdownButton, $mardownViewerToolbar
var loadingSpanAnimation = '<span id="loadingSpan" class="fa fa-spin fa-spinner" style="margin-left: 5px;"></span>';
var contentChanged = false
var defaultMode = 'viewer'
var currentMode
var splitEdit = true
var noEditorScroll = false // Necessary as onscroll cannot be unbound from CM
$(document).ready(function() {
$splitContainer = $('.split-container')
$editorContainer = $('#editor-container')
$viewerContainer = $('#viewer-container')
$rawContainer = $('div.raw-container')
$fullContainer = $('.markdownEditor-full-container')
$resizableHandle = $('#resizable-handle')
$editor = $('#editor')
$viewer = $('#viewer')
$raw = $('#raw')
$mardownViewerToolbar = $('#mardown-viewer-toolbar')
$loadingBackdrop = $('#loadingBackdrop')
$saveMarkdownButton = $('#saveMarkdownButton')
$autocompletionCB = $('#autocompletionCB')
$syncScrollCB = $('#syncScrollCB')
$autoRenderMarkdownCB = $('#autoRenderMarkdownCB')
$toggleFullScreenMode = $('#toggleFullScreenMode')
$topBar = $('#top-bar')
$lastModifiedField = $('#lastModifiedField')
$markdownDropdownRulesMenu = $('#markdown-dropdown-rules-menu')
$markdownDropdownGeneralMenu = $('#markdownDropdownGeneralMenu')
initMarkdownIt()
if (canEdit) {
initCodeMirror()
toggleSaveButton(false)
}
setMode(defaultMode)
if (canEdit) {
setEditorData(originalRaw);
$editorContainer.resizable({
handles: {
e: $resizableHandle
},
grid: 50,
minWidth: 300,
maxWidth: window.innerWidth -220 - 300,
start: function( event, ui ) {
ui.helper.detach().appendTo('.markdownEditor-full-container')
$fullContainer.css('position', 'inherit') // Need as resizable helper is position absolute
},
stop: function() {
$fullContainer.css('position', 'relative')
cm.refresh()
scrollMap = null;
},
helper: 'ui-resizable-helper'
})
}
doRender()
if (typeof injectCustomRulesMenu === 'function') {
injectCustomRulesMenu()
}
reloadRuleEnabledUI()
if (canEdit) {
$editorContainer.on('touchstart mouseover', function () {
noEditorScroll = false
$viewerContainer.off('scroll');
cm.on('scroll', function(event) {
if (!noEditorScroll) {
doScroll(syncResultScroll)
}
});
});
$viewerContainer.on('touchstart mouseover', function () {
noEditorScroll = true
$viewerContainer.on('scroll', function() {
doScroll(syncSrcScroll)
});
});
if (typeof cmCustomSetup === 'function') {
cmCustomSetup()
}
if (typeof insertCustomToolbarButtons === 'function') {
insertCustomToolbarButtons()
}
refreshLastUpdatedField()
$(window).bind('beforeunload', function(e){
if (!contentChanged) {
return undefined;
}
(e || window.event).returnValue = confirmationMessageUnsavedChanges; //Gecko + IE
return confirmationMessageUnsavedChanges; //Gecko + Webkit, Safari, Chrome etc.
})
}
})
function initMarkdownIt() {
var mdOptions = {
highlight: function (str, lang) {
if (lang && hljs.getLanguage(lang)) {
try {
return hljs.highlight(lang, str, true).value;
} catch (__) {}
}
return ''; // use external default escaping
}
}
md = window.markdownit('default', mdOptions);
md.disable([ 'link', 'image' ])
md.renderer.rules.table_open = function () {
return '<table class="table table-striped">\n';
};
md.renderer.rules.paragraph_open = injectLineNumbers;
md.renderer.rules.heading_open = injectLineNumbers;
if (typeof markdownItCustomPostInit === 'function') {
markdownItCustomPostInit()
}
}
function initCodeMirror() {
var cmOptions = {
mode: 'markdown',
theme:'default',
lineNumbers: true,
indentUnit: 4,
showCursorWhenSelecting: true,
lineWrapping: true,
scrollbarStyle: 'overlay',
extraKeys: {
"Esc": function(cm) {
},
"Ctrl-Space": "autocomplete",
"Ctrl-B": function() { replacementAction('bold') },
"Ctrl-I": function() { replacementAction('italic') },
"Ctrl-H": function() { replacementAction('heading') },
"Ctrl-M": function() { replacementAction('element') },
},
hintOptions: {
completeSingle: false
},
}
if (typeof cmCustomHints === 'function') {
cmOptions['hintOptions']['hint'] = cmCustomHints
}
cm = CodeMirror.fromTextArea($editor[0], cmOptions);
cm.on('changes', function(cm, event) {
if (event[0].origin !== 'setValue') {
invalidateContentCache()
}
doRender();
})
cm.on("keyup", function (cm, event) {
if (!cm.state.completionActive && /*Enables keyboard navigation in autocomplete list*/
event.keyCode != 13 && /*Enter - do not open autocomplete list just after item has been selected in it*/
$autocompletionCB.prop('checked')) {
cm.showHint()
}
});
checkIfFullScreenEnabled()
}
function markdownItToggleRule(rulename, event) {
if (event !== undefined) {
event.stopPropagation()
}
var enabled
var CustomRuleRes, rule
if (typeof markdownItToggleCustomRule === 'function') {
CustomRuleRes = markdownItToggleCustomRule(rulename, event)
}
if (CustomRuleRes.found) {
enabled = CustomRuleRes.enabled
} else {
if (rulename == 'image') {
rule = getRuleStatus('inline', 'ruler', 'image')
if (rule !== false) {
enabled = rule.enabled
}
} else if (rulename == 'link') {
rule = getRuleStatus('inline', 'ruler', 'link')
if (rule !== false) {
enabled = rule.enabled
}
}
}
if (enabled !== undefined) {
if (enabled) {
md.disable([rulename])
} else {
md.enable([rulename])
}
}
doRender()
reloadRuleEnabledUI()
}
function reloadRuleEnabledUI() {
var rulesToUpdate = [
['inline', 'ruler', 'image'],
['inline', 'ruler', 'link'],
['inline', 'ruler', 'MISP_element_rule'],
]
rulesToUpdate.forEach(function(rulePath) {
var rule = getRuleStatus(rulePath[0], rulePath[1], rulePath[2])
if (rule.enabled) {
$('#markdownparsing-' + rule.name + '-parsing-enabled').show()
$('#markdownparsing-' + rule.name + '-parsing-disabled').hide()
} else {
$('#markdownparsing-' + rule.name + '-parsing-enabled').hide()
$('#markdownparsing-' + rule.name + '-parsing-disabled').show()
}
})
}
function toggleSaveButton(enabled) {
$saveMarkdownButton
.prop('disabled', !enabled)
}
function toggleLoadingInSaveButton(saving) {
toggleSaveButton(!saving)
if (saving) {
$saveMarkdownButton.append(loadingSpanAnimation);
toggleMarkdownEditorLoading(true, 'Saving report')
} else {
$saveMarkdownButton.find('#loadingSpan').remove();
toggleMarkdownEditorLoading(false)
}
}
function toggleMarkdownEditorLoading(loading, message) {
if (loading) {
$loadingBackdrop.show()
$loadingBackdrop.append(
$('<div/>').css({
'font-size': '20px',
'color': 'white'
}).append(
$(loadingSpanAnimation).css({
'margin-right': '0.5em'
}),
$('<span/>').text(message)
)
)
} else {
$loadingBackdrop.empty().hide()
}
}
function toggleFullscreenMode() {
var wholeContainer = $fullContainer[0]
if (!document.fullscreenElement) {
beforeFullscreen()
wholeContainer.requestFullscreen();
} else {
if (document.exitFullscreen) {
afterFullscreen()
document.exitFullscreen();
}
}
}
function beforeFullscreen() {
$editorContainer.css({ // reset dimension if resizeable helper was used
height: 'inherit',
width: '50%'
})
$editorContainer.resizable( "option", { maxWidth: window.innerWidth - 300 } );
}
function afterFullscreen() {
$editorContainer.resizable( "option", { maxWidth: window.innerWidth -220 - 300 } );
}
function checkIfFullScreenEnabled() {
if (!document.fullscreenEnabled) {
$toggleFullScreenMode.hide()
}
}
function invalidateContentCache() {
contentChanged = true
toggleSaveButton(true)
$lastModifiedField.addClass('label-important').text(changeDetectedMessage)
}
function revalidateContentCache() {
contentChanged = false
toggleSaveButton(false)
$lastModifiedField.removeClass('label-important')
}
function refreshLastUpdatedField() {
$lastModifiedField.text(moment(parseInt(lastModified)).fromNow())
}
function sanitizeObject(obj) {
var newObj = {}
for (var key of Object.keys(obj)) {
var newVal = $('</p>').text(obj[key]).html()
newObj[key] = newVal
}
return newObj
}
function hideAll() {
$rawContainer.hide()
$editorContainer.hide()
$viewerContainer.hide()
$resizableHandle.hide()
}
function setMode(mode) {
currentMode = mode
$mardownViewerToolbar.find('button').removeClass('btn-inverse')
$mardownViewerToolbar.find('button[data-togglemode="' + mode + '"]').addClass('btn-inverse')
hideAll()
$editorContainer.css('width', '');
if (mode == 'raw') {
$rawContainer.show()
}
if (mode == 'splitscreen') {
$resizableHandle.show()
$splitContainer.addClass('split-actif')
} else {
$resizableHandle.hide()
$splitContainer.removeClass('split-actif')
}
if (mode == 'viewer' || mode == 'splitscreen') {
$viewerContainer.show()
}
if (mode == 'editor' || mode == 'splitscreen') {
$editorContainer.show({
duration: 0,
complete: function() {
cm.refresh()
// Make sure to build the scrollmap after the rendering
setTimeout(function() {
scrollMap = buildScrollMap()
}, 500);
}
})
}
}
function getEditorData() {
return cm !== undefined ? cm.getValue() : originalRaw
}
function setEditorData(data) {
cm.setValue(data)
}
function saveMarkdown() {
if (modelNameForSave === undefined || markdownModelFieldNameForSave === undefined) {
console.log('Model or field not defined. Save not possible')
return
}
if (!confirm(saveConfirmMessage)) {
return
}
var url = baseurl + "/eventReports/edit/" + reportid
fetchFormDataAjax(url, function(formHTML) {
$('body').append($('<div id="temp" style="display: none"/>').html(formHTML))
var $tmpForm = $('#temp form')
var formUrl = $tmpForm.attr('action')
$tmpForm.find('[name="data[' + modelNameForSave + '][' + markdownModelFieldNameForSave + ']"]').val(getEditorData())
$.ajax({
data: $tmpForm.serialize(),
beforeSend: function() {
toggleLoadingInSaveButton(true)
$editor.prop('disabled', true);
},
success:function(report, textStatus) {
if (report) {
showMessage('success', report.message);
if (report.data !== undefined) {
lastModified = report.data.EventReport.timestamp + '000'
refreshLastUpdatedField()
originalRaw = report.data.EventReport.content
revalidateContentCache()
}
}
},
error: function(jqXHR, textStatus, errorThrown) {
showMessage('fail', saveFailedMessage + ': ' + errorThrown);
},
complete:function() {
$('#temp').remove();
toggleLoadingInSaveButton(false)
$editor.prop('disabled', false);
},
type:"post",
url: formUrl
})
})
}
function downloadMarkdown(type) {
var content, fileType, baseName, extension
if (type == 'pdf') {
if (currentMode != 'viewer' && currentMode != 'splitscreen') {
setMode('viewer')
setTimeout(function (){ // let the parser render the document
if (confirm(savePDFConfirmMessage)) {
window.print()
}
}, 300);
} else {
if (confirm(savePDFConfirmMessage)) {
window.print()
}
}
return
} else if (type == 'text') {
content = getEditorData()
baseName = 'event-report-' + (new Date()).getTime()
extension = 'md'
fileType = 'text/markdown'
} else if (type == 'text-gfm') {
content = getEditorData()
if (typeof markdownGFMSubstitution === 'function') {
content = markdownGFMSubstitution(content)
}
baseName = 'event-report-' + (new Date()).getTime()
extension = 'md'
fileType = 'text/markdown'
}
var filename = baseName + '.' + extension
var blob = new Blob([content], {
type: fileType
})
saveAs(blob, filename)
}
function showHelp() {
$('#genericModal.markdown-modal-helper').modal();
}
function renderMarkdown() {
var toRender = getEditorData()
var result = md.render(toRender)
scrollMap = null
$viewer.html(result)
postRenderingAction()
}
function doRender() {
if ($autoRenderMarkdownCB.prop('checked')) {
clearTimeout(renderTimer);
renderTimer = setTimeout(renderMarkdown, debounceDelay);
}
}
function registerListener() {
if (typeof markdownCustomPostRenderingListener === 'function') {
markdownCustomPostRenderingListener()
}
}
function postRenderingAction() {
registerListener()
if (typeof markdownCustomPostRenderingActions === 'function') {
markdownCustomPostRenderingActions()
}
}
function replacementAction(action) {
var customReplacementTriggered = false
if (typeof customReplacementActions === 'function') {
customReplacementTriggered = customReplacementActions(action)
}
if (!customReplacementTriggered) {
baseReplacementAction(action)
}
}
function baseReplacementAction(action) {
var start = cm.getCursor('start')
var end = cm.getCursor('end')
var content = cm.getRange(start, end)
var replacement = content
var setCursorTo = false
switch (action) {
case 'bold':
replacement = '**' + content + '**'
break;
case 'italic':
replacement = '*' + content + '*'
break;
case 'heading':
start.ch = 0
replacement = cm.getRange({line: start.line, ch: 0}, {line: start.line, ch: 1}) == '#' ? '#' : '# '
end = null
break;
case 'strikethrough':
replacement = '~~' + content + '~~'
break;
case 'list-ul':
start.ch = 0
var currentFirstChar = cm.getRange({line: start.line, ch: 0}, {line: start.line, ch: 2})
if (currentFirstChar == '* ') {
replacement = ''
end.ch = 2
} else {
replacement = '* '
end = null
}
break;
case 'list-ol':
start.ch = 0
var currentFirstChar = cm.getRange({line: start.line, ch: 0}, {line: start.line, ch: 3})
if (currentFirstChar == '1. ') {
replacement = ''
end.ch = 3
} else {
replacement = '1. '
end = null
}
break;
case 'quote':
start.ch = 0
var currentFirstChar = cm.getRange({line: start.line, ch: 0}, {line: start.line, ch: 2})
if (currentFirstChar == '> ') {
replacement = ''
end.ch = 2
} else {
replacement = '> '
end = null
}
break;
case 'code':
cm.replaceRange('\n```', {line: start.line - 1})
cm.replaceRange('\n```', {line: end.line + 1})
cm.setCursor(start.line + 1)
cm.focus()
return;
case 'table':
var tableTemplate = '| Column 1 | Column 2 | Column 3 |\n| -------- | -------- | -------- |\n| Text | Text | Text |\n'
var lineContent = cm.getLine(start.line)
if (lineContent != '') {
tableTemplate = '\n' + tableTemplate
}
cm.replaceRange(tableTemplate, {line: start.line + 1})
var startSelection = start.line + 1
if (lineContent != '') {
startSelection++
}
cm.setSelection({line: startSelection, ch: 2}, {line: startSelection, ch: 10})
cm.focus()
return;
default:
break;
}
cm.replaceRange(replacement, start, end)
if (setCursorTo !== false) {
cm.setCursor(setCursorTo.line, setCursorTo.ch)
}
cm.focus()
}
function insertTopToolbarSection() {
$topBar.append($('<i />').addClass('top-bar-separator'))
}
function insertTopToolbarButton(FAClass, replacement, title) {
title = title === undefined ? '' : title
$topBar.append(
$('<span />').addClass('useCursorPointer icon fa fa-' + FAClass)
.attr('title', title)
.click(function() {
replacementAction(replacement)
})
)
}
// function createRulesMenuItem(ruleName, text, icon) {
function createRulesMenuItem(itemName, icon, ruleScope, ruleName) {
var functionName = 'markdownItToggleRule'
var classPrefix = 'parsing'
if (ruleScope == 'render' && typeof markdownItToggleRenderingRule === 'function') {
functionName = 'markdownItToggleRenderingRule'
classPrefix = 'rendering'
}
var $MISPElementMenuItem = $markdownDropdownRulesMenu.find('li:first').clone()
$MISPElementMenuItem.find('a').attr('onclick', functionName + '(\'' + ruleName + '\', arguments[0]); return false;')
if (icon instanceof jQuery){
$MISPElementMenuItem.find('span.icon').empty().append(icon)
} else {
$MISPElementMenuItem.find('span.icon > i').attr('class', 'fas fa-'+icon)
}
$MISPElementMenuItem.find('span.ruleText').text(itemName)
$MISPElementMenuItem.find('span.bold.green').attr('id', 'markdown' + classPrefix + '-' + ruleName + '-' + classPrefix + '-enabled')
$MISPElementMenuItem.find('span.bold.red').attr('id', 'markdown' + classPrefix + '-' + ruleName + '-' + classPrefix + '-disabled')
return $MISPElementMenuItem
}
function createMenuItem(itemName, icon, clickHandler) {
return $('<li/>').append(
$('<a/>').attr('tabindex', '-1').attr('href', '#').click(clickHandler).append(
$('<span/>').addClass('icon').append(
icon instanceof jQuery ? icon : $('<i/>').addClass(icon)
),
$('<span/>').text(' ' + itemName)
)
)
}
function createSubMenu(submenuConfig) {
var $submenus = $('<ul/>').addClass('dropdown-menu')
submenuConfig.items.forEach(function(item) {
if (item.isToggleableRule) {
$submenus.append(createRulesMenuItem(item.name, item.icon, item.ruleScope, item.ruleName))
} else {
$submenus.append(createMenuItem(item.name, item.icon, item.clickHandler))
}
});
var $dropdownSubmenu = createMenuItem(submenuConfig.name, submenuConfig.icon).addClass('dropdown-submenu').append($submenus)
$markdownDropdownGeneralMenu.append($dropdownSubmenu)
}
function getRuleStatus(context, rulername, rulename) {
var rules = md[context][rulername].__rules__
for (var i = 0; i < rules.length; i++) {
var rule = rules[i];
if (rule.name == rulename) {
return rule
}
}
return false
}
function isInsideModal() {
return $(cm.getWrapperElement()).closest('.modal').length > 0
}
// Inject line numbers for sync scroll. Notes:
//
// - We track only headings and paragraphs on first level. That's enough.
// - Footnotes content causes jumps. Level limit filter it automatically.
function injectLineNumbers(tokens, idx, options, env, slf) {
var line;
if (tokens[idx].map && tokens[idx].level === 0) {
line = tokens[idx].map[0];
tokens[idx].attrJoin('class', 'line');
tokens[idx].attrSet('data-line', String(line+1));
}
return slf.renderToken(tokens, idx, options, env, slf);
}
// Build offsets for each line (lines can be wrapped)
// That's a bit dirty to process each line everytime, but ok for demo.
// Optimizations are required only for big texts.
// Source: https://github.com/markdown-it/markdown-it/blob/master/support/demo_template/index.js
function buildScrollMap() {
var i, offset, nonEmptyList, pos, a, b, lineHeightMap, linesCount,
acc, sourceLikeDiv, textarea = $(cm.getWrapperElement()),
_scrollMap;
sourceLikeDiv = $('<div />').css({
position: 'absolute',
visibility: 'hidden',
height: 'auto',
width: textarea[0].clientWidth,
'font-size': textarea.css('font-size'),
'font-family': textarea.css('font-family'),
'line-height': textarea.css('line-height'),
'white-space': textarea.css('white-space')
}).appendTo('body');
offset = $viewerContainer.scrollTop() - $viewerContainer.offset().top;
if (isInsideModal()) { // inside a modal
offset -= 20
}
_scrollMap = [];
nonEmptyList = [];
lineHeightMap = [];
acc = 0;
cm.eachLine(function(line) {
var h, lh;
lineHeightMap.push(acc)
if (line.text.length === 0) {
acc++
return
}
sourceLikeDiv.text(line.text);
h = parseFloat(sourceLikeDiv.css('height'));
lh = parseFloat(sourceLikeDiv.css('line-height'));
acc += Math.round(h / lh);
})
sourceLikeDiv.remove();
lineHeightMap.push(acc);
linesCount = acc;
for (i = 0; i < linesCount; i++) { _scrollMap.push(-1); }
nonEmptyList.push(0);
_scrollMap[0] = 0;
$viewerContainer.find('.line').each(function (n, el) {
var $el = $(el), t = $el.data('line');
if (t === '') { return; }
t = lineHeightMap[t];
if (t !== 0) { nonEmptyList.push(t); }
_scrollMap[t] = Math.round($el.offset().top + offset);
});
nonEmptyList.push(linesCount);
_scrollMap[linesCount] = $viewerContainer[0].scrollHeight;
pos = 0;
for (i = 1; i < linesCount; i++) {
if (_scrollMap[i] !== -1) {
pos++;
continue;
}
a = nonEmptyList[pos];
b = nonEmptyList[pos + 1];
_scrollMap[i] = Math.round((_scrollMap[b] * (i - a) + _scrollMap[a] * (b - i)) / (b - a));
}
return _scrollMap;
}
function doScroll(fun) {
if ($syncScrollCB.prop('checked')) {
clearTimeout(scrollTimer);
scrollTimer = setTimeout(fun, debounceDelay);
}
}
// Synchronize scroll position from source to result
var syncResultScroll = function () {
var lineNo = Math.ceil(cm.getScrollInfo().top/cm.defaultTextHeight());
if (!scrollMap) { scrollMap = buildScrollMap(); }
var posTo = scrollMap[lineNo];
$viewerContainer.stop(true).animate({
scrollTop: posTo
}, 100, 'linear');
}
// Synchronize scroll position from result to source
var syncSrcScroll = function () {
var resultHtml = $viewerContainer,
scrollTop = resultHtml.scrollTop(),
lines,
i,
line;
if (!scrollMap) { scrollMap = buildScrollMap(); }
lines = Object.keys(scrollMap);
if (lines.length < 1) {
return;
}
line = lines[0];
for (i = 1; i < lines.length; i++) {
if (scrollMap[lines[i]] < scrollTop) {
line = lines[i];
continue;
}
break;
}
cm.scrollTo(0, line*cm.defaultTextHeight())
}

View File

@ -153,6 +153,7 @@ function cancelPrompt(isolated) {
if (isolated == undefined) {
$("#gray_out").fadeOut();
}
$("#popover_form").fadeOut();
$("#confirmation_box").fadeOut();
$("#confirmation_box").empty();
$('.have-a-popover').popover('destroy');
@ -1299,6 +1300,11 @@ function submitPopoverForm(context_id, referer, update_context_id, modal, popove
$('#sightingsListAllToggle').removeClass('btn-inverse');
$('#sightingsListAllToggle').addClass('btn-primary');
}
if (referer == 'addEventReport' && typeof window.reloadEventReportTable === 'function') {
context == 'eventReport'
reloadEventReportTable()
eventUnpublish()
}
if (
(
context == 'event' &&
@ -1739,7 +1745,7 @@ function openPopover(clicked, data, hover, placement, callback) {
}
} else {
// $clicked.popover('show');
$clicked.popover('show');
}
var popover = $clicked.data('popover');
@ -5221,6 +5227,24 @@ function changeLocationFromIndexDblclick(row_index) {
window.location = href;
}
function openIdSelection(clicked, scope, action) {
var onclick = 'redirectIdSelection(\'' + scope + '\', \'' + action + '\')'
var html = '<div class="input-append">'
+ '<input class="span2" id="eventIdSelectionInput" type="number" min="1" step="1" placeholder="42">'
+ '<button class="btn btn-primary" type="button" onclick="' + onclick + '">Submit</button>'
+ '</div>';
openPopover(clicked, html, false, 'right')
}
function redirectIdSelection(scope, action) {
var id = $('#eventIdSelectionInput').val()
if (id.length > 0) {
window.location = baseurl + '/' + scope + '/' + action + '/' + id
} else {
showMessage('fail', 'Not an valid event id');
}
}
$('body').on('click', '.hex-value-convert', function() {
var $hexValueSpan = $(this).parent().children(':first-child');
var val = $hexValueSpan.text().trim();

View File

@ -1350,6 +1350,107 @@
"extra": ""
}
],
"event_reports": [
{
"column_name": "id",
"is_nullable": "NO",
"data_type": "int",
"character_maximum_length": null,
"numeric_precision": "10",
"collation_name": null,
"column_type": "int(11)",
"column_default": null,
"extra": "auto_increment"
},
{
"column_name": "uuid",
"is_nullable": "NO",
"data_type": "varchar",
"character_maximum_length": "40",
"numeric_precision": null,
"collation_name": "utf8_bin",
"column_type": "varchar(40)",
"column_default": null,
"extra": ""
},
{
"column_name": "event_id",
"is_nullable": "NO",
"data_type": "int",
"character_maximum_length": null,
"numeric_precision": "10",
"collation_name": null,
"column_type": "int(11)",
"column_default": null,
"extra": ""
},
{
"column_name": "name",
"is_nullable": "NO",
"data_type": "varchar",
"character_maximum_length": "255",
"numeric_precision": null,
"collation_name": "utf8mb4_general_ci",
"column_type": "varchar(255)",
"column_default": null,
"extra": ""
},
{
"column_name": "content",
"is_nullable": "YES",
"data_type": "text",
"character_maximum_length": "65535",
"numeric_precision": null,
"collation_name": "utf8mb4_general_ci",
"column_type": "text",
"column_default": null,
"extra": ""
},
{
"column_name": "distribution",
"is_nullable": "NO",
"data_type": "tinyint",
"character_maximum_length": null,
"numeric_precision": "3",
"collation_name": null,
"column_type": "tinyint(4)",
"column_default": "0",
"extra": ""
},
{
"column_name": "sharing_group_id",
"is_nullable": "YES",
"data_type": "int",
"character_maximum_length": null,
"numeric_precision": "10",
"collation_name": null,
"column_type": "int(11)",
"column_default": null,
"extra": ""
},
{
"column_name": "timestamp",
"is_nullable": "NO",
"data_type": "int",
"character_maximum_length": null,
"numeric_precision": "10",
"collation_name": null,
"column_type": "int(11)",
"column_default": null,
"extra": ""
},
{
"column_name": "deleted",
"is_nullable": "NO",
"data_type": "tinyint",
"character_maximum_length": null,
"numeric_precision": "3",
"collation_name": null,
"column_type": "tinyint(1)",
"column_default": "0",
"extra": ""
}
],
"event_tags": [
{
"column_name": "id",
@ -6867,6 +6968,13 @@
"user_id": false,
"timestamp": false
},
"event_reports": {
"id": true,
"uuid": true,
"name": false,
"event_id": false,
"sharing_group_id": false
},
"event_tags": {
"id": true,
"event_id": false,
@ -7167,5 +7275,5 @@
"id": true
}
},
"db_version": "58"
}
"db_version": "59"
}