diff --git a/app/Controller/EventReportsController.php b/app/Controller/EventReportsController.php index 0c79c39cb..8b6ddf1cb 100644 --- a/app/Controller/EventReportsController.php +++ b/app/Controller/EventReportsController.php @@ -209,31 +209,30 @@ class EventReportsController extends AppController { if (!$this->request->is('ajax')) { throw new MethodNotAllowedException(__('This function can only be reached via AJAX.')); - } else { - if ($this->request->is('post')) { - $report = $this->EventReport->fetchIfAuthorized($this->Auth->user(), $reportId, 'edit', $throwErrors=true, $full=false); - $results = $this->EventReport->getComplexTypeToolResultWithReplacements($this->Auth->user(), $report); - $report['EventReport']['content'] = $results['replacementResult']['contentWithReplacements']; - $contextResults = $this->EventReport->extractWithReplacements($this->Auth->user(), $report, ['replace' => true]); - $suggestionResult = $this->EventReport->transformFreeTextIntoSuggestion($contextResults['contentWithReplacements'], $results['complexTypeToolResult']); - $errors = $this->EventReport->applySuggestions($this->Auth->user(), $report, $suggestionResult['contentWithSuggestions'], $suggestionResult['suggestionsMapping']); - if (empty($errors)) { - if (!empty($this->data['EventReport']['tag_event'])) { - $this->EventReport->attachTagsAfterReplacements($this->Auth->User(), $contextResults['replacedContext'], $report['EventReport']['event_id']); - } - $report = $this->EventReport->simpleFetchById($this->Auth->user(), $reportId); - $data = [ 'report' => $report ]; - $successMessage = __('Automatic extraction applied to Event Report %s', $reportId); - return $this->__getSuccessResponseBasedOnContext($successMessage, $data, 'applySuggestions', $reportId); - } else { - $errorMessage = __('Automatic extraction could not be applied to Event Report %s.%sReasons: %s', $reportId, PHP_EOL, json_encode($errors)); - return $this->__getFailResponseBasedOnContext($errorMessage, array(), 'applySuggestions', $reportId); - } - } - $this->layout = 'ajax'; - $this->set('reportId', $reportId); - $this->render('ajax/extractAllFromReport'); } + if ($this->request->is('post')) { + $report = $this->EventReport->fetchIfAuthorized($this->Auth->user(), $reportId, 'edit', $throwErrors=true, $full=false); + $results = $this->EventReport->getComplexTypeToolResultWithReplacements($this->Auth->user(), $report); + $report['EventReport']['content'] = $results['replacementResult']['contentWithReplacements']; + $contextResults = $this->EventReport->extractWithReplacements($this->Auth->user(), $report, ['replace' => true]); + $suggestionResult = $this->EventReport->transformFreeTextIntoSuggestion($contextResults['contentWithReplacements'], $results['complexTypeToolResult']); + $errors = $this->EventReport->applySuggestions($this->Auth->user(), $report, $suggestionResult['contentWithSuggestions'], $suggestionResult['suggestionsMapping']); + if (empty($errors)) { + if (!empty($this->data['EventReport']['tag_event'])) { + $this->EventReport->attachTagsAfterReplacements($this->Auth->User(), $contextResults['replacedContext'], $report['EventReport']['event_id']); + } + $report = $this->EventReport->simpleFetchById($this->Auth->user(), $reportId); + $data = [ 'report' => $report ]; + $successMessage = __('Automatic extraction applied to Event Report %s', $reportId); + return $this->__getSuccessResponseBasedOnContext($successMessage, $data, 'applySuggestions', $reportId); + } else { + $errorMessage = __('Automatic extraction could not be applied to Event Report %s.%sReasons: %s', $reportId, PHP_EOL, json_encode($errors)); + return $this->__getFailResponseBasedOnContext($errorMessage, array(), 'applySuggestions', $reportId); + } + } + $this->layout = 'ajax'; + $this->set('reportId', $reportId); + $this->render('ajax/extractAllFromReport'); } public function extractFromReport($reportId) diff --git a/app/Controller/TagsController.php b/app/Controller/TagsController.php index e00dcf69a..31836287c 100644 --- a/app/Controller/TagsController.php +++ b/app/Controller/TagsController.php @@ -1116,10 +1116,13 @@ class TagsController extends AppController 'conditions' => $conditions, 'recursive' => -1 )); - if (!$searchIfTagExists && empty($tags)) { - $tags = []; - foreach ($tag as $i => $tagName) { - $tags[] = ['Tag' => ['name' => $tagName], 'simulatedTag' => true]; + if (!$searchIfTagExists) { + $foundTagNames = Hash::extract($tags, "{n}.Tag.name"); + foreach ($tag as $tagName) { + if (!in_array($tagName, $foundTagNames, true)) { + // Tag not found, insert simulated tag + $tags[] = ['Tag' => ['name' => $tagName], 'simulatedTag' => true]; + } } } $this->loadModel('Taxonomy'); diff --git a/app/Lib/Tools/ComplexTypeTool.php b/app/Lib/Tools/ComplexTypeTool.php index 65a73e2ff..8ee39acce 100644 --- a/app/Lib/Tools/ComplexTypeTool.php +++ b/app/Lib/Tools/ComplexTypeTool.php @@ -234,6 +234,7 @@ class ComplexTypeTool if (isset($resultArray[$typeArray['value']])) { continue; } + $typeArray['original_value'] = $ioc; $resultArray[$typeArray['value']] = $typeArray; } return array_values($resultArray); diff --git a/app/Model/EventReport.php b/app/Model/EventReport.php index 30a93c438..bdc6b09ca 100644 --- a/app/Model/EventReport.php +++ b/app/Model/EventReport.php @@ -521,7 +521,8 @@ class EventReport extends AppModel return $errors; } - public function applySuggestions($user, $report, $contentWithSuggestions, $suggestionsMapping) { + public function applySuggestions(array $user, array $report, $contentWithSuggestions, array $suggestionsMapping) + { $errors = []; $replacedContent = $contentWithSuggestions; $success = 0; @@ -546,10 +547,10 @@ class EventReport extends AppModel return $errors; } - public function applySuggestionsInText($contentWithSuggestions, $attribute, $value) + public function applySuggestionsInText($contentWithSuggestions, array $attribute, $value) { - $textToBeReplaced = sprintf('@[suggestion](%s)', $value); - $textToInject = sprintf('@[attribute](%s)', $attribute['Attribute']['uuid']); + $textToBeReplaced = "@[suggestion]($value)"; + $textToInject = "@[attribute]({$attribute['Attribute']['uuid']})"; $replacedContent = str_replace($textToBeReplaced, $textToInject, $contentWithSuggestions); return $replacedContent; } @@ -642,25 +643,36 @@ class EventReport extends AppModel ]; } - public function transformFreeTextIntoSuggestion($content, $complexTypeToolResult) + public function transformFreeTextIntoSuggestion($content, array $complexTypeToolResult) { $replacedContent = $content; - $suggestionsMapping = []; $typeToCategoryMapping = $this->Event->Attribute->typeToCategoryMapping(); - foreach ($complexTypeToolResult as $i => $complexTypeToolEntry) { + + // Sort by original value string length, longest values first + usort($complexTypeToolResult, function ($a, $b) { + $strlenA = strlen($a['original_value']); + $strlenB = strlen($b['original_value']); + if ($strlenA === $strlenB) { + return 0; + } + return ($strlenA < $strlenB) ? 1 : -1; + }); + + $suggestionsMapping = []; + foreach ($complexTypeToolResult as $complexTypeToolEntry) { $textToBeReplaced = $complexTypeToolEntry['value']; - $textToInject = sprintf('@[suggestion](%s)', $textToBeReplaced); + $textToInject = "@[suggestion]($textToBeReplaced)"; $suggestionsMapping[$textToBeReplaced] = [ 'category' => $typeToCategoryMapping[$complexTypeToolEntry['default_type']][0], 'type' => $complexTypeToolEntry['default_type'], 'value' => $textToBeReplaced, 'to_ids' => $complexTypeToolEntry['to_ids'], ]; - $replacedContent = str_replace($textToBeReplaced, $textToInject, $replacedContent); + $replacedContent = str_replace($complexTypeToolEntry['original_value'], $textToInject, $replacedContent); } return [ 'contentWithSuggestions' => $replacedContent, - 'suggestionsMapping' => $suggestionsMapping + 'suggestionsMapping' => $suggestionsMapping, ]; } @@ -674,21 +686,17 @@ class EventReport extends AppModel return $complexTypeToolResult; } - public function getComplexTypeToolResultFromReport($content) + public function getComplexTypeToolResultWithReplacements(array $user, array $report) { App::uses('ComplexTypeTool', 'Tools'); $complexTypeTool = new ComplexTypeTool(); $this->Warninglist = ClassRegistry::init('Warninglist'); $complexTypeTool->setTLDs($this->Warninglist->fetchTLDLists()); - $complexTypeToolResult = $complexTypeTool->checkComplexRouter($content, 'freetext'); - return $complexTypeToolResult; - } - public function getComplexTypeToolResultWithReplacements($user, $report) - { - $complexTypeToolResult = $this->getComplexTypeToolResultFromReport($report['EventReport']['content']); + $complexTypeToolResult = $complexTypeTool->checkFreeText($report['EventReport']['content']); $replacementResult = $this->transformFreeTextIntoReplacement($user, $report, $complexTypeToolResult); - $complexTypeToolResult = $this->getComplexTypeToolResultFromReport($replacementResult['contentWithReplacements']); + $complexTypeToolResult = $complexTypeTool->checkFreeText($replacementResult['contentWithReplacements']); + return [ 'complexTypeToolResult' => $complexTypeToolResult, 'replacementResult' => $replacementResult, @@ -700,6 +708,7 @@ class EventReport extends AppModel * * @param array $user * @param array $report + * @param array $options * @return array */ public function extractWithReplacements(array $user, array $report, array $options = []) @@ -713,7 +722,6 @@ class EventReport extends AppModel 'attack' => true, ]; $options = array_merge($baseOptions, $options); - $originalContent = $report['EventReport']['content']; $this->GalaxyCluster = ClassRegistry::init('GalaxyCluster'); $mitreAttackGalaxyId = $this->GalaxyCluster->Galaxy->getMitreAttackGalaxyId(); $clusterContain = ['Tag']; @@ -734,17 +742,21 @@ class EventReport extends AppModel 'contain' => $clusterContain ]); + $originalContent = $report['EventReport']['content']; + // Remove all existing event report markers + $content = preg_replace("/@\[(attribute|tag|galaxymatrix)]\([^)]*\)/", '', $originalContent); + if ($options['tags']) { $this->Tag = ClassRegistry::init('Tag'); $tags = $this->Tag->fetchUsableTags($user); - foreach ($tags as $i => $tag) { + foreach ($tags as $tag) { $tagName = $tag['Tag']['name']; - $found = $this->isValidReplacementTag($originalContent, $tagName); + $found = $this->isValidReplacementTag($content, $tagName); if ($found) { $replacedContext[$tagName][$tagName] = $tag['Tag']; } else { $tagNameUpper = strtoupper($tagName); - $found = $this->isValidReplacementTag($originalContent, $tagNameUpper); + $found = $this->isValidReplacementTag($content, $tagNameUpper); if ($found) { $replacedContext[$tagNameUpper][$tagName] = $tag['Tag']; } @@ -752,10 +764,10 @@ class EventReport extends AppModel } } - foreach ($clusters as $i => $cluster) { + foreach ($clusters as $cluster) { $cluster['GalaxyCluster']['colour'] = '#0088cc'; $tagName = $cluster['GalaxyCluster']['tag_name']; - $found = $this->isValidReplacementTag($originalContent, $tagName); + $found = $this->isValidReplacementTag($content, $tagName); if ($found) { $replacedContext[$tagName][$tagName] = $cluster['GalaxyCluster']; } @@ -765,10 +777,10 @@ class EventReport extends AppModel $replacedContext[$cluster['GalaxyCluster']['value']][$tagName] = $cluster['GalaxyCluster']; } if ($options['synonyms']) { - foreach ($cluster['GalaxyElement'] as $j => $element) { + foreach ($cluster['GalaxyElement'] as $element) { if (strlen($element['value']) >= $options['synonyms_min_characters']) { $toSearch = ' ' . $element['value'] . ' '; - $found = strpos($originalContent, $toSearch) !== false; + $found = strpos($content, $toSearch) !== false; if ($found) { $replacedContext[$element['value']][$tagName] = $cluster['GalaxyCluster']; } @@ -783,22 +795,22 @@ class EventReport extends AppModel 'conditions' => ['GalaxyCluster.galaxy_id' => $mitreAttackGalaxyId], 'contain' => $clusterContain ]); - foreach ($attackClusters as $i => $cluster) { + foreach ($attackClusters as $cluster) { $cluster['GalaxyCluster']['colour'] = '#0088cc'; $tagName = $cluster['GalaxyCluster']['tag_name']; $toSearch = ' ' . $cluster['GalaxyCluster']['value'] . ' '; - $found = strpos($originalContent, $toSearch) !== false; + $found = strpos($content, $toSearch) !== false; if ($found) { $replacedContext[$cluster['GalaxyCluster']['value']][$tagName] = $cluster['GalaxyCluster']; } else { $clusterParts = explode(' - ', $cluster['GalaxyCluster']['value'], 2); $toSearch = ' ' . $clusterParts[0] . ' '; - $found = strpos($originalContent, $toSearch) !== false; + $found = strpos($content, $toSearch) !== false; if ($found) { $replacedContext[$clusterParts[0]][$tagName] = $cluster['GalaxyCluster']; - } else { + } else if (isset($clusterParts[1])) { $toSearch = ' ' . $clusterParts[1] . ' '; - $found = strpos($originalContent, $toSearch) !== false; + $found = strpos($content, $toSearch) !== false; if ($found) { $replacedContext[$clusterParts[1]][$tagName] = $cluster['GalaxyCluster']; } @@ -810,14 +822,32 @@ class EventReport extends AppModel 'replacedContext' => $replacedContext ]; if ($options['replace']) { + // Sort by original value string length, longest values first + uksort($replacedContext, function ($a, $b) { + $strlenA = strlen($a); + $strlenB = strlen($b); + if ($strlenA === $strlenB) { + return 0; + } + return ($strlenA < $strlenB) ? 1 : -1; + }); + $content = $originalContent; + $secondPassReplace = []; + // Replace in two pass to prevent double replace + $id = 0; foreach ($replacedContext as $rawText => $replacements) { // Replace with first one until a better strategy is found reset($replacements); $replacement = key($replacements); - $textToInject = sprintf('@[tag](%s)', $replacement); - $content = str_replace($rawText, $textToInject, $content); + ++$id; + $content = str_replace($rawText, "@[mark]($id)", $content); + $secondPassReplace[$id] = "@[tag]($replacement)"; } + + $content = preg_replace_callback("/@\[mark]\(([^)]*)\)/", function ($matches) use ($secondPassReplace) { + return $secondPassReplace[$matches[1]]; + }, $content); $toReturn['contentWithReplacements'] = $content; } return $toReturn; @@ -835,7 +865,6 @@ class EventReport extends AppModel 'event_id' => $event_id, 'url' => $url ]; - $module = $this->isFetchURLModuleEnabled(); if (!empty($module)) { $result = $this->Module->queryModuleServer($modulePayload, false); if (empty($result['results'][0]['values'][0])) { @@ -853,7 +882,7 @@ class EventReport extends AppModel } /** - * findValidReplacementTag Search if tagName is in content and is not wrapped in a tag reference + * findValidReplacementTag Search if tagName is in content * * @param string $content * @param string $tagName @@ -861,25 +890,8 @@ class EventReport extends AppModel */ private function isValidReplacementTag($content, $tagName) { - $lastIndex = 0; - $allIndices = []; $toSearch = strpos($tagName, ':') === false ? ' ' . $tagName . ' ' : $tagName; - while (($lastIndex = strpos($content, $toSearch, $lastIndex)) !== false) { - $allIndices[] = $lastIndex; - $lastIndex = $lastIndex + strlen($toSearch); - } - if (empty($allIndices)) { - return false; - } else { - $wrapper = '@[tag]('; - foreach ($allIndices as $i => $index) { - $stringBeforeTag = substr($content, $index - strlen($wrapper), strlen($wrapper)); - if ($stringBeforeTag != $wrapper) { - return true; - } - } - return false; - } + return strpos($content, $toSearch) !== false; } public function attachTagsAfterReplacements($user, $replacedContext, $eventId) diff --git a/app/webroot/js/markdownEditor/event-report.js b/app/webroot/js/markdownEditor/event-report.js index 9425bdb1d..4c8a5fb37 100644 --- a/app/webroot/js/markdownEditor/event-report.js +++ b/app/webroot/js/markdownEditor/event-report.js @@ -509,7 +509,7 @@ function renderMISPElement(scope, elementID, indexes) { type: suggestion.complexTypeToolResult.picked_type, origValue: elementID, value: suggestion.complexTypeToolResult.value, - 'indexStart': indexes.start, + indexStart: indexes.start, suggestionkey: suggestionKey, checked: suggestion.checked }) @@ -651,23 +651,34 @@ function attachRemoteMISPElements() { $div.html(cache_matrix[cacheKey]) } }) + + var tagNamesToLoad = []; + var tagsLoading = []; $('.embeddedTag[data-scope="tag"]').each(function() { - var $div = $(this) - $div.append($('').append(loadingSpanAnimation)) - var eventID = $div.data('eventid') - var elementID = $div.data('elementid') - var cacheKey = eventid + '-' + elementID - clearTimeout(tagTimers[cacheKey]); - if (cache_tag[cacheKey] === undefined) { - tagTimers[cacheKey] = setTimeout(function() { - fetchTagInfo(eventID, elementID, function() { - attachTagInfo($div, eventID, elementID, true) - }) - }, firstCustomPostRenderCall ? 0 : slowDebounceDelay); + var $div = $(this); + var elementID = $div.data('elementid'); + if (!(elementID in cache_tag)) { + $div.append($('').append(loadingSpanAnimation)); + tagNamesToLoad.push(elementID); + tagsLoading.push($div); } else { - $div.html(cache_tag[cacheKey]) + $div.html(cache_tag[elementID]); } - }) + }).promise().done(function() { + if (tagNamesToLoad.length === 0) { + return; + } + fetchTagInfo(tagNamesToLoad, function() { + $.each(tagsLoading, function() { + var $div = $(this); + var elementID = $div.data('elementid'); + if (elementID in cache_tag) { + $div.html(cache_tag[elementID]); + } + }); + }); + }); + if (firstCustomPostRenderCall) { // Wait, because .each calls are asynchronous setTimeout(function() { @@ -720,56 +731,53 @@ function attachGalaxyMatrix($elem, eventid, elementID) { }) } -function attachTagInfo($elem, eventid, elementID, all) { - var cacheKey = eventid + '-' + elementID - $elem.html(cache_tag[cacheKey]) - if (all === true) { - $('.embeddedTag[data-scope="tag"]').filter(function() { - return $(this).data('eventid') == eventid && $(this).data('elementid') == elementID - }).html(cache_tag[cacheKey]) - } -} - -function fetchTagInfo(eventid, tagName, callback) { +function fetchTagInfo(tagNames, callback) { $.ajax({ data: { - "tag": tagName, + "tag": tagNames, }, - success:function(data, textStatus) { - var $tag + success: function (data) { + var $tag, tagName; data = $.parseJSON(data) - var tagData; for (var i = 0; i < data.length; i++) { var tag = data[i]; - if (tag.Tag.name == tagName) { - tagData = data[i] - break + tagName = tag.Tag.name; + + proxyMISPElements['tag'][tagName] = tag; + + $tag = getTagReprensentation(tag); + cache_tag[tagName] = $tag[0].outerHTML; + } + + // If tag name doesn't exists, construct empty placeholder + for (i = 0; i < tagNames.length; i++) { + tagName = tagNames[i]; + if (!(tagName in cache_tag)) { + $tag = constructTagHtml(tagName, '#ffffff', {'border': '1px solid #000'}); + cache_tag[tagName] = $tag[0].outerHTML; } } - if (tagData === undefined) { - tagData = {} - $tag = constructTagHtml(tagName, '#ffffff', {'border': '1px solid #000'}) - } else { - $tag = getTagReprensentation(tagData) - proxyMISPElements['tag'][tagName] = tagData + }, + error: function (jqXHR, textStatus, errorThrown) { + // Query failed, fill cache with placeholder + var tagName, templateVariables; + for (var i = 0; i < tagNames.length; i++) { + tagName = tagNames[i]; + if (!(tagName in cache_tag)) { + templateVariables = sanitizeObject({ + scope: 'Error while fetching tag', + id: tagName + }); + cache_tag[tagName] = dotTemplateInvalid(templateVariables); + } } - var cacheKey = eventid + '-' + tagName - cache_tag[cacheKey] = $tag[0].outerHTML; }, - error: function(jqXHR, textStatus, errorThrown) { - var templateVariables = sanitizeObject({ - scope: 'Error while fetching tag', - id: tagName - }) - var placeholder = dotTemplateInvalid(templateVariables) - cache_tag[cacheKey] = placeholder; - }, - complete: function() { + complete: function () { if (callback !== undefined) { callback() } }, - type:"post", + type: "post", url: baseurl + "/tags/search/0/1/0" }) } @@ -947,7 +955,14 @@ function highlightPickedReplacementInReport() { function convertEntityIntoSuggestion(content, entity) { var converted = '' - var entityValue = entity.importRegexMatch ? entity.importRegexMatch : entity.value + var entityValue; + if (entity.importRegexMatch) { + entityValue = entity.importRegexMatch; + } else if (entity.original_value) { + entityValue = entity.original_value; + } else { + entityValue = entity.value; + } var splittedContent = content.split(entityValue) splittedContent.forEach(function(text, i) { converted += text @@ -1007,7 +1022,7 @@ function constructSuggestionMapping(entity, indicesInCM) { function injectNumberOfOccurrencesInReport(entities) { var content = getEditorData() entities.forEach(function(entity, i) { - entities[i].occurrences = getAllIndicesOf(content, entity.value, false, false).length + entities[i].occurrences = getAllIndicesOf(content, entity.original_value, false, false).length }) return entities } @@ -1089,7 +1104,7 @@ function pickSuggestionColumn(index, tableID, force) { tr: $tr, index: index } - if (tableID == 'replacementTable') { + if (tableID === 'replacementTable') { var uuid = $tr.find('select.attribute-replacement').val() pickedSuggestion['entity'] = { value: $tr.data('attributeValue'), @@ -1100,7 +1115,7 @@ function pickSuggestionColumn(index, tableID, force) { pickedSuggestion['entity']['importRegexMatch'] = proxyMISPElements['attribute'][uuid].importRegexValue } highlightPickedReplacementInReport() - } else if (tableID == 'contextReplacementTable') { + } else if (tableID === 'contextReplacementTable') { pickedSuggestion['entity'] = { value: $tr.data('contextValue'), picked_type: 'tag', @@ -1211,7 +1226,11 @@ function submitExtractionSuggestion() { } }, error: function(jqXHR, textStatus, errorThrown) { - showMessage('fail', saveFailedMessage + ': ' + errorThrown); + if (jqXHR.responseJSON) { + showMessage('fail', jqXHR.responseJSON.errors); + } else { + showMessage('fail', saveFailedMessage + ': ' + errorThrown); + } }, complete:function() { $('#temp').remove(); @@ -1412,10 +1431,11 @@ function constructTag(tagName) { function getTagReprensentation(tagData) { var $tag - if(tagData.GalaxyCluster !== undefined) { + if (tagData.GalaxyCluster !== undefined) { $tag = constructClusterTagHtml(tagData) } else { - $tag = constructTagHtml(tagData.Tag.name, tagData.Tag.colour) + var color = tagData.Tag.colour ? tagData.Tag.colour : tagData.TaxonomyPredicate.colour; + $tag = constructTagHtml(tagData.Tag.name, color) } return $tag } @@ -1480,8 +1500,7 @@ function constructTaxonomyInfo(tagData) { } function constructGalaxyInfo(tagData) { - var cacheKey = eventid + '-' + tagData.Tag.name - var tagHTML = cache_tag[cacheKey] + var tagHTML = cache_tag[tagData.Tag.name] var $tag = $(tagHTML) var $cluster = $('
').append( $('').append($tag), @@ -2011,16 +2030,14 @@ function isDoubleExtraction(content) { function getAllIndicesOf(haystack, needle, caseSensitive, requestLineNum) { var indices = [] - if (needle.length == 0) { + if (needle.length === 0) { return indices } - var startIndex = 0, index, indices = []; + var startIndex = 0, index = 0; if (!caseSensitive) { needle = needle.toLowerCase(); haystack = haystack.toLowerCase(); } - var startIndex = 0 - var index = 0 while (true) { index = haystack.indexOf(needle, startIndex) if (index === -1) { diff --git a/app/webroot/js/markdownEditor/markdownEditor.js b/app/webroot/js/markdownEditor/markdownEditor.js index 6562bf94e..68d9ee5dc 100644 --- a/app/webroot/js/markdownEditor/markdownEditor.js +++ b/app/webroot/js/markdownEditor/markdownEditor.js @@ -633,14 +633,16 @@ function createRulesMenuItem(itemName, icon, ruleScope, ruleName) { function createMenuItem(itemName, icon, clickHandler) { return $('
  • ').append( - $('').attr('tabindex', '-1').attr('href', '#').click(clickHandler).append( + $('').attr('tabindex', '-1').attr('href', '#').click(function (event) { + event.preventDefault(); + clickHandler(); + }).append( $('').addClass('icon').append( icon instanceof jQuery ? icon : $('').addClass(icon) ), $('').text(' ' + itemName) ) - ) - + ) } function createSubMenu(submenuConfig) { @@ -805,4 +807,4 @@ var syncSrcScroll = function () { break; } cm.scrollTo(0, line*cm.defaultTextHeight()) -} \ No newline at end of file +}