From 5253ada68037e70c4083f031478ebf8342e3c952 Mon Sep 17 00:00:00 2001 From: Jakub Onderka Date: Sat, 19 Mar 2022 13:58:19 +0100 Subject: [PATCH 001/120] chg: [sign] Simplified key handling --- app/Lib/Tools/ServerSyncTool.php | 9 +++- app/Model/CryptographicKey.php | 75 +++++++++++++++++++++----------- 2 files changed, 56 insertions(+), 28 deletions(-) diff --git a/app/Lib/Tools/ServerSyncTool.php b/app/Lib/Tools/ServerSyncTool.php index ffeca4e7b..cff26a9f4 100644 --- a/app/Lib/Tools/ServerSyncTool.php +++ b/app/Lib/Tools/ServerSyncTool.php @@ -20,6 +20,9 @@ class ServerSyncTool /** @var HttpSocketExtended */ private $socket; + /** @var CryptographicKey */ + private $cryptographicKey; + /** @var array|null */ private $info; @@ -418,8 +421,10 @@ class ServerSyncTool throw new Exception(__('Remote instance is not protected event aware yet (< 2.4.156), aborting.')); } - $this->CryptographicKey = ClassRegistry::init('CryptographicKey'); - $signature = $this->CryptographicKey->signWithInstanceKey($data); + if (!$this->cryptographicKey) { + $this->cryptographicKey = ClassRegistry::init('CryptographicKey'); + } + $signature = $this->cryptographicKey->signWithInstanceKey($data); if (empty($signature)) { throw new Exception(__("Invalid signing key. This should never happen.")); } diff --git a/app/Model/CryptographicKey.php b/app/Model/CryptographicKey.php index 2619f0c53..9d7f9aec9 100644 --- a/app/Model/CryptographicKey.php +++ b/app/Model/CryptographicKey.php @@ -35,6 +35,9 @@ class CryptographicKey extends AppModel public $validate = []; + /** @var CryptGpgExtended|null */ + private $gpg; + public function __construct($id = false, $table = null, $ds = null) { parent::__construct($id, $table, $ds); @@ -44,6 +47,12 @@ class CryptographicKey extends AppModel $this->gpg = null; } $this->validate = [ + 'uuid' => [ + 'uuid' => [ + 'rule' => 'uuid', + 'message' => 'Please provide a valid RFC 4122 UUID', + ], + ], 'type' => [ 'rule' => ['inList', $this->validTypes], 'message' => __('Invalid key type'), @@ -76,16 +85,18 @@ class CryptographicKey extends AppModel $this->data['CryptographicKey']['uuid'] = CakeText::uuid(); $this->data['CryptographicKey']['fingerprint'] = $this->extractKeyData($this->data['CryptographicKey']['type'], $this->data['CryptographicKey']['key_data']); } - $existingKeyForObject = $this->find('first', [ - 'recursive' - ]); return true; } + /** + * @return string + * @throws Crypt_GPG_BadPassphraseException + * @throws Crypt_GPG_Exception + */ public function ingestInstanceKey() { try { - $redis = $this->setupRedis(); + $redis = $this->setupRedisWithException(); } catch (Exception $e) { $redis = false; } @@ -123,6 +134,13 @@ class CryptographicKey extends AppModel return $fingerprint; } + /** + * @param string $data + * @return false|string + * @throws Crypt_GPG_BadPassphraseException + * @throws Crypt_GPG_Exception + * @throws Crypt_GPG_KeyNotFoundException + */ public function signWithInstanceKey($data) { if (!$this->ingestInstanceKey()) { @@ -142,6 +160,12 @@ class CryptographicKey extends AppModel return $signature; } + /** + * @param string $data + * @param string $signature + * @param string $key + * @return bool + */ public function verifySignature($data, $signature, $key) { $this->error = false; @@ -217,20 +241,15 @@ class CryptographicKey extends AppModel public function uniqueKeyForElement($data) { - $existingKey = $this->find('first', [ - 'recursive' => -1, - 'conditions' => [ - 'parent_type' => $this->data['CryptographicKey']['parent_type'], - 'parent_id' => $this->data['CryptographicKey']['parent_id'], - 'key_data' => $this->data['CryptographicKey']['key_data'], - 'type' => $this->data['CryptographicKey']['type'] - ], - 'fields' => ['id'] + return !$this->hasAny([ + 'parent_type' => $this->data['CryptographicKey']['parent_type'], + 'parent_id' => $this->data['CryptographicKey']['parent_id'], + 'key_data' => $this->data['CryptographicKey']['key_data'], + 'type' => $this->data['CryptographicKey']['type'], ]); - return empty($existingKey); } - public function validateProtectedEvent($raw_data, $user, $pgp_signature, $event) + public function validateProtectedEvent($raw_data, array $user, $pgp_signature, array $event) { $eventCryptoGraphicKey = []; if (!empty($event['Event']['CryptographicKey'])) { // Depending if $event comes from fetchEvent or from pushed data @@ -240,8 +259,7 @@ class CryptographicKey extends AppModel } if (empty($eventCryptoGraphicKey)) { $message = __('No valid signatures found for validating the signature.'); - $this->Log = ClassRegistry::init('Log'); - $this->Log->createLogEntry($user, 'validateSig', 'Event', $event['Event']['id'], $message); + $this->loadLog()->createLogEntry($user, 'validateSig', 'Event', $event['Event']['id'], $message); return false; } foreach ($eventCryptoGraphicKey as $supplied_key) { @@ -249,19 +267,26 @@ class CryptographicKey extends AppModel return true; } } - $this->Log = ClassRegistry::init('Log'); $message = __('Could not validate the signature.'); - $this->Log->createLogEntry($user, 'validateSig', 'Event', $event['Event']['id'], $message); + $this->loadLog()->createLogEntry($user, 'validateSig', 'Event', $event['Event']['id'], $message); return false; } - public function captureCryptographicKeyUpdate($user, $cryptographicKeys, $parent_id, $type) + /** + * @param array $user + * @param array $cryptographicKeys + * @param int $parent_id + * @param string $type + * @return void + * @throws Exception + */ + public function captureCryptographicKeyUpdate(array $user, array $cryptographicKeys, $parent_id, $type) { $existingKeys = $this->find('first', [ 'recursive' => -1, 'conditions' => [ 'parent_type' => $type, - 'parent_id' => $parent_id + 'parent_id' => $parent_id, ], 'fields' => [ 'id', @@ -269,15 +294,14 @@ class CryptographicKey extends AppModel 'parent_type', 'parent_id', 'revoked', - 'fingerprint' + 'fingerprint', ] ]); $toRemove = []; $results = ['add' => [], 'remove' => []]; - foreach ($existingKeys as $k => $existingKey) { + foreach ($existingKeys as $existingKey) { foreach ($cryptographicKeys as $k2 => $cryptographicKey) { if ($existingKey['fingerprint'] === $cryptographicKey['fingerprint']) { - $found = true; if ($cryptographicKey['revoked'] && !$existingKey['CryptographicKey']['revoked']) { $existingKey['CryptographicKey']['revoked'] = 1; $this->save($existingKey['CryptographicKey']); @@ -314,7 +338,6 @@ class CryptographicKey extends AppModel $parent_id ); $this->deleteAll(['CryptographicKey.id' => $toRemove]); - $this->Log = ClassRegistry::init('Log'); - $this->Log->createLogEntry($user, 'updateCryptoKeys', $cryptographicKey['parent_type'], $cryptographicKey['parent_id'], $message); + $this->loadLog()->createLogEntry($user, 'updateCryptoKeys', $cryptographicKey['parent_type'], $cryptographicKey['parent_id'], $message); } } From 6e75c3b5603703559108af618f1195070aea67d7 Mon Sep 17 00:00:00 2001 From: Jakub Onderka Date: Sat, 19 Mar 2022 14:58:35 +0100 Subject: [PATCH 002/120] chg: [gpgTool] Simplify code --- app/Lib/Tools/GpgTool.php | 71 ++++++++++++++++++--------------------- 1 file changed, 33 insertions(+), 38 deletions(-) diff --git a/app/Lib/Tools/GpgTool.php b/app/Lib/Tools/GpgTool.php index 82af45273..689bc1ba6 100644 --- a/app/Lib/Tools/GpgTool.php +++ b/app/Lib/Tools/GpgTool.php @@ -1,6 +1,9 @@ $homedir, 'gpgconf' => Configure::read('GnuPG.gpgconf'), 'binary' => Configure::read('GnuPG.binary') ?: '/usr/bin/gpg', - ); + ]; return new CryptGpgExtended($options); } - /** @var CryptGpgExtended */ - private $gpg; - - public function __construct($gpg) + public function __construct(CryptGpgExtended $gpg = null) { $this->gpg = $gpg; } @@ -47,11 +47,13 @@ class GpgTool public function searchGpgKey($search) { $uri = 'https://openpgp.circl.lu/pks/lookup?search=' . urlencode($search) . '&op=index&fingerprint=on&options=mr'; - $response = $this->keyServerLookup($uri); - if ($response->code == 404) { - return array(); // no keys found - } else if ($response->code != 200) { - throw new Exception("Fetching the '$uri' failed with HTTP error {$response->code}: {$response->reasonPhrase}"); + try { + $response = $this->keyServerLookup($uri); + } catch (HttpSocketHttpException $e) { + if ($e->getCode() === 404) { + return []; + } + throw $e; } return $this->extractKeySearch($response->body); } @@ -64,11 +66,13 @@ class GpgTool public function fetchGpgKey($fingerprint) { $uri = 'https://openpgp.circl.lu/pks/lookup?search=0x' . urlencode($fingerprint) . '&op=get&options=mr'; - $response = $this->keyServerLookup($uri); - if ($response->code == 404) { - return null; // key with given fingerprint not found - } else if ($response->code != 200) { - throw new Exception("Fetching the '$uri' failed with HTTP error {$response->code}: {$response->reasonPhrase}"); + try { + $response = $this->keyServerLookup($uri); + } catch (HttpSocketHttpException $e) { + if ($e->getCode() === 404) { + return null; + } + throw $e; } $key = $response->body; @@ -169,30 +173,20 @@ class GpgTool $advancedUrl = "https://openpgpkey.$domain/.well-known/openpgpkey/" . strtolower($domain) . "/hu/$localPartHash"; try { $response = $this->keyServerLookup($advancedUrl); - return $this->processWkdResponse($response); + return $this->gpg->enarmor($response->body()); } catch (Exception $e) { // pass, continue to direct method } $directUrl = "https://$domain/.well-known/openpgpkey/hu/$localPartHash"; - $response = $this->keyServerLookup($directUrl); - return $this->processWkdResponse($response); - } - - /** - * @param HttpSocketResponse $response - * @return string - * @throws Crypt_GPG_Exception - * @throws Crypt_GPG_InvalidOperationException - */ - private function processWkdResponse(HttpSocketResponse $response) - { - if ($response->code == 404) { - throw new NotFoundException("Key not found"); - } else if (!$response->isOk()) { - throw new Exception("Fetching the WKD failed with HTTP error {$response->code}: {$response->reasonPhrase}"); + try { + $response = $this->keyServerLookup($directUrl); + } catch (HttpSocketHttpException $e) { + if ($e->getCode() === 404) { + throw new NotFoundException("Key not found"); + } + throw $e; } - return $this->gpg->enarmor($response->body()); } @@ -232,17 +226,18 @@ class GpgTool /** * @param string $uri - * @return HttpSocketResponse + * @return HttpSocketResponseExtended + * @throws HttpSocketHttpException * @throws Exception */ private function keyServerLookup($uri) { App::uses('SyncTool', 'Tools'); $syncTool = new SyncTool(); - $HttpSocket = $syncTool->setupHttpSocket(); + $HttpSocket = $syncTool->createHttpSocket(['compress' => true]); $response = $HttpSocket->get($uri); - if ($response === false) { - throw new Exception("Could not fetch '$uri'."); + if (!$response->isOk()) { + throw new HttpSocketHttpException($response, $uri); } return $response; } From b21ad57b477157d707dbb5d1b96fed03c8be1842 Mon Sep 17 00:00:00 2001 From: Jakub Onderka Date: Sun, 20 Mar 2022 17:48:26 +0100 Subject: [PATCH 003/120] new: [test] GpgToolTest --- .github/workflows/main.yml | 19 ++------- app/Lib/Tools/CryptGpgExtended.php | 17 ++++++++ app/Lib/Tools/TmpFileTool.php | 21 ++++++---- app/Test/CryptGpgExtendedTest.php | 65 ++++++++++++++++++++++++++++++ 4 files changed, 99 insertions(+), 23 deletions(-) create mode 100644 app/Test/CryptGpgExtendedTest.php diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 18211593b..c3533193b 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -30,10 +30,6 @@ jobs: with: submodules: 'recursive' - # Stop mysql - - name: Shutdown Ubuntu MySQL (SUDO) - run: sudo service mysql stop - # Run mariadb - uses: getong/mariadb-action@v1.1 with: @@ -71,7 +67,7 @@ jobs: LC_ALL=C.UTF-8 sudo apt-add-repository ppa:ondrej/php -y if [[ $php_version == "7.2" ]]; then # hotfix due to: https://bugs.php.net/bug.php?id=81640 TODO: remove after libpcre2-8-0:10.36 gets to stable channel - sudo apt --fix-broken install + sudo apt-get --fix-broken install fi sudo apt-get -y install curl python3 python3-zmq python3-requests python3-pip python3-nose python3-redis python3-lxml apache2 libapache2-mod-php$php_version sudo pip3 install virtualenv # virtualenv must be instaled from pip and not from ubuntu packages @@ -209,16 +205,7 @@ jobs: sudo -E su $USER -c 'app/Console/cake Admin setSetting "MISP.python_bin" "$GITHUB_WORKSPACE/venv/bin/python"' . ./venv/bin/activate export PYTHONPATH=$PYTHONPATH:./app/files/scripts - pushd ./app/files/scripts/cti-python-stix2 - pip install . - popd - pushd ./app/files/scripts/python-stix - pip install . - popd - pushd PyMISP - pip install .[fileobjects,email] - popd - pip install zmq redis plyara + pip install ./PyMISP[fileobjects,email] ./app/files/scripts/python-stix ./app/files/scripts/cti-python-stix2 zmq redis plyara deactivate - name: Test if apache is working @@ -242,7 +229,7 @@ jobs: - name: Run PHP tests run: | ./app/Vendor/bin/parallel-lint --exclude app/Lib/cakephp/ --exclude app/Vendor/ --exclude app/Lib/random_compat/ -e php,ctp app/ - ./app/Vendor/bin/phpunit app/Test/ + sudo -u www-data ./app/Vendor/bin/phpunit app/Test/ - name: Run tests run: | diff --git a/app/Lib/Tools/CryptGpgExtended.php b/app/Lib/Tools/CryptGpgExtended.php index 3ea777b43..6a71884f0 100644 --- a/app/Lib/Tools/CryptGpgExtended.php +++ b/app/Lib/Tools/CryptGpgExtended.php @@ -173,4 +173,21 @@ class CryptGpgExtended extends Crypt_GPG return $armored; } + + /** + * @param mixed $data + * @param bool $isFile + * @param bool $allowEmpty + * @return resource|string|null + * @throws Crypt_GPG_FileException + * @throws Crypt_GPG_NoDataException + */ + protected function _prepareInput($data, $isFile = false, $allowEmpty = true) + { + if ($isFile && $data instanceof TmpFileTool) { + return $data->resource(); + } + + return parent::_prepareInput($data, $isFile, $allowEmpty); + } } diff --git a/app/Lib/Tools/TmpFileTool.php b/app/Lib/Tools/TmpFileTool.php index 39a90f9ed..601e30bea 100644 --- a/app/Lib/Tools/TmpFileTool.php +++ b/app/Lib/Tools/TmpFileTool.php @@ -1,14 +1,14 @@ rewind(); $string = stream_get_contents($this->tmpfile); if ($string === false) { throw new Exception('Could not read from temporary file.'); } - if ($close) { - $this->close(); - } + $this->close(); return $string; } @@ -175,6 +172,16 @@ class TmpFileTool $this->close(); } + /** + * @return resource + * @throws Exception + */ + public function resource() + { + $this->rewind(); + return $this->tmpfile; + } + /** * @return int * @throws Exception diff --git a/app/Test/CryptGpgExtendedTest.php b/app/Test/CryptGpgExtendedTest.php new file mode 100644 index 000000000..ce9a55e86 --- /dev/null +++ b/app/Test/CryptGpgExtendedTest.php @@ -0,0 +1,65 @@ +init(); + $this->assertInstanceOf('CryptGpgExtended', $gpg); + $this->assertIsString($gpg->getVersion()); + } + + public function testSignAndVerify() + { + $gpg = $this->init(); + include __DIR__ . '/../Config/config.php'; + $gpg->addSignKey($config['GnuPG']['email'], $config['GnuPG']['password']); + + $testString = 'ahojSvete'; + + $signature = $gpg->sign($testString, Crypt_GPG::SIGN_MODE_DETACHED, Crypt_GPG::ARMOR_BINARY); + $this->assertIsString($signature); + + $verified = $gpg->verify($testString, $signature); + $this->assertIsArray($verified); + $this->assertCount(1, $verified); + $this->assertTrue($verified[0]->isValid()); + + $signature = $gpg->sign($testString, Crypt_GPG::SIGN_MODE_DETACHED, Crypt_GPG::ARMOR_ASCII); + $this->assertIsString($signature); + + $verified = $gpg->verify($testString, $signature); + $this->assertIsArray($verified); + $this->assertCount(1, $verified); + $this->assertTrue($verified[0]->isValid()); + + // Tmp file + $tmpFile = new TmpFileTool(); + $tmpFile->write($testString); + $signature = $gpg->signFile($tmpFile, null, Crypt_GPG::SIGN_MODE_DETACHED, Crypt_GPG::ARMOR_BINARY); + $this->assertIsString($signature); + + $verified = $gpg->verify($testString, $signature); + $this->assertIsArray($verified); + $this->assertCount(1, $verified); + $this->assertTrue($verified[0]->isValid()); + } + + private function init(): CryptGpgExtended + { + require_once 'Crypt/GPG.php'; + include __DIR__ . '/../Config/config.php'; + + $options = [ + 'homedir' => $config['GnuPG']['homedir'], + 'gpgconf' => $config['GnuPG']['gpgconf'] ?? null, + 'binary' => $config['GnuPG']['binary'] ?? '/usr/bin/gpg', + ]; + return new CryptGpgExtended($options); + } +} From cc3b7271d937ae5ec6a1d0ffe48bdd2708004ad9 Mon Sep 17 00:00:00 2001 From: Jakub Onderka Date: Sat, 26 Mar 2022 10:37:40 +0100 Subject: [PATCH 004/120] fix: [api] EventsController::toggleProtect --- app/Controller/EventsController.php | 29 ++++++++++++++------------- tests/testlive_comprehensive_local.py | 16 +++++++++++++++ 2 files changed, 31 insertions(+), 14 deletions(-) diff --git a/app/Controller/EventsController.php b/app/Controller/EventsController.php index 773f8a10c..969561df8 100644 --- a/app/Controller/EventsController.php +++ b/app/Controller/EventsController.php @@ -6139,7 +6139,7 @@ class EventsController extends AppController /** * @param array $event - * @return CakeResponseTmp + * @return CakeResponseFile * @throws Exception */ private function __restResponse(array $event) @@ -6171,23 +6171,24 @@ class EventsController extends AppController public function protect($id) { - $this->__toggleProtect($id, true); + return $this->__toggleProtect($id, true); } public function unprotect($id) { - $this->__toggleProtect($id, false); + return $this->__toggleProtect($id, false); } + /** + * @param string|int $id Event ID or UUID + * @param bool $protect + * @return CakeResponse|void + * @throws Exception + */ private function __toggleProtect($id, $protect) { - $id = $this->Toolbox->findIdByUuid($this->Event, $id); - $event = $this->Event->fetchSimpleEvent($this->Auth->user(), $id, ['contain' => ['Orgc']]); - if ( - (!$this->_isSiteAdmin && $event['Event']['orgc_id'] !== $this->Auth->user('org_id')) || - !$event || - !$this->__canModifyEvent($event) - ) { + $event = $this->Event->fetchSimpleEvent($this->Auth->user(), $id); + if (empty($event) || !$this->__canModifyEvent($event)) { throw new NotFoundException(__('Invalid event')); } if ($this->request->is('post')) { @@ -6197,7 +6198,7 @@ class EventsController extends AppController if ($this->Event->save($event)) { $message = __('Event switched to %s mode.', $protect ? __('protected') : __('unprotected')); if ($this->_isRest()) { - return $this->RestResponse->saveSuccessResponse('events', $protect ? 'protect' : 'unprotect', $id, false, $message); + return $this->RestResponse->saveSuccessResponse('events', $protect ? 'protect' : 'unprotect', $event['Event']['id'], false, $message); } else { $this->Flash->success($message); $this->redirect(['controller' => 'events', 'action' => 'view', $id]); @@ -6205,14 +6206,14 @@ class EventsController extends AppController } else { $message = __('Something went wrong - could not switch event to %s mode.', $protect ? __('protected') : __('unprotected')); if ($this->_isRest()) { - return $this->RestResponse->saveFailResponse('Events', $protect ? 'protect' : 'unprotect', false, $message, $this->response->type()); + return $this->RestResponse->saveFailResponse('Events', $protect ? 'protect' : 'unprotect', $event['Event']['id'], $message); } else { $this->Flash->error($message); - $this->redirect(['controller' => 'events', 'action' => 'view', $id]); + $this->redirect(['controller' => 'events', 'action' => 'view', $event['Event']['id']]); } } } else { - $this->set('id', $id); + $this->set('id', $event['Event']['id']); $this->set('title', $protect ? __('Protect event') : __('Remove event protection')); $this->set( 'question', diff --git a/tests/testlive_comprehensive_local.py b/tests/testlive_comprehensive_local.py index 3c81bf241..0326fd3fe 100644 --- a/tests/testlive_comprehensive_local.py +++ b/tests/testlive_comprehensive_local.py @@ -719,6 +719,22 @@ class TestComprehensive(unittest.TestCase): response = request(self.admin_misp_connector, 'POST', f'warninglists/delete/{wl["Warninglist"]["id"]}') check_response(response) + def test_protected_event(self): + event = create_simple_event() + event = check_response(self.admin_misp_connector.add_event(event)) + + response = request(self.admin_misp_connector, 'POST', f'events/protect/{event.id}') + check_response(response) + + response = request(self.admin_misp_connector, 'POST', f'events/unprotect/{event.uuid}') + check_response(response) + + response = request(self.admin_misp_connector, 'POST', f'events/protect/{event.uuid}') + check_response(response) + + response = self.admin_misp_connector._prepare_request('GET', f'events/view/{event.id}') + print(response.headers) + def _search(self, query: dict): response = self.admin_misp_connector._prepare_request('POST', 'events/restSearch', data=query) response = self.admin_misp_connector._check_response(response) From f1dd24933cf7a9e23761ba37b07ff09f5c3639ab Mon Sep 17 00:00:00 2001 From: Jakub Onderka Date: Sat, 26 Mar 2022 11:20:55 +0100 Subject: [PATCH 005/120] fix: [sign] Allow to sign event by key stored in gpg homedir --- .github/workflows/main.yml | 45 ++++++++++++++------------- app/Model/CryptographicKey.php | 16 +++++++++- app/Model/Server.php | 9 ++++++ tests/testlive_comprehensive_local.py | 4 ++- 4 files changed, 50 insertions(+), 24 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index c3533193b..e5d210e5b 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -145,32 +145,33 @@ jobs: - name: Configure MISP run: | - sudo -E su $USER -c 'app/Console/cake userInit -q | sudo tee ./key.txt' + sudo -u $USER app/Console/cake userInit -q | sudo tee ./key.txt echo "AUTH=`cat key.txt`" >> $GITHUB_ENV - sudo -E su $USER -c 'app/Console/cake Admin setSetting "Session.autoRegenerate" 0' - sudo -E su $USER -c 'app/Console/cake Admin setSetting "Session.timeout" 600' - sudo -E su $USER -c 'app/Console/cake Admin setSetting "Session.cookieTimeout" 3600' - sudo -E su $USER -c 'app/Console/cake Admin setSetting "MISP.host_org_id" 1' - sudo -E su $USER -c 'app/Console/cake Admin setSetting "MISP.email" "info@admin.test"' - sudo -E su $USER -c 'app/Console/cake Admin setSetting "MISP.disable_emailing" false' - sudo -E su $USER -c 'app/Console/cake Admin setSetting --force "debug" true' - sudo -E su $USER -c 'app/Console/cake Admin setSetting "Plugin.CustomAuth_disable_logout" false' - sudo -E su $USER -c 'app/Console/cake Admin setSetting "MISP.redis_host" "127.0.0.1"' - sudo -E su $USER -c 'app/Console/cake Admin setSetting "MISP.redis_port" 6379' - sudo -E su $USER -c 'app/Console/cake Admin setSetting "MISP.redis_database" 13' - sudo -E su $USER -c 'app/Console/cake Admin setSetting "MISP.redis_password" ""' - sudo -E su $USER -c 'app/Console/cake Admin setSetting "GnuPG.email" "info@admin.test"' - sudo -E su $USER -c 'app/Console/cake Admin setSetting "GnuPG.homedir" "`pwd`/.gnupg"' - sudo -E su $USER -c 'app/Console/cake Admin setSetting "GnuPG.password" "travistest"' + sudo -u $USER app/Console/cake Admin setSetting "Session.autoRegenerate" 0 + sudo -u $USER app/Console/cake Admin setSetting "Session.timeout" 600 + sudo -u $USER app/Console/cake Admin setSetting "Session.cookieTimeout" 3600 + sudo -u $USER app/Console/cake Admin setSetting "MISP.host_org_id" 1 + sudo -u $USER app/Console/cake Admin setSetting "MISP.email" "info@admin.test" + sudo -u $USER app/Console/cake Admin setSetting "MISP.disable_emailing" false + sudo -u $USER app/Console/cake Admin setSetting --force "debug" true + sudo -u $USER app/Console/cake Admin setSetting "Plugin.CustomAuth_disable_logout" false + sudo -u $USER app/Console/cake Admin setSetting "MISP.redis_host" "127.0.0.1" + sudo -u $USER app/Console/cake Admin setSetting "MISP.redis_port" 6379 + sudo -u $USER app/Console/cake Admin setSetting "MISP.redis_database" 13 + sudo -u $USER app/Console/cake Admin setSetting "MISP.redis_password" "" + sudo -u $USER app/Console/cake Admin setSetting "GnuPG.email" "info@admin.test" + sudo -u $USER app/Console/cake Admin setSetting "GnuPG.homedir" "`pwd`/.gnupg" + sudo -u $USER app/Console/cake Admin setSetting "GnuPG.password" "travistest" + sudo -u $USER app/Console/cake Admin setSetting "MISP.download_gpg_from_homedir" 1 - name: Configure ZMQ run: | - sudo -E su $USER -c 'app/Console/cake Admin setSetting "Plugin.ZeroMQ_redis_host" "127.0.0.1"' - sudo -E su $USER -c 'app/Console/cake Admin setSetting "Plugin.ZeroMQ_redis_port" 6379' - sudo -E su $USER -c 'app/Console/cake Admin setSetting "Plugin.ZeroMQ_redis_database" 1' - sudo -E su $USER -c 'app/Console/cake Admin setSetting "Plugin.ZeroMQ_redis_password" ""' - sudo -E su $USER -c 'app/Console/cake Admin setSetting "Plugin.ZeroMQ_enable" 1' - sudo -E su $USER -c 'app/Console/cake Admin setSetting "Plugin.ZeroMQ_audit_notifications_enable" 1' + sudo -u $USER app/Console/cake Admin setSetting "Plugin.ZeroMQ_redis_host" "127.0.0.1" + sudo -u $USER app/Console/cake Admin setSetting "Plugin.ZeroMQ_redis_port" 6379 + sudo -u $USER app/Console/cake Admin setSetting "Plugin.ZeroMQ_redis_database" 1 + sudo -u $USER app/Console/cake Admin setSetting "Plugin.ZeroMQ_redis_password" "" + sudo -u $USER app/Console/cake Admin setSetting "Plugin.ZeroMQ_enable" 1 + sudo -u $USER app/Console/cake Admin setSetting "Plugin.ZeroMQ_audit_notifications_enable" 1 - name: Update Galaxies run: sudo -E su $USER -c 'app/Console/cake Admin updateGalaxies' diff --git a/app/Model/CryptographicKey.php b/app/Model/CryptographicKey.php index 9d7f9aec9..c0d8f1564 100644 --- a/app/Model/CryptographicKey.php +++ b/app/Model/CryptographicKey.php @@ -89,12 +89,26 @@ class CryptographicKey extends AppModel } /** - * @return string + * @return string Instance key fingerprint * @throws Crypt_GPG_BadPassphraseException * @throws Crypt_GPG_Exception */ public function ingestInstanceKey() { + // If instance just key stored just in GPG homedir, use that key. + if (Configure::read('MISP.download_gpg_from_homedir')) { + if (!$this->gpg) { + throw new Exception("Could not initiate GPG"); + } + /** @var Crypt_GPG_Key[] $keys */ + $keys = $this->gpg->getKeys(Configure::read('GnuPG.email')); + if (empty($keys)) { + return false; + } + $this->gpg->addSignKey($keys[0], Configure::read('GnuPG.password')); + return $keys[0]->getPrimaryKey()->getFingerprint(); + } + try { $redis = $this->setupRedisWithException(); } catch (Exception $e) { diff --git a/app/Model/Server.php b/app/Model/Server.php index c32b16cca..e9e60098a 100644 --- a/app/Model/Server.php +++ b/app/Model/Server.php @@ -5666,6 +5666,15 @@ class Server extends AppModel 'type' => 'boolean', 'null' => true, ], + 'download_gpg_from_homedir' => [ + 'level' => self::SETTING_OPTIONAL, + 'description' => __('Fetch GPG instance key from GPG homedir.'), + 'value' => false, + 'test' => 'testBool', + 'type' => 'boolean', + 'null' => true, + 'cli_only' => true, + ], ), 'GnuPG' => array( 'branch' => 1, diff --git a/tests/testlive_comprehensive_local.py b/tests/testlive_comprehensive_local.py index 0326fd3fe..f554ab615 100644 --- a/tests/testlive_comprehensive_local.py +++ b/tests/testlive_comprehensive_local.py @@ -733,7 +733,9 @@ class TestComprehensive(unittest.TestCase): check_response(response) response = self.admin_misp_connector._prepare_request('GET', f'events/view/{event.id}') - print(response.headers) + self.assertIn('x-pgp-signature', response.headers) + self.assertTrue(len(response.headers['x-pgp-signature']) > 0, response.headers['x-pgp-signature']) + print(response.headers['x-pgp-signature']) def _search(self, query: dict): response = self.admin_misp_connector._prepare_request('POST', 'events/restSearch', data=query) From edbf26d9b2e90750c0cc17173e02fab5db69c144 Mon Sep 17 00:00:00 2001 From: Jakub Onderka Date: Sat, 26 Mar 2022 12:09:16 +0100 Subject: [PATCH 006/120] fix: [internal] Simplify RestResponse code --- app/Controller/Component/RestResponseComponent.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/app/Controller/Component/RestResponseComponent.php b/app/Controller/Component/RestResponseComponent.php index 33bf6e28d..341f34eff 100644 --- a/app/Controller/Component/RestResponseComponent.php +++ b/app/Controller/Component/RestResponseComponent.php @@ -592,14 +592,14 @@ class RestResponseComponent extends Component } if ($response instanceof TmpFileTool) { - App::uses('CakeResponseFile', 'Tools'); - $cakeResponse = new CakeResponseFile(['status' => $code, 'type' => $type]); if ($this->signContents) { $this->CryptographicKey = ClassRegistry::init('CryptographicKey'); $data = $response->intoString(); $headers['x-pgp-signature'] = base64_encode($this->CryptographicKey->signWithInstanceKey($data)); - $cakeResponse = new CakeResponse(array('body' => $data, 'status' => $code, 'type' => $type)); + $cakeResponse = new CakeResponse(['body' => $data, 'status' => $code, 'type' => $type]); } else { + App::uses('CakeResponseFile', 'Tools'); + $cakeResponse = new CakeResponseFile(['status' => $code, 'type' => $type]); $cakeResponse->file($response); } } else { From a07625294b6c8ce3a658eb50ea9d43fbd6501866 Mon Sep 17 00:00:00 2001 From: Jakub Onderka Date: Sat, 26 Mar 2022 12:10:11 +0100 Subject: [PATCH 007/120] chg: [sign] Return signature in binary format --- app/Model/CryptographicKey.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/Model/CryptographicKey.php b/app/Model/CryptographicKey.php index c0d8f1564..4ee8722ba 100644 --- a/app/Model/CryptographicKey.php +++ b/app/Model/CryptographicKey.php @@ -161,7 +161,7 @@ class CryptographicKey extends AppModel return false; } $data = preg_replace("/\s+/", "", $data); - $signature = $this->gpg->sign($data, Crypt_GPG::SIGN_MODE_DETACHED); + $signature = $this->gpg->sign($data, Crypt_GPG::SIGN_MODE_DETACHED, Crypt_GPG::ARMOR_BINARY); return $signature; } From 6af7503b367df16ae3ae9b8348d65f8682709599 Mon Sep 17 00:00:00 2001 From: Jakub Onderka Date: Sat, 26 Mar 2022 13:16:51 +0100 Subject: [PATCH 008/120] fix: [sign] Remove unused method --- app/Model/CryptographicKey.php | 9 --------- 1 file changed, 9 deletions(-) diff --git a/app/Model/CryptographicKey.php b/app/Model/CryptographicKey.php index 4ee8722ba..2f2f94179 100644 --- a/app/Model/CryptographicKey.php +++ b/app/Model/CryptographicKey.php @@ -165,15 +165,6 @@ class CryptographicKey extends AppModel return $signature; } - public function signFileWithInstanceKey($path) - { - if (!$this->ingestInstanceKey()) { - return false; - } - $signature = $this->gpg->signFile($path, Crypt_GPG::SIGN_MODE_DETACHED); - return $signature; - } - /** * @param string $data * @param string $signature From 5b65286f51682ec936c6ddcbb996e02ce9b99b79 Mon Sep 17 00:00:00 2001 From: Jakub Onderka Date: Sat, 26 Mar 2022 13:17:19 +0100 Subject: [PATCH 009/120] new: [test] JSONConverterToolTest --- app/Test/JSONConverterToolTest.php | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/app/Test/JSONConverterToolTest.php b/app/Test/JSONConverterToolTest.php index 983a90a30..14c1d3742 100644 --- a/app/Test/JSONConverterToolTest.php +++ b/app/Test/JSONConverterToolTest.php @@ -41,6 +41,12 @@ class JSONConverterToolTest extends TestCase foreach (JSONConverterTool::streamConvert($event) as $part) { $json .= $part; } + + // This check is important for protected events + $jsonStreamWithoutSpaces = preg_replace("/\s+/", "", $json); + $jsonNormalWithoutSpaces = preg_replace("/\s+/", "", JSONConverterTool::convert($event)); + $this->assertEquals($jsonNormalWithoutSpaces, $jsonStreamWithoutSpaces); + if (defined('JSON_THROW_ON_ERROR')) { json_decode($json, true, 512, JSON_THROW_ON_ERROR); $this->assertTrue(true); From bfb9e98c565b797350132f8c979b4b122aef9037 Mon Sep 17 00:00:00 2001 From: Jakub Onderka Date: Sat, 26 Mar 2022 13:38:38 +0100 Subject: [PATCH 010/120] fixup! fix: [sign] Allow to sign event by key stored in gpg homedir --- tests/testlive_comprehensive_local.py | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/testlive_comprehensive_local.py b/tests/testlive_comprehensive_local.py index f554ab615..aac58a386 100644 --- a/tests/testlive_comprehensive_local.py +++ b/tests/testlive_comprehensive_local.py @@ -735,7 +735,6 @@ class TestComprehensive(unittest.TestCase): response = self.admin_misp_connector._prepare_request('GET', f'events/view/{event.id}') self.assertIn('x-pgp-signature', response.headers) self.assertTrue(len(response.headers['x-pgp-signature']) > 0, response.headers['x-pgp-signature']) - print(response.headers['x-pgp-signature']) def _search(self, query: dict): response = self.admin_misp_connector._prepare_request('POST', 'events/restSearch', data=query) From e6202e80788048ff62d074595e2105d18ea5f2ce Mon Sep 17 00:00:00 2001 From: Jakub Onderka Date: Sat, 26 Mar 2022 13:43:10 +0100 Subject: [PATCH 011/120] fixup! new: [test] JSONConverterToolTest --- app/Lib/Tools/JSONConverterTool.php | 4 ++-- app/Test/JSONConverterToolTest.php | 12 +++++++++++- 2 files changed, 13 insertions(+), 3 deletions(-) diff --git a/app/Lib/Tools/JSONConverterTool.php b/app/Lib/Tools/JSONConverterTool.php index 15b614991..0e66184bf 100644 --- a/app/Lib/Tools/JSONConverterTool.php +++ b/app/Lib/Tools/JSONConverterTool.php @@ -93,7 +93,7 @@ class JSONConverterTool if ($raw) { return $result; } - return json_encode($result, JSON_PRETTY_PRINT); + return json_encode($result, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES); } /** @@ -127,7 +127,7 @@ class JSONConverterTool } } if (isset($event['errors'])) { - yield '},"errors":' . json_encode($event['errors']) . '}'; + yield '},"errors":' . json_encode($event['errors'], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) . '}'; } else { yield "}}"; } diff --git a/app/Test/JSONConverterToolTest.php b/app/Test/JSONConverterToolTest.php index 14c1d3742..5e7758148 100644 --- a/app/Test/JSONConverterToolTest.php +++ b/app/Test/JSONConverterToolTest.php @@ -35,6 +35,16 @@ class JSONConverterToolTest extends TestCase $this->check($event); } + public function testCheckJsonIsValidUnicodeSlashes(): void + { + $attribute = ['id' => 1, 'event_id' => 2, 'type' => 'ip-src', 'value' => '1.1.1.1']; + $event = ['Event' => ['id' => 2, 'info' => 'Test event ěšřžýáí \/'], 'errors' => 'chyba ě+š']; + for ($i = 0; $i < 5; $i++) { + $event['Attribute'][] = $attribute; + } + $this->check($event); + } + private function check(array $event): void { $json = ''; @@ -42,7 +52,7 @@ class JSONConverterToolTest extends TestCase $json .= $part; } - // This check is important for protected events + // Check if result is the same without spaces $jsonStreamWithoutSpaces = preg_replace("/\s+/", "", $json); $jsonNormalWithoutSpaces = preg_replace("/\s+/", "", JSONConverterTool::convert($event)); $this->assertEquals($jsonNormalWithoutSpaces, $jsonStreamWithoutSpaces); From 8636c1f90303f04acc6604924e87806fb82b1559 Mon Sep 17 00:00:00 2001 From: Jakub Onderka Date: Sun, 27 Mar 2022 13:05:33 +0200 Subject: [PATCH 012/120] chg: [syslog] Remove duplicate date and log type from log --- app/Model/AuditLog.php | 2 +- app/Model/Log.php | 6 +++--- app/Model/User.php | 5 ++--- app/Plugin/SysLog/Lib/SysLog.php | 17 ++++++----------- 4 files changed, 12 insertions(+), 18 deletions(-) diff --git a/app/Model/AuditLog.php b/app/Model/AuditLog.php index 2a0002ae7..3b53ad9f9 100644 --- a/app/Model/AuditLog.php +++ b/app/Model/AuditLog.php @@ -272,7 +272,7 @@ class AuditLog extends AppModel if ($title) { $entry .= " -- $title"; } - $this->syslog->write('info', $entry); + $this->syslog->write(LOG_INFO, $entry); } return true; } diff --git a/app/Model/Log.php b/app/Model/Log.php index 7d6c224f6..5d49ddd1f 100644 --- a/app/Model/Log.php +++ b/app/Model/Log.php @@ -383,13 +383,13 @@ class Log extends AppModel } } if ($this->syslog) { - $action = 'info'; + $action = LOG_INFO; if (isset($data['Log']['action'])) { if (in_array($data['Log']['action'], self::ERROR_ACTIONS, true)) { - $action = 'err'; + $action = LOG_ERR; } if (in_array($data['Log']['action'], self::WARNING_ACTIONS, true)) { - $action = 'warning'; + $action = LOG_WARNING; } } diff --git a/app/Model/User.php b/app/Model/User.php index 36098453d..a70eb3968 100644 --- a/app/Model/User.php +++ b/app/Model/User.php @@ -1223,13 +1223,12 @@ class User extends AppModel } // query - $this->Log = ClassRegistry::init('Log'); - $result = $this->Log->createLogEntry($user, $action, $model, $modelId, $description, $fieldsResult); + $result = $this->loadLog()->createLogEntry($user, $action, $model, $modelId, $description, $fieldsResult); // write to syslogd as well App::import('Lib', 'SysLog.SysLog'); $syslog = new SysLog(); - $syslog->write('notice', "$description -- $action" . (empty($fieldsResult) ? '' : ' -- ' . $result['Log']['change'])); + $syslog->write(LOG_NOTICE, "$description -- $action" . (empty($fieldsResult) ? '' : ' -- ' . $result['Log']['change'])); } /** diff --git a/app/Plugin/SysLog/Lib/SysLog.php b/app/Plugin/SysLog/Lib/SysLog.php index 719300c6f..72fd443f4 100644 --- a/app/Plugin/SysLog/Lib/SysLog.php +++ b/app/Plugin/SysLog/Lib/SysLog.php @@ -38,9 +38,9 @@ class SysLog * @param array $options Options for the SysLog, see above. * @return void */ - public function __construct($options = array()) + public function __construct($options = []) { - $options += array('ident' => LOGS, 'facility' => LOG_LOCAL0, 'to_stderr' => true); + $options += ['ident' => LOGS, 'facility' => LOG_LOCAL0, 'to_stderr' => true]; $option = LOG_PID; // include PID with each message if ($options['to_stderr']) { $option |= LOG_PERROR; // print log message also to standard error @@ -51,7 +51,7 @@ class SysLog /** * Implements writing to the specified syslog * - * @param string $type The type of log you are making. + * @param int $type The type of log you are making. * @param string $message The message you want to log. * @return boolean success of write. */ @@ -60,14 +60,9 @@ class SysLog if (!$this->_log) { return false; } - $debugTypes = array('notice', 'info', 'debug'); - $priority = LOG_INFO; - if ($type == 'error' || $type == 'warning') { - $priority = LOG_ERR; - } else if (in_array($type, $debugTypes)) { - $priority = LOG_DEBUG; + if (!is_int($type)) { + throw new InvalidArgumentException("Invalid log type `$type`, must be one of LOG_* constant."); } - $output = date('Y-m-d H:i:s') . ' ' . ucfirst($type) . ': ' . $message; - return syslog($priority, $output); + return syslog($type, $message); } } From 72099918d551e59439bf74c2aef8b52c165cd171 Mon Sep 17 00:00:00 2001 From: Jakub Onderka Date: Sun, 27 Mar 2022 16:04:16 +0200 Subject: [PATCH 013/120] new: [test] advanced_authkeys_non_exists_user --- tests/testlive_security.py | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/tests/testlive_security.py b/tests/testlive_security.py index d6187c823..d51b2828e 100644 --- a/tests/testlive_security.py +++ b/tests/testlive_security.py @@ -130,7 +130,7 @@ class TestSecurity(unittest.TestCase): def setUpClass(cls): warnings.simplefilter("ignore", ResourceWarning) - # Connect as admin + # Connect as site admin cls.admin_misp_connector = PyMISP(url, key) # Set expected config values check_response(cls.admin_misp_connector.set_server_setting('debug', 1, force=True)) @@ -415,12 +415,16 @@ class TestSecurity(unittest.TestCase): time.sleep(1) + def test_advanced_authkeys_non_exists_user(self): + new_auth_key = send(self.admin_misp_connector, "POST", "authKeys/add/9999", check_errors=False) + self.assertErrorResponse(new_auth_key) + self.assertIn("user_id", new_auth_key["errors"][1]["errors"]) + def test_advanced_authkeys_own_key_not_possible(self): - with self.__setting("Security.advanced_authkeys", True): - authkey = ("a" * 40) - auth_key = self.__create_advanced_authkey(self.test_usr.id, {"authkey": authkey}) - self.__delete_advanced_authkey(auth_key["id"]) - self.assertNotEqual(authkey, auth_key["authkey_raw"]) + authkey = ("a" * 40) + auth_key = self.__create_advanced_authkey(self.test_usr.id, {"authkey": authkey}) + self.__delete_advanced_authkey(auth_key["id"]) + self.assertNotEqual(authkey, auth_key["authkey_raw"]) def test_advanced_authkeys_reset_own(self): with self.__setting("Security.advanced_authkeys", True): @@ -1619,7 +1623,7 @@ class TestSecurity(unittest.TestCase): def __create_advanced_authkey(self, user_id: int, data: Optional[dict] = None) -> dict: auth_key = send(self.admin_misp_connector, "POST", f'authKeys/add/{user_id}', data=data)["AuthKey"] # it is not possible to call `assertEqual`, because we use this method in `setUpClass` method - assert user_id == auth_key["user_id"], "Key was created for different user" + assert int(user_id) == int(auth_key["user_id"]), f"Key was created for different user ({user_id} != {auth_key['user_id']})" return auth_key def __login(self, user: MISPUser) -> PyMISP: From cba6e6dc8c0d16b486d5031d4a368299c347b8af Mon Sep 17 00:00:00 2001 From: Jakub Onderka Date: Sun, 27 Mar 2022 17:01:36 +0200 Subject: [PATCH 014/120] chg: [test] Try to avoid installing poetry --- .github/workflows/main.yml | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index e5d210e5b..8c9ad79c8 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -71,7 +71,6 @@ jobs: fi sudo apt-get -y install curl python3 python3-zmq python3-requests python3-pip python3-nose python3-redis python3-lxml apache2 libapache2-mod-php$php_version sudo pip3 install virtualenv # virtualenv must be instaled from pip and not from ubuntu packages - curl -sSL https://install.python-poetry.org | python - sudo chown $USER:www-data $HOME/.composer pushd app sudo -H -u $USER php composer.phar install --no-progress @@ -206,7 +205,7 @@ jobs: sudo -E su $USER -c 'app/Console/cake Admin setSetting "MISP.python_bin" "$GITHUB_WORKSPACE/venv/bin/python"' . ./venv/bin/activate export PYTHONPATH=$PYTHONPATH:./app/files/scripts - pip install ./PyMISP[fileobjects,email] ./app/files/scripts/python-stix ./app/files/scripts/cti-python-stix2 zmq redis plyara + pip install ./PyMISP[fileobjects,email] ./app/files/scripts/python-stix ./app/files/scripts/cti-python-stix2 pyzmq redis plyara deactivate - name: Test if apache is working @@ -234,27 +233,28 @@ jobs: - name: Run tests run: | - export PATH=$HOME/.local/env:$PATH # enable poetry binary - pushd tests ./curl_tests_GH.sh $AUTH $HOST popd sudo chmod -R g+ws `pwd`/app/tmp/logs + + . ./venv/bin/activate pushd PyMISP - poetry install -E fileobjects -E openioc -E virustotal -E docs -E pdfexport -E email - poetry run python tests/testlive_comprehensive.py - poetry add lxml - poetry run python ../tests/testlive_security.py -v - poetry run python ../tests/testlive_sync.py - poetry run python ../tests/testlive_comprehensive_local.py -v - poetry run python tests/test_mispevent.py + python tests/testlive_comprehensive.py + popd + python tests/testlive_security.py -v + python tests/testlive_sync.py + python tests/testlive_comprehensive_local.py -v + pushd PyMISP + python tests/test_mispevent.py popd cp PyMISP/tests/keys.py PyMISP/examples/events/ pushd PyMISP/examples/events/ - poetry run python ./create_massive_dummy_events.py -l 5 -a 30 + python ./create_massive_dummy_events.py -l 5 -a 30 popd - python3 tools/misp-feed/validate.py + python tools/misp-feed/validate.py + deactivate - name: Logs if: ${{ always() }} From 1a589c64f82ff192d0700ba0cfddb334a497d3a2 Mon Sep 17 00:00:00 2001 From: Jakub Onderka Date: Sun, 27 Mar 2022 17:02:36 +0200 Subject: [PATCH 015/120] chg [authkeys] Add validation --- .github/workflows/main.yml | 23 ++++++------ app/Controller/AuthKeysController.php | 9 +++-- app/Controller/Component/CRUDComponent.php | 13 +++---- app/Lib/Tools/RandomTool.php | 2 +- app/Model/AuthKey.php | 41 ++++++++++++++++------ 5 files changed, 53 insertions(+), 35 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 8c9ad79c8..e0d4fa9c4 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -57,19 +57,22 @@ jobs: echo "USER=`id -u -n`" >> $GITHUB_ENV echo "HOST=localhost" >> $GITHUB_ENV - # Runs a set of commands using the runners shell - - name: Install deps + - name: Install system deps env: php_version: ${{ matrix.php }} run: | - sudo apt-get -y update - # Repo is missing for unknown reason - LC_ALL=C.UTF-8 sudo apt-add-repository ppa:ondrej/php -y - if [[ $php_version == "7.2" ]]; then - # hotfix due to: https://bugs.php.net/bug.php?id=81640 TODO: remove after libpcre2-8-0:10.36 gets to stable channel - sudo apt-get --fix-broken install - fi - sudo apt-get -y install curl python3 python3-zmq python3-requests python3-pip python3-nose python3-redis python3-lxml apache2 libapache2-mod-php$php_version + sudo apt-get -y update + # Repo is missing for unknown reason + LC_ALL=C.UTF-8 sudo apt-add-repository ppa:ondrej/php -y + if [[ $php_version == "7.2" ]]; then + # hotfix due to: https://bugs.php.net/bug.php?id=81640 TODO: remove after libpcre2-8-0:10.36 gets to stable channel + sudo apt-get --fix-broken install + fi + sudo apt-get -y install curl python3 python3-zmq python3-requests python3-pip python3-nose python3-redis python3-lxml apache2 libapache2-mod-php$php_version + + # Runs a set of commands using the runners shell + - name: Install deps + run: | sudo pip3 install virtualenv # virtualenv must be instaled from pip and not from ubuntu packages sudo chown $USER:www-data $HOME/.composer pushd app diff --git a/app/Controller/AuthKeysController.php b/app/Controller/AuthKeysController.php index 492a50725..507167ed4 100644 --- a/app/Controller/AuthKeysController.php +++ b/app/Controller/AuthKeysController.php @@ -141,10 +141,10 @@ class AuthKeysController extends AppController } $params = [ 'displayOnSuccess' => 'authkey_display', - 'saveModelVariable' => ['authkey_raw'], 'override' => ['authkey' => null], // do not allow to use own key, always generate random one - 'afterFind' => function ($authKey) { // remove hashed key from response + 'afterFind' => function (array $authKey, array $savedData) { // remove hashed key from response unset($authKey['AuthKey']['authkey']); + $authKey['AuthKey']['authkey_raw'] = $savedData['AuthKey']['authkey_raw']; return $authKey; } ]; @@ -195,11 +195,10 @@ class AuthKeysController extends AppController if ($this->IndexFilter->isRest()) { return $this->restResponsePayload; } - $this->loadModel('User'); $dropdownData = [ - 'user' => $this->User->find('list', [ + 'user' => $this->AuthKey->User->find('list', [ 'sort' => ['username' => 'asc'], - 'conditions' => $selectConditions + 'conditions' => $selectConditions, ]) ]; $this->set(compact('dropdownData')); diff --git a/app/Controller/Component/CRUDComponent.php b/app/Controller/Component/CRUDComponent.php index 7b9697b7c..fb276e34d 100644 --- a/app/Controller/Component/CRUDComponent.php +++ b/app/Controller/Component/CRUDComponent.php @@ -90,7 +90,8 @@ class CRUDComponent extends Component } /** @var Model $model */ $model = $this->Controller->{$modelName}; - if ($model->save($data)) { + $savedData = $model->save($data); + if ($savedData) { if (isset($params['afterSave'])) { $params['afterSave']($data); } @@ -100,15 +101,11 @@ class CRUDComponent extends Component 'id' => $model->id ] ]); - if (!empty($params['saveModelVariable'])) { - foreach ($params['saveModelVariable'] as $var) { - if (isset($model->$var)) { - $data[$modelName][$var] = $model->$var; - } - } + if (empty($data)) { + throw new Exception("Something went wrong, saved data not found in database."); } if (isset($params['afterFind'])) { - $data = $params['afterFind']($data); + $data = $params['afterFind']($data, $savedData); } $message = __('%s added.', $modelName); if ($this->Controller->IndexFilter->isRest()) { diff --git a/app/Lib/Tools/RandomTool.php b/app/Lib/Tools/RandomTool.php index 52439c129..a729a6ace 100644 --- a/app/Lib/Tools/RandomTool.php +++ b/app/Lib/Tools/RandomTool.php @@ -19,7 +19,7 @@ class RandomTool * @return string * @throws Exception */ - public function random_str($crypto_secure = true, $length = 32, $charset = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ') + public static function random_str($crypto_secure = true, $length = 32, $charset = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ') { // Type checks: if (!is_bool($crypto_secure)) { diff --git a/app/Model/AuthKey.php b/app/Model/AuthKey.php index b06e902f9..954e0a693 100644 --- a/app/Model/AuthKey.php +++ b/app/Model/AuthKey.php @@ -1,8 +1,6 @@ array( - 'userModel' => 'User', - 'userKey' => 'user_id', - 'change' => 'full'), + 'userModel' => 'User', + 'userKey' => 'user_id', + 'change' => 'full' + ), 'Containable', ); @@ -25,9 +24,20 @@ class AuthKey extends AppModel 'User' ); - public $authkey_raw = false; + public $validate = [ + 'uuid' => [ + 'rule' => 'uuid', + 'message' => 'Please provide a valid RFC 4122 UUID', + ], + 'user_id' => [ + 'rule' => 'userExists', + 'message' => 'User doesn\'t exists', + ], + 'read_only' => [ + 'rule' => 'boolean', + ], + ]; - // massage the data before we send it off for validation before saving anything public function beforeValidate($options = array()) { if (empty($this->data['AuthKey']['id'])) { @@ -35,16 +45,14 @@ class AuthKey extends AppModel $this->data['AuthKey']['uuid'] = CakeText::uuid(); } if (empty($this->data['AuthKey']['authkey'])) { - $authkey = (new RandomTool())->random_str(true, 40); + $authkey = RandomTool::random_str(true, 40); } else { $authkey = $this->data['AuthKey']['authkey']; } - $passwordHasher = $this->getHasher(); - $this->data['AuthKey']['authkey'] = $passwordHasher->hash($authkey); + $this->data['AuthKey']['authkey'] = $this->getHasher()->hash($authkey); $this->data['AuthKey']['authkey_start'] = substr($authkey, 0, 4); $this->data['AuthKey']['authkey_end'] = substr($authkey, -4); $this->data['AuthKey']['authkey_raw'] = $authkey; - $this->authkey_raw = $authkey; } if (!empty($this->data['AuthKey']['allowed_ips'])) { @@ -54,6 +62,7 @@ class AuthKey extends AppModel if (empty($allowedIps)) { $allowedIps = []; } else { + // Split by new line char or by comma $allowedIps = preg_split('/([\n,])/', $allowedIps); $allowedIps = array_map('trim', $allowedIps); } @@ -336,6 +345,16 @@ class AuthKey extends AppModel $this->User->updateAll(['date_modified' => time()], ['User.id' => $userId]); } + /** + * Validation + * @param array $check + * @return bool + */ + public function userExists(array $check) + { + return $this->User->hasAny(['id' => $check['user_id']]); + } + /** * @return AbstractPasswordHasher */ From 8639c7436eefad90a3d1e9c7450bdae34030ca18 Mon Sep 17 00:00:00 2001 From: Jakub Onderka Date: Sun, 27 Mar 2022 18:07:23 +0200 Subject: [PATCH 016/120] chg: [test] Do not install useless system packages --- .github/workflows/main.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index e0d4fa9c4..e9319a4d3 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -68,7 +68,7 @@ jobs: # hotfix due to: https://bugs.php.net/bug.php?id=81640 TODO: remove after libpcre2-8-0:10.36 gets to stable channel sudo apt-get --fix-broken install fi - sudo apt-get -y install curl python3 python3-zmq python3-requests python3-pip python3-nose python3-redis python3-lxml apache2 libapache2-mod-php$php_version + sudo apt-get -y install curl python3 python3-pip python3-nose apache2 libapache2-mod-php$php_version # Runs a set of commands using the runners shell - name: Install deps From 67fd15f5435a3960a647ab26d7b7fbac9e41dca4 Mon Sep 17 00:00:00 2001 From: Jakub Onderka Date: Sun, 27 Mar 2022 18:28:00 +0200 Subject: [PATCH 017/120] chg: [warninglist] Insert in bigger chunks --- app/Model/Warninglist.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/Model/Warninglist.php b/app/Model/Warninglist.php index 3ae8f2218..cdf7c2886 100644 --- a/app/Model/Warninglist.php +++ b/app/Model/Warninglist.php @@ -383,7 +383,7 @@ class Warninglist extends AppModel $keys = array_keys($list['list']); if ($keys === array_keys($keys)) { - foreach (array_chunk($list['list'], 500) as $chunk) { + foreach (array_chunk($list['list'], 1000) as $chunk) { $valuesToInsert = []; foreach ($chunk as $value) { if (!empty($value)) { @@ -393,7 +393,7 @@ class Warninglist extends AppModel $result = $db->insertMulti('warninglist_entries', ['value', 'warninglist_id'], $valuesToInsert); } } else { // import warninglist with comments - foreach (array_chunk($list['list'], 500, true) as $chunk) { + foreach (array_chunk($list['list'], 1000, true) as $chunk) { $valuesToInsert = []; foreach ($chunk as $value => $comment) { if (!empty($value)) { From 4af34a999cf8f94539100d1188bd5634a1c603d3 Mon Sep 17 00:00:00 2001 From: Jakub Onderka Date: Sun, 27 Mar 2022 18:45:14 +0200 Subject: [PATCH 018/120] chg [galaxy] Simplify saving galaxies --- app/Model/Galaxy.php | 26 ++++++++++++++------------ app/Model/GalaxyCluster.php | 1 + app/Model/GalaxyClusterRelation.php | 5 ----- 3 files changed, 15 insertions(+), 17 deletions(-) diff --git a/app/Model/Galaxy.php b/app/Model/Galaxy.php index a14da5c44..8faa91fa4 100644 --- a/app/Model/Galaxy.php +++ b/app/Model/Galaxy.php @@ -101,6 +101,11 @@ class Galaxy extends AppModel ]; } + /** + * @param array $galaxies + * @param array $cluster_package + * @return array + */ private function __getPreExistingClusters(array $galaxies, array $cluster_package) { $temp = $this->GalaxyCluster->find('all', array( @@ -110,11 +115,7 @@ class Galaxy extends AppModel 'recursive' => -1, 'fields' => array('version', 'id', 'value', 'uuid') )); - $existingClusters = []; - foreach ($temp as $v) { - $existingClusters[$v['GalaxyCluster']['value']] = $v; - } - return $existingClusters; + return array_column(array_column($temp, 'GalaxyCluster'), null, 'value'); } private function __deleteOutdated(bool $force, array $cluster_package, array $existingClusters) @@ -132,19 +133,20 @@ class Galaxy extends AppModel } else { $cluster_package['values'][$k]['version'] = 0; } - if (!empty($existingClusters[$cluster['value']])) { - if ($force || $existingClusters[$cluster['value']]['GalaxyCluster']['version'] < $cluster_package['values'][$k]['version']) { - $cluster_ids_to_delete[] = $existingClusters[$cluster['value']]['GalaxyCluster']['id']; - $cluster_uuids_to_delete[] = $existingClusters[$cluster['value']]['GalaxyCluster']['uuid']; + if (isset($existingClusters[$cluster['value']])) { + $existing = $existingClusters[$cluster['value']]; + if ($force || $existing['version'] < $cluster_package['values'][$k]['version']) { + $cluster_ids_to_delete[] = $existing['id']; + $cluster_uuids_to_delete[] = $existing['uuid']; } else { unset($cluster_package['values'][$k]); } } } if (!empty($cluster_ids_to_delete)) { - $this->GalaxyCluster->GalaxyElement->deleteAll(array('GalaxyElement.galaxy_cluster_id' => $cluster_ids_to_delete), false, false); - $this->GalaxyCluster->GalaxyClusterRelation->deleteRelations(array('GalaxyClusterRelation.galaxy_cluster_uuid' => $cluster_uuids_to_delete)); - $this->GalaxyCluster->deleteAll(array('GalaxyCluster.id' => $cluster_ids_to_delete), false, false); + $this->GalaxyCluster->GalaxyElement->deleteAll(array('GalaxyElement.galaxy_cluster_id' => $cluster_ids_to_delete), false); + $this->GalaxyCluster->GalaxyClusterRelation->deleteAll(array('GalaxyClusterRelation.galaxy_cluster_uuid' => $cluster_uuids_to_delete)); + $this->GalaxyCluster->deleteAll(array('GalaxyCluster.id' => $cluster_ids_to_delete), false); } return $cluster_package; } diff --git a/app/Model/GalaxyCluster.php b/app/Model/GalaxyCluster.php index addfdb26a..ef62bea99 100644 --- a/app/Model/GalaxyCluster.php +++ b/app/Model/GalaxyCluster.php @@ -5,6 +5,7 @@ App::uses('TmpFileTool', 'Tools'); /** * @property Tag $Tag * @property GalaxyClusterRelation $GalaxyClusterRelation + * @property GalaxyElement $GalaxyElement */ class GalaxyCluster extends AppModel { diff --git a/app/Model/GalaxyClusterRelation.php b/app/Model/GalaxyClusterRelation.php index 00b69153b..7119c9417 100644 --- a/app/Model/GalaxyClusterRelation.php +++ b/app/Model/GalaxyClusterRelation.php @@ -157,11 +157,6 @@ class GalaxyClusterRelation extends AppModel return array_unique(array_merge($existingRelationships, $objectRelationships)); } - public function deleteRelations($conditions) - { - $this->deleteAll($conditions, false, false); - } - /** * saveRelations * From 582658545450149fb66c104bcd4ef1bc74ac97f3 Mon Sep 17 00:00:00 2001 From: Jakub Onderka Date: Sun, 27 Mar 2022 18:53:50 +0200 Subject: [PATCH 019/120] chg: [test] Try to use virtualenv from system packages --- .github/workflows/main.yml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index e9319a4d3..e5ca1fa09 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -68,12 +68,11 @@ jobs: # hotfix due to: https://bugs.php.net/bug.php?id=81640 TODO: remove after libpcre2-8-0:10.36 gets to stable channel sudo apt-get --fix-broken install fi - sudo apt-get -y install curl python3 python3-pip python3-nose apache2 libapache2-mod-php$php_version + sudo apt-get -y install curl python3 python3-pip python3-virtualenv apache2 libapache2-mod-php$php_version # Runs a set of commands using the runners shell - name: Install deps run: | - sudo pip3 install virtualenv # virtualenv must be instaled from pip and not from ubuntu packages sudo chown $USER:www-data $HOME/.composer pushd app sudo -H -u $USER php composer.phar install --no-progress From 5167e4090f62045d90919cf6860a1c9cf251358a Mon Sep 17 00:00:00 2001 From: Jakub Onderka Date: Sun, 27 Mar 2022 19:15:10 +0200 Subject: [PATCH 020/120] chg: [galaxy] Simplify code for fetching galaxy cluster --- app/Model/GalaxyCluster.php | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/app/Model/GalaxyCluster.php b/app/Model/GalaxyCluster.php index ef62bea99..dc609a808 100644 --- a/app/Model/GalaxyCluster.php +++ b/app/Model/GalaxyCluster.php @@ -109,10 +109,10 @@ class GalaxyCluster extends AppModel if (!isset($cluster['published'])) { $cluster['published'] = false; } - if (!isset($cluster['authors']) || $cluster['authors'] === null) { + if (!isset($cluster['authors'])) { $cluster['authors'] = ''; } elseif (is_array($cluster['authors'])) { - $cluster['authors'] = json_encode($cluster['authors']); + $cluster['authors'] = JsonTool::encode($cluster['authors']); } return true; } @@ -121,24 +121,24 @@ class GalaxyCluster extends AppModel { foreach ($results as $k => $result) { if (isset($result[$this->alias]['authors'])) { - $results[$k][$this->alias]['authors'] = json_decode($results[$k][$this->alias]['authors'], true); + $results[$k][$this->alias]['authors'] = json_decode($result[$this->alias]['authors'], true); } - if (isset($result[$this->alias]['distribution']) && $results[$k][$this->alias]['distribution'] != 4) { + if (isset($result[$this->alias]['distribution']) && $result[$this->alias]['distribution'] != 4) { unset($results[$k]['SharingGroup']); } - if (isset($result[$this->alias]['org_id']) && $results[$k][$this->alias]['org_id'] == 0) { + if (isset($result[$this->alias]['org_id']) && $result[$this->alias]['org_id'] == 0) { if (isset($results[$k]['Org'])) { $results[$k]['Org'] = Organisation::GENERIC_MISP_ORGANISATION; } } - if (isset($result[$this->alias]['orgc_id']) && $results[$k][$this->alias]['orgc_id'] == 0) { + if (isset($result[$this->alias]['orgc_id']) && $result[$this->alias]['orgc_id'] == 0) { if (isset($results[$k]['Orgc'])) { $results[$k]['Orgc'] = Organisation::GENERIC_MISP_ORGANISATION; } } if (!empty($result['GalaxyClusterRelation'])) { - foreach ($results[$k]['GalaxyClusterRelation'] as $i => $relation) { + foreach ($result['GalaxyClusterRelation'] as $i => $relation) { if (isset($relation['distribution']) && $relation['distribution'] != 4) { unset($results[$k]['GalaxyClusterRelation'][$i]['SharingGroup']); } From 8f5ed8891edbdbd1e8badbeeecb7d0f7106415a7 Mon Sep 17 00:00:00 2001 From: Jakub Onderka Date: Sun, 27 Mar 2022 21:47:46 +0200 Subject: [PATCH 021/120] chg: [ui] Remove two inline onclick --- .../Elements/Events/View/event_contents.ctp | 12 ++++++------ app/webroot/js/misp.js | 18 +++++++++++++----- 2 files changed, 19 insertions(+), 11 deletions(-) diff --git a/app/View/Elements/Events/View/event_contents.ctp b/app/View/Elements/Events/View/event_contents.ctp index 50cbb43c0..0580f98a4 100644 --- a/app/View/Elements/Events/View/event_contents.ctp +++ b/app/View/Elements/Events/View/event_contents.ctp @@ -5,10 +5,10 @@ - - -
-
+
+
- 1) echo $this->element('pivot'); ?> + 1) echo $this->element('pivot'); ?>
@@ -60,7 +60,7 @@
- element('eventattribute'); ?> + element('eventattribute'); ?>
diff --git a/app/webroot/js/misp.js b/app/webroot/js/misp.js index f8fabb2d8..f888ce714 100644 --- a/app/webroot/js/misp.js +++ b/app/webroot/js/misp.js @@ -2168,14 +2168,14 @@ function quickFilter(passedArgs, url) { function runIndexFilter(element) { var dataFields = $(element).data(); - for (var k in $(element).data()) { + for (var k in dataFields) { if (k in passedArgsArray) { delete(passedArgsArray[k]); } else { passedArgsArray[k] = dataFields[k]; } } - url = here; + var url = here; for (var key in passedArgsArray) { url += "/" + key + ":" + passedArgsArray[key]; } @@ -4211,19 +4211,27 @@ function selectAllInbetween(last, current) { $('#eventToggleButtons button').click(function() { var element = $(this).data('toggle-type'); var $button = $(this).children('span'); + var $element = $('#' + element + '_div'); if ($button.hasClass('fa-minus')) { $button.addClass('fa-plus'); $button.removeClass('fa-minus'); - $('#' + element + '_div').hide(); + $element.hide(); } else { $button.removeClass('fa-plus'); $button.addClass('fa-minus'); - $('#' + element + '_div').show(); + $element.show(); + + // Special cases when another action must be made + if (element === 'eventtimeline') { + enable_timeline(); + } else if (element === 'eventgraph') { + enable_interactive_graph(); + } var loadUrl = $(this).data('load-url'); if (loadUrl) { $.get(loadUrl, function(data) { - $('#' + element + '_div').html(data); + $element.html(data); }).fail(xhrFailCallback); } } From 990fc207828aa55c5463cacd8e2f2937ebe7f0e1 Mon Sep 17 00:00:00 2001 From: Jakub Onderka Date: Sun, 27 Mar 2022 21:48:55 +0200 Subject: [PATCH 022/120] chg: [ui] Remove useless spaces from HTML code --- .../ListTopBar/element_embedded.ctp | 5 ++--- .../genericElements/ListTopBar/element_simple.ctp | 15 ++++++++++----- 2 files changed, 12 insertions(+), 8 deletions(-) diff --git a/app/View/Elements/genericElements/ListTopBar/element_embedded.ctp b/app/View/Elements/genericElements/ListTopBar/element_embedded.ctp index c1b2d0472..e46ba809f 100644 --- a/app/View/Elements/genericElements/ListTopBar/element_embedded.ctp +++ b/app/View/Elements/genericElements/ListTopBar/element_embedded.ctp @@ -9,7 +9,7 @@ if (!isset($data['requirement']) || $data['requirement']) { } $onClickParams = implode(',', $onClickParams); $onClick = sprintf( - 'onClick = "%s%s"', + 'onclick="%s%s"', (empty($data['url'])) ? 'event.preventDefault();' : '', (!empty($data['onClick']) ? sprintf( '%s(%s)', @@ -28,7 +28,6 @@ if (!isset($data['requirement']) || $data['requirement']) { ); } } - $dataFields = implode(' ', $dataFields); echo sprintf( '
  • %s%s%s
  • ', empty($data['class']) ? '' : h($data['class']), @@ -36,7 +35,7 @@ if (!isset($data['requirement']) || $data['requirement']) { empty($data['id']) ? '' : h($data['id']), empty($data['url']) ? '#' : h($data['url']), // prevent default is passed if the url is not set empty($onClick) ? '' : $onClick, // pass $data['onClick'] for the function name to call and $data['onClickParams'] for the parameter list - empty($dataFields) ? '' : $dataFields, + empty($dataFields) ? '' : implode(' ', $dataFields), empty($data['title']) ? '' : sprintf('title="%s"', h($data['title'])), !empty($data['text']) ? '' : (!empty($data['title']) ? sprintf('aria-label="%s"', h($data['title'])) : ''), empty($data['fa-icon']) ? '' : sprintf('', $data['fa-icon']), // this has to be sanitised beforehand! diff --git a/app/View/Elements/genericElements/ListTopBar/element_simple.ctp b/app/View/Elements/genericElements/ListTopBar/element_simple.ctp index d89c4235b..2a4720bfb 100644 --- a/app/View/Elements/genericElements/ListTopBar/element_simple.ctp +++ b/app/View/Elements/genericElements/ListTopBar/element_simple.ctp @@ -32,15 +32,20 @@ ); } } - $dataFields = implode(' ', $dataFields); + + $classes = ['btn', 'btn-small']; + $classes[] = empty($data['active']) ? 'btn-inverse' : 'btn-primary'; + if (!empty($data['class'])) { + $classes[] = $data['class']; + } + echo sprintf( - '%s%s%s %s', - empty($data['class']) ? '' : h($data['class']), - empty($data['active']) ? 'btn-inverse' : 'btn-primary', // Change the default class for highlighted/active toggles here + '%s%s%s %s', + implode(' ', $classes), empty($data['id']) ? '' : 'id="' . h($data['id']) . '"', empty($data['url']) ? '#' : h($data['url']), // prevent default is passed if the url is not set empty($onClick) ? '' : $onClick, // pass $data['onClick'] for the function name to call and $data['onClickParams'] for the parameter list - empty($dataFields) ? '' : $dataFields, + empty($dataFields) ? '' : implode(' ', $dataFields), empty($data['title']) ? '' : sprintf('title="%s"', h($data['title'])), empty($data['style']) ? '' : sprintf('style="%s"', h($data['style'])), !empty($data['text']) ? '' : (!empty($data['title']) ? sprintf('aria-label="%s"', h($data['title'])) : ''), From 0feeefdc2095256c45fe45a3413a6b728c7a300c Mon Sep 17 00:00:00 2001 From: Jakub Onderka Date: Sun, 27 Mar 2022 21:49:56 +0200 Subject: [PATCH 023/120] chg: [UI] Remove three inline onclick from code --- app/View/EventReports/ajax/indexForEvent.ctp | 23 +++++++++----------- 1 file changed, 10 insertions(+), 13 deletions(-) diff --git a/app/View/EventReports/ajax/indexForEvent.ctp b/app/View/EventReports/ajax/indexForEvent.ctp index 127c2ed35..86596ff76 100644 --- a/app/View/EventReports/ajax/indexForEvent.ctp +++ b/app/View/EventReports/ajax/indexForEvent.ctp @@ -13,29 +13,29 @@ 'type' => 'simple', 'children' => array( array( - 'onClick' => 'openGenericModal', - 'onClickParams' => [$baseurl . '/eventReports/add/' . h($event_id)], + 'url' => $baseurl . '/eventReports/add/' . h($event_id), 'active' => true, 'text' => __('Add Event Report'), 'fa-icon' => 'plus', + 'class' => 'modal-open', 'requirement' => $canModify, ), array( - 'onClick' => 'openGenericModal', - 'onClickParams' => [$baseurl . '/eventReports/importReportFromUrl/' . h($event_id)], + 'url' => $baseurl . '/eventReports/importReportFromUrl/' . h($event_id), 'active' => true, 'text' => __('Import from URL'), 'title' => __('Content for this URL will be downloaded and converted to Markdown'), 'fa-icon' => 'link', + 'class' => 'modal-open', 'requirement' => $canModify && $importModuleEnabled, ), array( - 'onClick' => 'openGenericModal', - 'onClickParams' => [$baseurl . '/eventReports/reportFromEvent/' . h($event_id)], + 'url' => $baseurl . '/eventReports/reportFromEvent/' . h($event_id), 'active' => true, 'text' => __('Generate report from Event'), 'title' => __('Based on filters, create a report summarizing the event'), 'fa-icon' => 'list-alt', + 'class' => 'modal-open', 'requirement' => $canModify, ), ) @@ -115,11 +115,10 @@ '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']; + 'function' => function ($row, $options) use ($me) { + return ($me['Role']['perm_site_admin'] || $me['org_id'] == $options['datapath']['orgc']) && !$options['datapath']['deleted']; }, 'options' => array( - 'me' => $me, 'datapath' => array( 'orgc' => 'EventReport.orgc_id', 'deleted' => 'EventReport.deleted' @@ -135,11 +134,10 @@ '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']; + 'function' => function ($row, $options) use ($me) { + return ($me['perm_site_admin'] || $me['org_id'] == $options['datapath']['orgc']) && $options['datapath']['deleted']; }, 'options' => array( - 'me' => $me, 'datapath' => array( 'orgc' => 'EventReport.orgc_id', 'deleted' => 'EventReport.deleted' @@ -152,7 +150,6 @@ )); ?>
    - diff --git a/app/View/Elements/genericElements/SideMenu/side_menu.ctp b/app/View/Elements/genericElements/SideMenu/side_menu.ctp index c1d7bece5..fb710aecb 100644 --- a/app/View/Elements/genericElements/SideMenu/side_menu.ctp +++ b/app/View/Elements/genericElements/SideMenu/side_menu.ctp @@ -98,10 +98,13 @@ $divider = $this->element('/genericElements/SideMenu/side_menu_divider'); 'url' => $baseurl . '/events/edit/' . $eventId, 'text' => __('Edit Event') )); - echo $this->element('/genericElements/SideMenu/side_menu_post_link', array( + echo $this->element('/genericElements/SideMenu/side_menu_link', array( 'url' => $baseurl . '/events/delete/' . $eventId, 'text' => __('Delete Event'), - 'message' => __('Are you sure you want to delete event #%s?', $eventId) + 'onClick' => array( + 'function' => 'deleteEventPopup', + 'params' => [$eventId] + ), )); echo $this->element('/genericElements/SideMenu/side_menu_link', array( 'element_id' => 'addAttribute', diff --git a/app/webroot/js/misp.js b/app/webroot/js/misp.js index f888ce714..4dc0458e0 100644 --- a/app/webroot/js/misp.js +++ b/app/webroot/js/misp.js @@ -899,10 +899,11 @@ function multiSelectDeleteEvents() { } } }); - $.get(baseurl + "/events/delete/" + JSON.stringify(selected), function(data) { - $("#confirmation_box").html(data); - openPopup("#confirmation_box"); - }).fail(xhrFailCallback); + deleteEventPopup(JSON.stringify(selected)); +} + +function deleteEventPopup(eventId) { + $.get(baseurl + "/events/delete/" + eventId, openConfirmation).fail(xhrFailCallback); } function multiSelectExportEvents() { @@ -1673,6 +1674,12 @@ function templateElementFileCategoryChange(category) { } } +function openConfirmation(data) { + var $box = $("#confirmation_box"); + $box.html(data); + openPopup($box); +} + function openPopup(id, adjust_layout, callback) { var $id = $(id); adjust_layout = adjust_layout === undefined ? true : adjust_layout; From 9140ece0d14c503e77b4640013840db5bdfa24fa Mon Sep 17 00:00:00 2001 From: Jakub Onderka Date: Sun, 27 Mar 2022 22:22:29 +0200 Subject: [PATCH 025/120] chg: [UI] Cleanup code for event confirmation dialog --- .../Events/ajax/eventDeleteConfirmationForm.ctp | 15 ++++++--------- app/webroot/js/misp.js | 13 ++----------- 2 files changed, 8 insertions(+), 20 deletions(-) diff --git a/app/View/Events/ajax/eventDeleteConfirmationForm.ctp b/app/View/Events/ajax/eventDeleteConfirmationForm.ctp index 3c65d8fbc..fc3907cb2 100644 --- a/app/View/Events/ajax/eventDeleteConfirmationForm.ctp +++ b/app/View/Events/ajax/eventDeleteConfirmationForm.ctp @@ -1,23 +1,22 @@
    Form->create('Event', array('style' => 'margin:0px;', 'id' => 'PromptForm', 'url' => $baseurl . '/events/delete')); - echo $this->Form->hidden('id'); + echo $this->Form->hidden('id'); ?>
    1) { - $message .= count($idArray) . ' Events?'; + $message = __('Are you sure you want to delete %s events?', count($idArray)); } else { - $message .= __(' Event #') . $idArray[0] . '?'; + $message = __('Are you sure you want to delete event #%s?', $idArray[0]); } ?> -

    +

    @@ -27,7 +26,5 @@
    - +
    -Form->end(); -?> +Form->end(); ?>
    diff --git a/app/webroot/js/misp.js b/app/webroot/js/misp.js index 4dc0458e0..03c04d50c 100644 --- a/app/webroot/js/misp.js +++ b/app/webroot/js/misp.js @@ -124,11 +124,7 @@ function publishPopup(id, type, scope) { if (type == "publish") action = "publish"; if (type == "unpublish") action = "unpublish"; if (type == "sighting") action = "publishSightings"; - var destination = 'attributes'; - $.get(baseurl + "/" + scope + "/" + action + "/" + id, function(data) { - $("#confirmation_box").html(data); - openPopup("#confirmation_box"); - }).fail(xhrFailCallback); + $.get(baseurl + "/" + scope + "/" + action + "/" + id, openConfirmation).fail(xhrFailCallback); } function delegatePopup(id) { @@ -170,7 +166,7 @@ function screenshotPopup(url, title) { $("#gray_out").fadeIn(); } -function submitPublish(id, type) { +function submitPublish() { $("#PromptForm").submit(); } @@ -1060,11 +1056,6 @@ function submitMassTaxonomyTag() { $('#PromptForm').submit(); } -function submitMassEventDelete() { - $('#PromptForm').trigger('submit'); - event.preventDefault(); -} - function getSelected() { var selected = []; $(".select_attribute").each(function() { From f636c4e94da548f5e9787a7e091b46aea574680e Mon Sep 17 00:00:00 2001 From: Jakub Onderka Date: Mon, 28 Mar 2022 12:05:45 +0200 Subject: [PATCH 026/120] fix: [UI] Add missing onclick prevent default --- app/View/Elements/Events/eventIndexTable.ctp | 2 +- app/View/Elements/genericElements/SideMenu/side_menu_link.ctp | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/app/View/Elements/Events/eventIndexTable.ctp b/app/View/Elements/Events/eventIndexTable.ctp index a623b13a5..3ad19cb0c 100644 --- a/app/View/Elements/Events/eventIndexTable.ctp +++ b/app/View/Elements/Events/eventIndexTable.ctp @@ -210,7 +210,7 @@ ?> " title="" aria-label=""> ', __('Delete'), __('Delete'), $eventId); + echo sprintf('', __('Delete'), __('Delete'), $eventId); endif; ?> " title="" aria-label=""> diff --git a/app/View/Elements/genericElements/SideMenu/side_menu_link.ctp b/app/View/Elements/genericElements/SideMenu/side_menu_link.ctp index 331fd9139..068bf7787 100644 --- a/app/View/Elements/genericElements/SideMenu/side_menu_link.ctp +++ b/app/View/Elements/genericElements/SideMenu/side_menu_link.ctp @@ -44,7 +44,7 @@ if (!empty($onClick)) { $params .= "'" . h($param) . "'"; } } - $a .= sprintf(' onclick="%s(%s)"', $onClick['function'], $params); + $a .= sprintf(' onclick="event.preventDefault();%s(%s)"', $onClick['function'], $params); } if (!empty($download)) { $a .= ' download="' . h($download) . '"'; From 86cfcefb10a20afeaa3fa90d5c57f919b84674f0 Mon Sep 17 00:00:00 2001 From: Jakub Onderka Date: Mon, 28 Mar 2022 12:10:34 +0200 Subject: [PATCH 027/120] fix: [UI] Normalize publish popup --- app/View/Elements/Events/eventIndexTable.ctp | 2 +- app/webroot/js/misp.js | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/app/View/Elements/Events/eventIndexTable.ctp b/app/View/Elements/Events/eventIndexTable.ctp index 3ad19cb0c..ec44e3e62 100644 --- a/app/View/Elements/Events/eventIndexTable.ctp +++ b/app/View/Elements/Events/eventIndexTable.ctp @@ -203,7 +203,7 @@ Form->postLink('', array('action' => 'alert', $eventId), array('class' => 'black fa fa-upload', 'title' => __('Publish Event'), 'aria-label' => __('Publish Event')), __('Are you sure this event is complete and everyone should be informed?')); + echo sprintf('', __('Publish Event'), __('Publish Event'), $eventId); } if ($isSiteAdmin || ($isAclModify && $event['Event']['user_id'] == $me['id']) || ($isAclModifyOrg && $event['Event']['orgc_id'] == $me['org_id'])): diff --git a/app/webroot/js/misp.js b/app/webroot/js/misp.js index 03c04d50c..853bb5e03 100644 --- a/app/webroot/js/misp.js +++ b/app/webroot/js/misp.js @@ -121,9 +121,9 @@ function flexibleAddSighting(clicked, type, attribute_id, event_id, placement) { function publishPopup(id, type, scope) { scope = scope === undefined ? 'events' : scope; var action = "alert"; - if (type == "publish") action = "publish"; - if (type == "unpublish") action = "unpublish"; - if (type == "sighting") action = "publishSightings"; + if (type === "publish") action = "publish"; + if (type === "unpublish") action = "unpublish"; + if (type === "sighting") action = "publishSightings"; $.get(baseurl + "/" + scope + "/" + action + "/" + id, openConfirmation).fail(xhrFailCallback); } From e385e71551978c6f51704ca25b085d602f22786b Mon Sep 17 00:00:00 2001 From: Jakub Onderka Date: Mon, 28 Mar 2022 12:44:56 +0200 Subject: [PATCH 028/120] fix: [UI] Avoid calling submitPublish() JS method --- app/View/EventBlocklists/mass_delete.ctp | 12 +++++------- .../Events/ajax/eventPublishConfirmationForm.ctp | 6 ++---- app/View/Events/ajax/toggle_correlation.ctp | 6 ++---- app/View/Feeds/ajax/feedToggleConfirmation.ctp | 6 ++---- .../GalaxyClusters/ajax/publishConfirmationForm.ctp | 6 ++---- app/webroot/js/misp.js | 4 ---- 6 files changed, 13 insertions(+), 27 deletions(-) diff --git a/app/View/EventBlocklists/mass_delete.ctp b/app/View/EventBlocklists/mass_delete.ctp index 38f7f87a3..2fb591eb3 100644 --- a/app/View/EventBlocklists/mass_delete.ctp +++ b/app/View/EventBlocklists/mass_delete.ctp @@ -2,9 +2,9 @@ Form->create('EventBlocklist', array('style' => 'margin:0px;', 'id' => 'PromptForm')); echo $this->Form->input('ids', array( - 'type' => 'hidden', - 'div' => 'hidden', - 'value' => json_encode($event_ids) + 'type' => 'hidden', + 'div' => 'hidden', + 'value' => json_encode($event_ids), )); ?> @@ -13,7 +13,7 @@ @@ -23,7 +23,5 @@
    - Yes +
    - Form->end(); - ?> + Form->end(); ?> diff --git a/app/View/Events/ajax/eventPublishConfirmationForm.ctp b/app/View/Events/ajax/eventPublishConfirmationForm.ctp index 6da745041..10bfd4d3f 100644 --- a/app/View/Events/ajax/eventPublishConfirmationForm.ctp +++ b/app/View/Events/ajax/eventPublishConfirmationForm.ctp @@ -25,7 +25,7 @@ @@ -35,7 +35,5 @@
    - +
    - Form->end(); - ?> + Form->end(); ?> diff --git a/app/View/Events/ajax/toggle_correlation.ctp b/app/View/Events/ajax/toggle_correlation.ctp index c2c7d3619..7b2d31aa9 100644 --- a/app/View/Events/ajax/toggle_correlation.ctp +++ b/app/View/Events/ajax/toggle_correlation.ctp @@ -17,7 +17,7 @@ @@ -27,7 +27,5 @@
    - +
    - Form->end(); - ?> + Form->end(); ?> diff --git a/app/View/Feeds/ajax/feedToggleConfirmation.ctp b/app/View/Feeds/ajax/feedToggleConfirmation.ctp index facc1e4c0..ba5160175 100644 --- a/app/View/Feeds/ajax/feedToggleConfirmation.ctp +++ b/app/View/Feeds/ajax/feedToggleConfirmation.ctp @@ -8,7 +8,7 @@ @@ -18,7 +18,5 @@
    - Yes +
    - Form->end(); - ?> + Form->end(); ?> diff --git a/app/View/GalaxyClusters/ajax/publishConfirmationForm.ctp b/app/View/GalaxyClusters/ajax/publishConfirmationForm.ctp index e18935307..3f5a2cef4 100644 --- a/app/View/GalaxyClusters/ajax/publishConfirmationForm.ctp +++ b/app/View/GalaxyClusters/ajax/publishConfirmationForm.ctp @@ -22,7 +22,7 @@ @@ -32,7 +32,5 @@
    - +
    - Form->end(); - ?> + Form->end(); ?> diff --git a/app/webroot/js/misp.js b/app/webroot/js/misp.js index 853bb5e03..f54823f76 100644 --- a/app/webroot/js/misp.js +++ b/app/webroot/js/misp.js @@ -166,10 +166,6 @@ function screenshotPopup(url, title) { $("#gray_out").fadeIn(); } -function submitPublish() { - $("#PromptForm").submit(); -} - function editTemplateElement(type, id) { simplePopup(baseurl + "/template_elements/edit/" + type + "/" + id); } From 3adc05a01cc0b5a4fe9587a0bfbad63e979189f5 Mon Sep 17 00:00:00 2001 From: Jakub Onderka Date: Mon, 28 Mar 2022 12:47:49 +0200 Subject: [PATCH 029/120] fix: [UI] Avoid calling submitMassTaxonomyTag() JS method --- app/View/Taxonomies/ajax/taxonomy_mass_confirmation.ctp | 8 +++----- app/View/Taxonomies/ajax/taxonomy_mass_hide.ctp | 8 +++----- app/View/Taxonomies/ajax/taxonomy_mass_unhide.ctp | 8 +++----- app/webroot/js/misp.js | 4 ---- 4 files changed, 9 insertions(+), 19 deletions(-) diff --git a/app/View/Taxonomies/ajax/taxonomy_mass_confirmation.ctp b/app/View/Taxonomies/ajax/taxonomy_mass_confirmation.ctp index 756321443..83d8d8c80 100644 --- a/app/View/Taxonomies/ajax/taxonomy_mass_confirmation.ctp +++ b/app/View/Taxonomies/ajax/taxonomy_mass_confirmation.ctp @@ -16,7 +16,7 @@ @@ -27,11 +27,9 @@
    - +
    -Form->end(); -?> +Form->end(); ?> diff --git a/app/View/Taxonomies/ajax/taxonomy_mass_hide.ctp b/app/View/Taxonomies/ajax/taxonomy_mass_hide.ctp index c24290509..fa174f329 100644 --- a/app/View/Taxonomies/ajax/taxonomy_mass_hide.ctp +++ b/app/View/Taxonomies/ajax/taxonomy_mass_hide.ctp @@ -16,7 +16,7 @@ @@ -27,12 +27,10 @@
    - +
    -Form->end(); -?> +Form->end(); ?> diff --git a/app/View/Taxonomies/ajax/taxonomy_mass_unhide.ctp b/app/View/Taxonomies/ajax/taxonomy_mass_unhide.ctp index 942ddb086..ed3a15de0 100644 --- a/app/View/Taxonomies/ajax/taxonomy_mass_unhide.ctp +++ b/app/View/Taxonomies/ajax/taxonomy_mass_unhide.ctp @@ -16,7 +16,7 @@ @@ -27,12 +27,10 @@
    - +
    -Form->end(); -?> +Form->end(); ?> diff --git a/app/webroot/js/misp.js b/app/webroot/js/misp.js index f54823f76..be4c341e3 100644 --- a/app/webroot/js/misp.js +++ b/app/webroot/js/misp.js @@ -1048,10 +1048,6 @@ function unhideSelectedTags(taxonomy) { }).fail(xhrFailCallback); } -function submitMassTaxonomyTag() { - $('#PromptForm').submit(); -} - function getSelected() { var selected = []; $(".select_attribute").each(function() { From cebcd227df3f90eb538c0430e4c2608557fa3d54 Mon Sep 17 00:00:00 2001 From: Jakub Onderka Date: Mon, 28 Mar 2022 12:49:54 +0200 Subject: [PATCH 030/120] fix: [UI] Remove unused method submitQuickTag --- app/webroot/js/misp.js | 4 ---- 1 file changed, 4 deletions(-) diff --git a/app/webroot/js/misp.js b/app/webroot/js/misp.js index be4c341e3..921473de9 100644 --- a/app/webroot/js/misp.js +++ b/app/webroot/js/misp.js @@ -519,10 +519,6 @@ function activateField(type, id, field, event) { }); } -function submitQuickTag(form) { - $('#' + form).submit(); -} - //if someone clicks an inactive field, replace it with the hidden form field. Also, focus it and bind a focusout event, so that it gets saved if the user clicks away. //If a user presses enter, submit the form function postActivationScripts(name, type, id, field, event) { From b7d7fd8f28432fc124925d22e652d2d21cd1114e Mon Sep 17 00:00:00 2001 From: Jakub Onderka Date: Tue, 29 Mar 2022 12:30:36 +0200 Subject: [PATCH 031/120] fix: [UI] Typo --- .../Elements/Events/View/row_attribute.ctp | 34 +++++++++---------- app/webroot/js/misp.js | 18 +++++----- 2 files changed, 24 insertions(+), 28 deletions(-) diff --git a/app/View/Elements/Events/View/row_attribute.ctp b/app/View/Elements/Events/View/row_attribute.ctp index 255dc37c3..9a985fcc3 100644 --- a/app/View/Elements/Events/View/row_attribute.ctp +++ b/app/View/Elements/Events/View/row_attribute.ctp @@ -32,7 +32,7 @@ if (!empty($k)) { $tr_class .= ' row_' . h($k); } -$objectId = h($object['id']); +$objectId = (int) $object['id']; $quickEdit = function($fieldName) use ($editScope, $object, $event) { if ($object['deleted']) { @@ -143,9 +143,8 @@ $quickEdit = function($fieldName) use ($editScope, $object, $event) {
    - element( - 'ajaxTags', - array('attributeId' => $object['id'], + element('ajaxTags', array( + 'attributeId' => $objectId, 'tags' => $object['AttributeTag'], 'tagAccess' => ($isSiteAdmin || $mayModify), 'localTagAccess' => ($isSiteAdmin || $mayModify || $me['org_id'] == $event['Event']['org_id'] || (int)$me['org_id'] === Configure::read('MISP.host_org_id')), @@ -160,7 +159,7 @@ $quickEdit = function($fieldName) use ($editScope, $object, $event) { if (!empty($includeRelatedTags)) { $element = ''; if (!empty($object['RelatedTags'])) { - $element = $this->element('ajaxAttributeTags', array('attributeId' => $object['id'], 'attributeTags' => $object['RelatedTags'], 'tagAccess' => false)); + $element = $this->element('ajaxAttributeTags', array('attributeId' => $objectId, 'attributeTags' => $object['RelatedTags'], 'tagAccess' => false)); } echo sprintf( '
    %s
    ', @@ -169,15 +168,14 @@ $quickEdit = function($fieldName) use ($editScope, $object, $event) { ); } ?> - - + element('galaxyQuickViewNew', array( 'mayModify' => $mayModify, 'isAclTagger' => $isAclTagger, 'data' => (!empty($object['Galaxy']) ? $object['Galaxy'] : array()), 'event' => $event, - 'target_id' => $object['id'], + 'target_id' => $objectId, 'target_type' => 'attribute', )); ?> @@ -204,7 +202,7 @@ $quickEdit = function($fieldName) use ($editScope, $object, $event) { '; echo $this->element('Events/View/attribute_correlations', array( 'scope' => 'Attribute', @@ -354,25 +352,25 @@ $quickEdit = function($fieldName) use ($editScope, $object, $event) { if ($object['deleted']): if ($isSiteAdmin || $mayModify): ?> - - + + -   +   - + - + @@ -382,12 +380,12 @@ $quickEdit = function($fieldName) use ($editScope, $object, $event) { if ($isSiteAdmin || $mayModify): if (isset($modules) && isset($modules['types'][$object['type']])): ?> -   +   - + @@ -395,11 +393,11 @@ $quickEdit = function($fieldName) use ($editScope, $object, $event) { - + - + Date: Tue, 29 Mar 2022 17:17:48 +0200 Subject: [PATCH 032/120] chg: [UI] Remove onclick when adding tag --- app/View/Elements/ajaxTags.ctp | 21 ++++----------------- app/webroot/js/misp.js | 6 ++++++ 2 files changed, 10 insertions(+), 17 deletions(-) diff --git a/app/View/Elements/ajaxTags.ctp b/app/View/Elements/ajaxTags.ctp index c1cdda933..d925cc0f0 100644 --- a/app/View/Elements/ajaxTags.ctp +++ b/app/View/Elements/ajaxTags.ctp @@ -107,35 +107,23 @@ $buttonData = array(); if ($full) { $buttonData[] = sprintf( - '', + '', __('Add a tag'), __('Add a tag'), 'addTagButton btn btn-inverse noPrint', 'line-height:10px; padding: 2px;', - sprintf( - "popoverPopup(this, '%s%s', '%s', '%s');", - $id, - ($scope === 'event') ? '' : ('/' . $scope), - 'tags', - 'selectTaxonomy' - ), + $baseurl . '/tags/selectTaxonomy/' . $id . ($scope === 'event') ? '' : ('/' . $scope), ' +' ); } if ($full || $fullLocal) { $buttonData[] = sprintf( - '', + '', __('Add a local tag'), __('Add a local tag'), 'addLocalTagButton btn btn-inverse noPrint', 'line-height:10px; padding: 2px;', - sprintf( - "popoverPopup(this, '%s%s', '%s', '%s')", - $id, - ($scope === 'event') ? '' : ('/' . $scope), - 'tags', - 'selectTaxonomy/local:1' - ), + $baseurl . '/tags/selectTaxonomy/local:1/' . $id . ($scope === 'event') ? '' : ('/' . $scope), ' +' ); } @@ -181,4 +169,3 @@ $tagConflictData .= '
    '; } echo $tagConflictData; -?> diff --git a/app/webroot/js/misp.js b/app/webroot/js/misp.js index ab068366b..691062ecd 100644 --- a/app/webroot/js/misp.js +++ b/app/webroot/js/misp.js @@ -4779,6 +4779,12 @@ $(document.body).on('click', 'a.modal-open', function (e) { openGenericModal($(this).attr('href')); }); +$(document.body).on('click', '[data-popover-popup]', function (e) { + e.preventDefault(); + var url = $(this).data('popover-popup'); + popoverPopup(this, url); +}); + function queryEventLock(event_id, timestamp) { if (!document.hidden) { $.ajax({ From d85fbbaf95e43dfa8134427534c7b52b40bbe9af Mon Sep 17 00:00:00 2001 From: Jakub Onderka Date: Tue, 29 Mar 2022 17:20:51 +0200 Subject: [PATCH 033/120] chg: [UI] Simplify code for adding galaxies --- app/View/Attributes/index.ctp | 3 --- app/View/Elements/eventattribute.ctp | 3 --- app/View/Elements/galaxyQuickViewNew.ctp | 16 ++++++++-------- app/View/Events/ajax/ajaxGalaxies.ctp | 5 ----- app/View/TagCollections/get_row.ctp | 8 -------- app/View/TagCollections/index.ctp | 9 +-------- app/webroot/js/misp.js | 12 ------------ 7 files changed, 9 insertions(+), 47 deletions(-) diff --git a/app/View/Attributes/index.ctp b/app/View/Attributes/index.ctp index 407892a1e..506e5aedd 100755 --- a/app/View/Attributes/index.ctp +++ b/app/View/Attributes/index.ctp @@ -320,9 +320,6 @@ echo $this->element('/genericElements/SideMenu/side_menu', ['menuList' => 'event $('.screenshot').click(function() { screenshotPopup($(this).attr('src'), $(this).attr('title')); }); - $('.addGalaxy').click(function() { - addGalaxyListener(this); - }); $('.sightings_advanced_add').click(function() { var selected = []; var object_context = $(this).data('object-context'); diff --git a/app/View/Elements/eventattribute.ctp b/app/View/Elements/eventattribute.ctp index 210b89167..17cc76a0a 100644 --- a/app/View/Elements/eventattribute.ctp +++ b/app/View/Elements/eventattribute.ctp @@ -227,9 +227,6 @@ attributes or the appropriate distribution level. If you think there is a mistak var deleted = ; var includeRelatedTags = ; $(function() { - $('.addGalaxy').click(function() { - addGalaxyListener(this); - }); diff --git a/app/View/Elements/galaxyQuickViewNew.ctp b/app/View/Elements/galaxyQuickViewNew.ctp index 3ac0650c7..85fa0dfaf 100755 --- a/app/View/Elements/galaxyQuickViewNew.ctp +++ b/app/View/Elements/galaxyQuickViewNew.ctp @@ -123,21 +123,21 @@ echo $this->Form->end(); %s', - 'useCursorPointer btn btn-inverse addGalaxy', - h($target_type), - h($target_id), + '', + 'useCursorPointer btn btn-inverse', + $link, ' ' ); } if ($editButtonsLocalEnabled) { + $link = "$baseurl/galaxies/selectGalaxyNamespace/" . h($target_id) . "/" . h($target_type) . "/local:1"; echo sprintf( - '', - 'useCursorPointer btn btn-inverse addGalaxy', - h($target_type), - h($target_id), + '', + 'useCursorPointer btn btn-inverse', + $link, ' ' ); } diff --git a/app/View/Events/ajax/ajaxGalaxies.ctp b/app/View/Events/ajax/ajaxGalaxies.ctp index 989837067..73a37390f 100644 --- a/app/View/Events/ajax/ajaxGalaxies.ctp +++ b/app/View/Events/ajax/ajaxGalaxies.ctp @@ -14,9 +14,4 @@ echo $this->element('galaxyQuickViewNew', [ ?> diff --git a/app/View/TagCollections/get_row.ctp b/app/View/TagCollections/get_row.ctp index a52ff938e..7d196df1a 100644 --- a/app/View/TagCollections/get_row.ctp +++ b/app/View/TagCollections/get_row.ctp @@ -1,11 +1,3 @@ element('TagCollections/index_row'); ?> - - diff --git a/app/View/TagCollections/index.ctp b/app/View/TagCollections/index.ctp index 5253edd6e..7d4d0195a 100755 --- a/app/View/TagCollections/index.ctp +++ b/app/View/TagCollections/index.ctp @@ -19,11 +19,4 @@ )); echo $this->element('/genericElements/SideMenu/side_menu', array('menuList' => 'tag-collections', 'menuItem' => 'index')); -?> - + diff --git a/app/webroot/js/misp.js b/app/webroot/js/misp.js index 691062ecd..b7140adb8 100644 --- a/app/webroot/js/misp.js +++ b/app/webroot/js/misp.js @@ -4221,18 +4221,6 @@ $('#eventToggleButtons button').click(function() { } }); -function addGalaxyListener(id) { - var target_type = $(id).data('target-type'); - var target_id = $(id).data('target-id'); - var local = $(id).data('local'); - if (local) { - local = 1; - } else { - local = 0; - } - popoverPopup(id, target_id + '/' + target_type + '/local:' + local, 'galaxies', 'selectGalaxyNamespace'); -} - function quickSubmitGalaxyForm(cluster_ids, additionalData) { cluster_ids = cluster_ids === null ? [] : cluster_ids; var target_id = additionalData['target_id']; From 84129c3bf9d749ea56d5d52646741862591c0ddf Mon Sep 17 00:00:00 2001 From: Jakub Onderka Date: Tue, 29 Mar 2022 17:25:03 +0200 Subject: [PATCH 034/120] fix: [UI] Remove popover from URL --- app/View/Elements/ajaxTags.ctp | 4 ++-- app/View/Elements/galaxyQuickViewNew.ctp | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/app/View/Elements/ajaxTags.ctp b/app/View/Elements/ajaxTags.ctp index d925cc0f0..c575cb37b 100644 --- a/app/View/Elements/ajaxTags.ctp +++ b/app/View/Elements/ajaxTags.ctp @@ -112,7 +112,7 @@ __('Add a tag'), 'addTagButton btn btn-inverse noPrint', 'line-height:10px; padding: 2px;', - $baseurl . '/tags/selectTaxonomy/' . $id . ($scope === 'event') ? '' : ('/' . $scope), + '/tags/selectTaxonomy/' . $id . ($scope === 'event') ? '' : ('/' . $scope), ' +' ); } @@ -123,7 +123,7 @@ __('Add a local tag'), 'addLocalTagButton btn btn-inverse noPrint', 'line-height:10px; padding: 2px;', - $baseurl . '/tags/selectTaxonomy/local:1/' . $id . ($scope === 'event') ? '' : ('/' . $scope), + '/tags/selectTaxonomy/local:1/' . $id . ($scope === 'event') ? '' : ('/' . $scope), ' +' ); } diff --git a/app/View/Elements/galaxyQuickViewNew.ctp b/app/View/Elements/galaxyQuickViewNew.ctp index 85fa0dfaf..77134f53d 100755 --- a/app/View/Elements/galaxyQuickViewNew.ctp +++ b/app/View/Elements/galaxyQuickViewNew.ctp @@ -123,7 +123,7 @@ echo $this->Form->end(); %s', 'useCursorPointer btn btn-inverse', @@ -133,7 +133,7 @@ if ($editButtonsEnabled) { } if ($editButtonsLocalEnabled) { - $link = "$baseurl/galaxies/selectGalaxyNamespace/" . h($target_id) . "/" . h($target_type) . "/local:1"; + $link = "/galaxies/selectGalaxyNamespace/" . h($target_id) . "/" . h($target_type) . "/local:1"; echo sprintf( '', 'useCursorPointer btn btn-inverse', From 9b92b502cc83756c1217aa1a318d1e35f1234494 Mon Sep 17 00:00:00 2001 From: Jakub Onderka Date: Tue, 29 Mar 2022 17:37:59 +0200 Subject: [PATCH 035/120] chg: [UI] Remove two onclick --- app/View/Elements/Events/View/row_object.ctp | 2 +- app/View/Elements/eventattributetoolbar.ctp | 10 ++++++---- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/app/View/Elements/Events/View/row_object.ctp b/app/View/Elements/Events/View/row_object.ctp index 1f462e4ba..9c2bb426e 100644 --- a/app/View/Elements/Events/View/row_object.ctp +++ b/app/View/Elements/Events/View/row_object.ctp @@ -161,5 +161,5 @@ 'child' => $attrKey == $lastElement ? 'last' : true )); } - echo ''; + echo ''; } diff --git a/app/View/Elements/eventattributetoolbar.ctp b/app/View/Elements/eventattributetoolbar.ctp index 18937d5ec..aef7f1c2a 100644 --- a/app/View/Elements/eventattributetoolbar.ctp +++ b/app/View/Elements/eventattributetoolbar.ctp @@ -71,8 +71,9 @@ 'title' => __('Tag selected Attributes'), 'class' => 'mass-select hidden', 'fa-icon' => 'tag', - 'onClick' => 'popoverPopup', - 'onClickParams' => array('this', 'selected/attribute', 'tags', 'selectTaxonomy') + 'data' => [ + 'popover-popup' => '/tags/selectTaxonomy/selected/attribute', + ], ), array( 'id' => 'multi-galaxy-button', @@ -80,8 +81,9 @@ 'class' => 'mass-select hidden', 'fa-icon' => 'rebel', 'fa-source' => 'fab', - 'onClick' => 'popoverPopup', - 'onClickParams' => array('this', 'selected/attribute/eventid:' . $eventId, 'galaxies', 'selectGalaxyNamespace') + 'data' => [ + 'popover-popup' => '/galaxies/selectGalaxyNamespace/selected/attribute/eventid:' . $eventId, + ], ), array( 'id' => 'group-into-object-button', From c155dabc7b13128f6b6908b6782d69323b61965a Mon Sep 17 00:00:00 2001 From: Jakub Onderka Date: Tue, 29 Mar 2022 18:50:22 +0200 Subject: [PATCH 036/120] fix: [UI] Popovers --- app/View/Elements/Events/View/row_object.ctp | 2 +- app/View/Elements/ajaxTags.ctp | 18 ++++++++---------- app/View/Elements/eventattributetoolbar.ctp | 4 ++-- app/View/Elements/galaxyQuickViewNew.ctp | 8 ++++---- app/webroot/css/main.css | 4 ++-- app/webroot/js/misp.js | 19 ++++++++++++------- 6 files changed, 29 insertions(+), 26 deletions(-) diff --git a/app/View/Elements/Events/View/row_object.ctp b/app/View/Elements/Events/View/row_object.ctp index 9c2bb426e..5aabff3da 100644 --- a/app/View/Elements/Events/View/row_object.ctp +++ b/app/View/Elements/Events/View/row_object.ctp @@ -161,5 +161,5 @@ 'child' => $attrKey == $lastElement ? 'last' : true )); } - echo ''; + echo ''; } diff --git a/app/View/Elements/ajaxTags.ctp b/app/View/Elements/ajaxTags.ctp index c575cb37b..bdafaf6eb 100644 --- a/app/View/Elements/ajaxTags.ctp +++ b/app/View/Elements/ajaxTags.ctp @@ -107,24 +107,22 @@ $buttonData = array(); if ($full) { $buttonData[] = sprintf( - '', + '', __('Add a tag'), __('Add a tag'), - 'addTagButton btn btn-inverse noPrint', - 'line-height:10px; padding: 2px;', - '/tags/selectTaxonomy/' . $id . ($scope === 'event') ? '' : ('/' . $scope), - ' +' + 'addTagButton addButton btn btn-inverse noPrint', + $baseurl . '/tags/selectTaxonomy/' . $id . ($scope === 'event' ? '' : ('/' . $scope)), + ' ' ); } if ($full || $fullLocal) { $buttonData[] = sprintf( - '', + '', __('Add a local tag'), __('Add a local tag'), - 'addLocalTagButton btn btn-inverse noPrint', - 'line-height:10px; padding: 2px;', - '/tags/selectTaxonomy/local:1/' . $id . ($scope === 'event') ? '' : ('/' . $scope), - ' +' + 'addLocalTagButton addButton btn btn-inverse noPrint', + $baseurl . '/tags/selectTaxonomy/local:1/' . $id . ($scope === 'event' ? '' : ('/' . $scope)), + ' ' ); } if (!empty($buttonData)) { diff --git a/app/View/Elements/eventattributetoolbar.ctp b/app/View/Elements/eventattributetoolbar.ctp index aef7f1c2a..9476747d6 100644 --- a/app/View/Elements/eventattributetoolbar.ctp +++ b/app/View/Elements/eventattributetoolbar.ctp @@ -72,7 +72,7 @@ 'class' => 'mass-select hidden', 'fa-icon' => 'tag', 'data' => [ - 'popover-popup' => '/tags/selectTaxonomy/selected/attribute', + 'popover-popup' => $baseurl . '/tags/selectTaxonomy/selected/attribute', ], ), array( @@ -82,7 +82,7 @@ 'fa-icon' => 'rebel', 'fa-source' => 'fab', 'data' => [ - 'popover-popup' => '/galaxies/selectGalaxyNamespace/selected/attribute/eventid:' . $eventId, + 'popover-popup' => $baseurl . '/galaxies/selectGalaxyNamespace/selected/attribute/eventid:' . $eventId, ], ), array( diff --git a/app/View/Elements/galaxyQuickViewNew.ctp b/app/View/Elements/galaxyQuickViewNew.ctp index 77134f53d..04b819de5 100755 --- a/app/View/Elements/galaxyQuickViewNew.ctp +++ b/app/View/Elements/galaxyQuickViewNew.ctp @@ -123,20 +123,20 @@ echo $this->Form->end(); %s', - 'useCursorPointer btn btn-inverse', + 'useCursorPointer addButton btn btn-inverse', $link, ' ' ); } if ($editButtonsLocalEnabled) { - $link = "/galaxies/selectGalaxyNamespace/" . h($target_id) . "/" . h($target_type) . "/local:1"; + $link = "$baseurl/galaxies/selectGalaxyNamespace/" . h($target_id) . "/" . h($target_type) . "/local:1"; echo sprintf( '', - 'useCursorPointer btn btn-inverse', + 'useCursorPointer addButton btn btn-inverse', $link, ' ' ); diff --git a/app/webroot/css/main.css b/app/webroot/css/main.css index a1af5ea46..f45b2f006 100644 --- a/app/webroot/css/main.css +++ b/app/webroot/css/main.css @@ -2056,14 +2056,14 @@ a.discrete { font-size: 12px; } -button.addGalaxy { +button.addButton { line-height: 10px; padding: 2px; margin-right: 5px; margin-top: 1px; } -button.addGalaxy .fa-plus { +button.addButton .fa-plus { font-size: 11px; line-height: 13px; } diff --git a/app/webroot/js/misp.js b/app/webroot/js/misp.js index b7140adb8..5a55e0fb5 100644 --- a/app/webroot/js/misp.js +++ b/app/webroot/js/misp.js @@ -1802,6 +1802,7 @@ function getPopup(id, context, target, admin, popupType) { } // Same as getPopup function but create a popover to populate first +// DEPRECATED function popoverPopup(clicked, id, context, target, admin) { var url = baseurl; if (typeof admin !== 'undefined' && admin != '') url+= "/admin"; @@ -1810,20 +1811,24 @@ function popoverPopup(clicked, id, context, target, admin) { } if (target != '') url += "/" + target; if (id != '') url += "/" + id; - var popover = openPopover(clicked, undefined); - $clicked = $(clicked); + popoverPopupNew(clicked, url); +} + +function popoverPopupNew(clicked, url) { + var $clicked = $(clicked); + var popover = openPopover($clicked, undefined); // actual request // $.ajax({ - dataType:"html", + dataType: "html", cache: false, - success:function (data) { + success: function (data) { if (popover.options.content !== data) { - popover.options.content = data; + popover.options.content = data; $clicked.popover('show'); } }, - error:function(jqXHR ) { + error: function(jqXHR) { var errorJSON = ''; try { errorJSON = JSON.parse(jqXHR.responseText); @@ -4770,7 +4775,7 @@ $(document.body).on('click', 'a.modal-open', function (e) { $(document.body).on('click', '[data-popover-popup]', function (e) { e.preventDefault(); var url = $(this).data('popover-popup'); - popoverPopup(this, url); + popoverPopupNew(this, url); }); function queryEventLock(event_id, timestamp) { From e589f8be933552c92711016f804a0ec8e4805756 Mon Sep 17 00:00:00 2001 From: Jakub Onderka Date: Tue, 29 Mar 2022 20:47:33 +0200 Subject: [PATCH 037/120] chg: [UI] Use same UI for adding tags --- app/View/Elements/ajaxTagCollectionTags.ctp | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/app/View/Elements/ajaxTagCollectionTags.ctp b/app/View/Elements/ajaxTagCollectionTags.ctp index 9492509da..b18288f4b 100644 --- a/app/View/Elements/ajaxTagCollectionTags.ctp +++ b/app/View/Elements/ajaxTagCollectionTags.ctp @@ -25,9 +25,10 @@ +', - sprintf("'%s/tag_collection', 'tags', 'selectTaxonomy'", h($tagCollection['TagCollection']['id'])) + '', + $url ); } echo $addTagButton; From cb643b4b895a74512c012710d7800f4da91c0f6d Mon Sep 17 00:00:00 2001 From: Jakub Onderka Date: Wed, 30 Mar 2022 09:15:29 +0200 Subject: [PATCH 038/120] chg: [UI] Use same UI for adding tags --- app/View/Elements/ajaxTags.ctp | 15 ++++++--------- 1 file changed, 6 insertions(+), 9 deletions(-) diff --git a/app/View/Elements/ajaxTags.ctp b/app/View/Elements/ajaxTags.ctp index bdafaf6eb..e94d028ab 100644 --- a/app/View/Elements/ajaxTags.ctp +++ b/app/View/Elements/ajaxTags.ctp @@ -88,7 +88,7 @@ $span_delete = ''; if ($full || ($fullLocal && $tag['Tag']['local'])) { $span_delete = sprintf( - 'x', + 'x', 'black-white tag useCursorPointer noPrint', __('Remove tag'), "button", @@ -126,10 +126,7 @@ ); } if (!empty($buttonData)) { - $tagData .= sprintf( - '%s', - implode(' ', $buttonData) - ); + $tagData .= implode('', $buttonData); } echo sprintf( '%s', @@ -142,11 +139,11 @@ $tagConflictData .= '
    '; foreach ($tagConflicts['global'] as $tagConflict) { $tagConflictData .= sprintf( - '%s
    ', + '%s
    ', h($tagConflict['conflict']) ); foreach ($tagConflict['tags'] as $tag) { - $tagConflictData .= sprintf('%s
    ', h($tag)); + $tagConflictData .= sprintf('%s
    ', h($tag)); } } $tagConflictData .= '
    '; @@ -157,11 +154,11 @@ $tagConflictData .= '
    '; foreach ($tagConflicts['local'] as $tagConflict) { $tagConflictData .= sprintf( - '%s
    ', + '%s
    ', h($tagConflict['conflict']) ); foreach ($tagConflict['tags'] as $tag) { - $tagConflictData .= sprintf('%s
    ', h($tag)); + $tagConflictData .= sprintf('%s
    ', h($tag)); } } $tagConflictData .= '
    '; From 464504fa1661348ba09d9e1f90747411b82d6bce Mon Sep 17 00:00:00 2001 From: Jakub Onderka Date: Wed, 30 Mar 2022 10:34:22 +0200 Subject: [PATCH 039/120] fix: [UI] Show context button --- app/View/Elements/Events/View/event_contents.ctp | 3 --- app/View/Elements/ajaxTags.ctp | 2 +- app/View/Events/ajax/ajaxGalaxies.ctp | 4 ---- app/webroot/js/misp.js | 16 +++++++++------- 4 files changed, 10 insertions(+), 15 deletions(-) diff --git a/app/View/Elements/Events/View/event_contents.ctp b/app/View/Elements/Events/View/event_contents.ctp index 0580f98a4..0c767dd07 100644 --- a/app/View/Elements/Events/View/event_contents.ctp +++ b/app/View/Elements/Events/View/event_contents.ctp @@ -66,7 +66,6 @@ diff --git a/app/webroot/js/misp.js b/app/webroot/js/misp.js index 5a55e0fb5..0b9576582 100644 --- a/app/webroot/js/misp.js +++ b/app/webroot/js/misp.js @@ -4102,23 +4102,25 @@ function feedFormUpdate() { } function setContextFields() { + if (typeof showContext === "undefined") { + showContext = false; + } + + var $button = $('#show_attribute_context'); if (showContext) { $('.context').show(); - $('#show_context').addClass("attribute_filter_text_active"); - $('#show_context').removeClass("attribute_filter_text"); + $button.removeClass("btn-inverse").addClass("btn-primary"); } else { $('.context').hide(); - $('#show_context').addClass("attribute_filter_text"); - $('#show_context').removeClass("attribute_filter_text_active"); + $button.removeClass("btn-primary").addClass("btn-inverse"); } } function toggleContextFields() { - if (!showContext) { - showContext = true; - } else { + if (typeof showContext === "undefined") { showContext = false; } + showContext = !showContext; setContextFields(); } From ab09c905d24a3336c7271d6571b2fb60bc58b5be Mon Sep 17 00:00:00 2001 From: Alexandre Dulaunoy Date: Thu, 31 Mar 2022 09:09:19 +0200 Subject: [PATCH 040/120] chg: [misp-galaxy] updated --- app/files/misp-galaxy | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/files/misp-galaxy b/app/files/misp-galaxy index 8c2a9af8b..4242732af 160000 --- a/app/files/misp-galaxy +++ b/app/files/misp-galaxy @@ -1 +1 @@ -Subproject commit 8c2a9af8b894de56403e5ed6f3d3006d04bfe875 +Subproject commit 4242732af18c873e192a8450bac466bd59742b91 From 8afcc6552b71aa6b2298cb603e40c975f7a38027 Mon Sep 17 00:00:00 2001 From: Sami Mokaddem Date: Thu, 31 Mar 2022 15:25:07 +0200 Subject: [PATCH 041/120] chg: [events:index] Usage of UUIDfor restSearchExport feature --- app/Controller/AppController.php | 2 +- app/View/Elements/Events/eventIndexTable.ctp | 2 +- app/webroot/js/misp.js | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/app/Controller/AppController.php b/app/Controller/AppController.php index 06a28cfe6..bdc05ac47 100755 --- a/app/Controller/AppController.php +++ b/app/Controller/AppController.php @@ -34,7 +34,7 @@ class AppController extends Controller public $helpers = array('OrgImg', 'FontAwesome', 'UserName'); - private $__queryVersion = '137'; + private $__queryVersion = '138'; public $pyMispVersion = '2.4.157'; public $phpmin = '7.2'; public $phprec = '7.4'; diff --git a/app/View/Elements/Events/eventIndexTable.ctp b/app/View/Elements/Events/eventIndexTable.ctp index ec44e3e62..97a94e8af 100644 --- a/app/View/Elements/Events/eventIndexTable.ctp +++ b/app/View/Elements/Events/eventIndexTable.ctp @@ -49,7 +49,7 @@ - + diff --git a/app/webroot/js/misp.js b/app/webroot/js/misp.js index 0b9576582..c89c9f417 100644 --- a/app/webroot/js/misp.js +++ b/app/webroot/js/misp.js @@ -895,7 +895,7 @@ function multiSelectExportEvents() { var selected = []; $(".select").each(function() { if ($(this).is(":checked")) { - var temp = $(this).data("id"); + var temp = $(this).data("uuid"); if (temp != null) { selected.push(temp); } From 5c9f2a8b6e145d99e90281e700bd4903f715ca0e Mon Sep 17 00:00:00 2001 From: Jakub Onderka Date: Fri, 1 Apr 2022 13:26:01 +0200 Subject: [PATCH 042/120] fix: [UI] Unpublish button title --- .../ajax/eventPublishConfirmationForm.ctp | 33 ++++++++++--------- 1 file changed, 17 insertions(+), 16 deletions(-) diff --git a/app/View/Events/ajax/eventPublishConfirmationForm.ctp b/app/View/Events/ajax/eventPublishConfirmationForm.ctp index 10bfd4d3f..b5b93403e 100644 --- a/app/View/Events/ajax/eventPublishConfirmationForm.ctp +++ b/app/View/Events/ajax/eventPublishConfirmationForm.ctp @@ -1,36 +1,37 @@
    Form->create('Event', array('style' => 'margin:0px;', 'id' => 'PromptForm', 'url' => $baseurl . '/events/' . $type . '/' . $id)); - $extraTitle = ""; - if ($type == 'publish') $extraTitle = ' (no email)'; - $message = __('Publish Event%s', $extraTitle); + echo $this->Form->create('Event', ['style' => 'margin:0px;', 'id' => 'PromptForm', 'url' => $baseurl . '/events/' . $type . '/' . $id]); if ($type === 'unpublish') { - $message = __('Unpublish Event%s', $extraTitle); + $message = __('Unpublish Event'); + $buttonTitle = __('Unpublish'); + } else { + $extraTitle = $type === 'publish' ? ' (no email)' : ''; + $message = __('Publish Event%s', $extraTitle); + $buttonTitle = __('Publish'); } ?> - - +
    - ' . __('Are you sure this event is complete and everyone should be informed?') . '

    '; +

    ' . __('Are you sure you wish to unpublish the event?') . '

    '; + echo __('Are you sure you wish to unpublish the event?'); } else if ($type === 'publishSightings') { - echo '

    ' . __('Are you sure you wish publish and synchronise all sightings attached to this event?') . '

    '; + echo __('Are you sure you wish publish and synchronise all sightings attached to this event?'); } else { - echo '

    ' . __('Publish but do NOT send alert email? Only for minor changes!') . '

    '; + echo __('Publish but do NOT send alert email? Only for minor changes!'); } - ?> + ?>

    - + - +
    From 6c258015a15521e6a019ba2bb1d370af83f17645 Mon Sep 17 00:00:00 2001 From: Sami Mokaddem Date: Mon, 4 Apr 2022 11:52:47 +0200 Subject: [PATCH 043/120] chg: [servers:getAllTypes] Moved the type and object collection action for filtering in the model --- app/Controller/FeedsController.php | 10 ++++++++++ app/Controller/ServersController.php | 30 ++-------------------------- app/Model/Server.php | 22 ++++++++++++++++++++ 3 files changed, 34 insertions(+), 28 deletions(-) diff --git a/app/Controller/FeedsController.php b/app/Controller/FeedsController.php index 7d0fb45bd..90002a838 100644 --- a/app/Controller/FeedsController.php +++ b/app/Controller/FeedsController.php @@ -298,6 +298,9 @@ class FeedsController extends AppController $tags = $this->Event->EventTag->Tag->find('list', array('fields' => array('Tag.name'), 'order' => array('lower(Tag.name) asc'))); $tags[0] = 'None'; + $this->loadModel('Server'); + $allTypes = $this->Server->getAllTypes(); + $dropdownData = [ 'orgs' => $this->Event->Orgc->find('list', array( 'fields' => array('id', 'name'), @@ -309,6 +312,8 @@ class FeedsController extends AppController 'distributionLevels' => $distributionLevels, 'inputSources' => $inputSources ]; + $this->set('allAttributeTypes', $allTypes['attribute']); + $this->set('allObjectTypes', $allTypes['object']); $this->set(compact('dropdownData')); $this->set('defaultPullRules', json_encode(Feed::DEFAULT_FEED_PULL_RULES)); $this->set('menuData', array('menuList' => 'feeds', 'menuItem' => 'add')); @@ -437,6 +442,11 @@ class FeedsController extends AppController $tags = $this->Event->EventTag->Tag->find('list', array('fields' => array('Tag.name'), 'order' => array('lower(Tag.name) asc'))); $tags[0] = 'None'; + $this->loadModel('Server'); + $allTypes = $this->Server->getAllTypes(); + $this->set('allAttributeTypes', $allTypes['attribute']); + $this->set('allObjectTypes', $allTypes['object']); + $dropdownData = [ 'orgs' => $this->Event->Orgc->find('list', array( 'fields' => array('id', 'name'), diff --git a/app/Controller/ServersController.php b/app/Controller/ServersController.php index 81c5bc2e7..8520b17fd 100644 --- a/app/Controller/ServersController.php +++ b/app/Controller/ServersController.php @@ -443,20 +443,7 @@ class ServersController extends AppController $allOrgs[] = array('id' => $o['Organisation']['id'], 'name' => $o['Organisation']['name']); } - $allTypes = []; - $this->loadModel('Attribute'); - $this->loadModel('ObjectTemplate'); - $objects = $this->ObjectTemplate->find('all', [ - 'recursive' => -1, - 'fields' => ['uuid', 'name'], - 'group' => ['uuid', 'name'], - ]); - $allTypes = [ - 'attribute' => array_unique(Hash::extract(Hash::extract($this->Attribute->categoryDefinitions, '{s}.types'), '{n}.{n}')), - 'object' => Hash::map($objects, '{n}.ObjectTemplate', function ($item) { - return ['id' => $item['uuid'], 'name' => sprintf('%s (%s)', $item['name'], $item['uuid'])]; - }) - ]; + $allTypes = $this->Server->getAllTypes(); $this->set('host_org_id', Configure::read('MISP.host_org_id')); $this->set('organisationOptions', $organisationOptions); @@ -641,20 +628,7 @@ class ServersController extends AppController $allOrgs[] = array('id' => $o['Organisation']['id'], 'name' => $o['Organisation']['name']); } - $allTypes = []; - $this->loadModel('Attribute'); - $this->loadModel('ObjectTemplate'); - $objects = $this->ObjectTemplate->find('all', [ - 'recursive' => -1, - 'fields' => ['uuid', 'name'], - 'group' => ['uuid', 'name'], - ]); - $allTypes = [ - 'attribute' => array_unique(Hash::extract(Hash::extract($this->Attribute->categoryDefinitions, '{s}.types'), '{n}.{n}')), - 'object' => Hash::map($objects, '{n}.ObjectTemplate', function ($item) { - return ['id' => $item['uuid'], 'name' => sprintf('%s (%s)', $item['name'], $item['uuid'])]; - }) - ]; + $allTypes = $this->Server->getAllTypes(); $oldRemoteSetting = 0; if (!$this->Server->data['RemoteOrg']['local']) { diff --git a/app/Model/Server.php b/app/Model/Server.php index fad2a33ca..82e655318 100644 --- a/app/Model/Server.php +++ b/app/Model/Server.php @@ -4614,6 +4614,28 @@ class Server extends AppModel return $this->saveMany($toSave, ['validate' => false, 'fields' => ['authkey']]); } + /** + * Return all Attribute and Object types + */ + public function getAllTypes(): array + { + $allTypes = []; + $this->Attribute = ClassRegistry::init('Attribute'); + $this->ObjectTemplate = ClassRegistry::init('ObjectTemplate'); + $objects = $this->ObjectTemplate->find('all', [ + 'recursive' => -1, + 'fields' => ['uuid', 'name'], + 'group' => ['uuid', 'name'], + ]); + $allTypes = [ + 'attribute' => array_unique(Hash::extract(Hash::extract($this->Attribute->categoryDefinitions, '{s}.types'), '{n}.{n}')), + 'object' => Hash::map($objects, '{n}.ObjectTemplate', function ($item) { + return ['id' => $item['uuid'], 'name' => sprintf('%s (%s)', $item['name'], $item['uuid'])]; + }) + ]; + return $allTypes; + } + /** * Invalidate config.php from php opcode cache */ From 077b43c33ef2b68422d1de7893f9554b22df8425 Mon Sep 17 00:00:00 2001 From: Sami Mokaddem Date: Mon, 4 Apr 2022 11:56:55 +0200 Subject: [PATCH 044/120] fix: [feed:filterEventIndex] Correctly filter out events based on the tag's filter rule --- app/Model/Feed.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/Model/Feed.php b/app/Model/Feed.php index f85c038ae..ff55fe863 100644 --- a/app/Model/Feed.php +++ b/app/Model/Feed.php @@ -813,7 +813,7 @@ class Feed extends AppModel } } if (!$found) { - unset($k); + unset($events[$k]); continue; } } @@ -828,7 +828,7 @@ class Feed extends AppModel } } if ($found) { - unset($k); + unset($events[$k]); } } } From 671c5588f46d88d9b7e148fc5b5353a11e043eb1 Mon Sep 17 00:00:00 2001 From: Sami Mokaddem Date: Mon, 4 Apr 2022 12:00:15 +0200 Subject: [PATCH 045/120] fix: [feed] Apply url_param filtering rules Currently only support timestamp and publish_timestamp --- app/Model/Feed.php | 39 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 39 insertions(+) diff --git a/app/Model/Feed.php b/app/Model/Feed.php index ff55fe863..32795b4a2 100644 --- a/app/Model/Feed.php +++ b/app/Model/Feed.php @@ -776,6 +776,10 @@ class Feed extends AppModel } } } + $url_params = !empty($filterRules['url_params']) ? $filterRules['url_params'] : []; + if (!$this->passesURLParamFilters($url_params, $event)) { + return false; + } return true; } @@ -832,10 +836,44 @@ class Feed extends AppModel } } } + $url_params = !empty($filterRules['url_params']) ? $filterRules['url_params'] : []; + if (!$this->passesURLParamFilters($url_params, $event)) { + unset($events[$k]); + } } return $events; } + private function passesURLParamFilters($url_params, $event): bool + { + $this->Attribute = ClassRegistry::init('Attribute'); + if (!empty($url_params['timestamp'])) { + $timestamps = $this->Attribute->setTimestampConditions($url_params['timestamp'], [], '', true); + if (is_array($timestamps)) { + if ($event['timestamp'] < $timestamps[0] || $event['timestamp'] > $timestamps[1]) { + return false; + } + } else { + if ($event['timestamp'] < $timestamps) { + return false; + } + } + } + if (!empty($url_params['publish_timestamp'])) { + $timestamps = $this->Attribute->setTimestampConditions($url_params['publish_timestamp'], [], '', true); + if (is_array($timestamps)) { + if ($event['timestamp'] < $timestamps[0] || $event['timestamp'] > $timestamps[1]) { + return false; + } + } else { + if ($event['timestamp'] < $timestamps) { + return false; + } + } + } + return true; + } + /** * @param array $feed * @param string $uuid @@ -997,6 +1035,7 @@ class Feed extends AppModel if ($filterRules === null) { throw new Exception('Could not parse feed filter rules JSON: ' . json_last_error_msg(), json_last_error()); } + $filterRules['url_params'] = !empty($filterRules['url_params']) ? $this->jsonDecode($filterRules['url_params']) : []; } return $filterRules; } From 990a00cceca01acc850702876f9f9d788b0f5e27 Mon Sep 17 00:00:00 2001 From: Sami Mokaddem Date: Mon, 4 Apr 2022 12:01:03 +0200 Subject: [PATCH 046/120] chg: [feeds:edit] Default filtering rules if not set --- app/Controller/FeedsController.php | 3 +++ 1 file changed, 3 insertions(+) diff --git a/app/Controller/FeedsController.php b/app/Controller/FeedsController.php index 90002a838..d6cee67a8 100644 --- a/app/Controller/FeedsController.php +++ b/app/Controller/FeedsController.php @@ -355,6 +355,9 @@ class FeedsController extends AppController ], 'afterFind' => function (array $feed) { $feed['Feed']['settings'] = json_decode($feed['Feed']['settings'], true); + if ($feed['Feed']['source_format'] == 'misp' && empty($feed['Feed']['rules'])) { + $feed['Feed']['rules'] = json_encode(Feed::DEFAULT_FEED_PULL_RULES); + } return $feed; }, From 8d5f6e66625e6ea951a7210581fd6985ddb908dd Mon Sep 17 00:00:00 2001 From: Sami Mokaddem Date: Mon, 4 Apr 2022 12:01:59 +0200 Subject: [PATCH 047/120] chg: [feed:pullRules] Added hints suggestions for url_params --- app/Controller/FeedsController.php | 1 + app/Model/Feed.php | 5 +++ .../Form/Fields/pullRulesField.ctp | 4 +- app/View/Elements/serverRuleElements/pull.ctp | 41 +++++++++++++++++++ 4 files changed, 50 insertions(+), 1 deletion(-) diff --git a/app/Controller/FeedsController.php b/app/Controller/FeedsController.php index d6cee67a8..07cc00d98 100644 --- a/app/Controller/FeedsController.php +++ b/app/Controller/FeedsController.php @@ -449,6 +449,7 @@ class FeedsController extends AppController $allTypes = $this->Server->getAllTypes(); $this->set('allAttributeTypes', $allTypes['attribute']); $this->set('allObjectTypes', $allTypes['object']); + $this->set('supportedUrlparams', Feed::SUPPORTED_URL_PARAM_FILTERS); $dropdownData = [ 'orgs' => $this->Event->Orgc->find('list', array( diff --git a/app/Model/Feed.php b/app/Model/Feed.php index 32795b4a2..ff4911910 100644 --- a/app/Model/Feed.php +++ b/app/Model/Feed.php @@ -74,6 +74,11 @@ class Feed extends AppModel 'url_params' => '' ]; + const SUPPORTED_URL_PARAM_FILTERS = [ + 'timestamp', + 'publish_timestamp', + ]; + const CACHE_DIR = APP . 'tmp' . DS . 'cache' . DS . 'feeds' . DS; /* diff --git a/app/View/Elements/genericElements/Form/Fields/pullRulesField.ctp b/app/View/Elements/genericElements/Form/Fields/pullRulesField.ctp index 3a2b8953b..fec45b50c 100644 --- a/app/View/Elements/genericElements/Form/Fields/pullRulesField.ctp +++ b/app/View/Elements/genericElements/Form/Fields/pullRulesField.ctp @@ -4,6 +4,7 @@ +

    @@ -23,7 +24,8 @@ 'context' => $this->Form->defaultModel, 'allTags' => $fieldData['tags'], 'allOrganisations' => $fieldData['orgs'], - 'ruleObject' => $pullRules + 'ruleObject' => $pullRules, + 'coreMirrorHints' => $supportedUrlparams, ]) ] ], diff --git a/app/View/Elements/serverRuleElements/pull.ctp b/app/View/Elements/serverRuleElements/pull.ctp index a19bfce7c..cde8ec236 100755 --- a/app/View/Elements/serverRuleElements/pull.ctp +++ b/app/View/Elements/serverRuleElements/pull.ctp @@ -99,6 +99,7 @@ echo $this->element('genericElements/assetLoader', array( 'js' => array( 'codemirror/codemirror', 'codemirror/modes/javascript', + 'codemirror/addons/show-hint', 'codemirror/addons/closebrackets', 'codemirror/addons/lint', 'codemirror/addons/jsonlint', @@ -114,6 +115,7 @@ echo $this->element('genericElements/assetLoader', array( - " ).appendTo( body ); - } - - if ( o.opacity ) { // opacity option - if ( this.helper.css( "opacity" ) ) { - this._storedOpacity = this.helper.css( "opacity" ); - } - this.helper.css( "opacity", o.opacity ); - } - - if ( o.zIndex ) { // zIndex option - if ( this.helper.css( "zIndex" ) ) { - this._storedZIndex = this.helper.css( "zIndex" ); - } - this.helper.css( "zIndex", o.zIndex ); - } - - //Prepare scrolling - if ( this.scrollParent[ 0 ] !== this.document[ 0 ] && - this.scrollParent[ 0 ].tagName !== "HTML" ) { - this.overflowOffset = this.scrollParent.offset(); - } - - //Call callbacks - this._trigger( "start", event, this._uiHash() ); - - //Recache the helper size - if ( !this._preserveHelperProportions ) { - this._cacheHelperProportions(); - } - - //Post "activate" events to possible containers - if ( !noActivation ) { - for ( i = this.containers.length - 1; i >= 0; i-- ) { - this.containers[ i ]._trigger( "activate", event, this._uiHash( this ) ); - } - } - - //Prepare possible droppables - if ( $.ui.ddmanager ) { - $.ui.ddmanager.current = this; - } - - if ( $.ui.ddmanager && !o.dropBehaviour ) { - $.ui.ddmanager.prepareOffsets( this, event ); - } - - this.dragging = true; - - this._addClass( this.helper, "ui-sortable-helper" ); - - // Execute the drag once - this causes the helper not to be visiblebefore getting its - // correct position - this._mouseDrag( event ); - return true; - - }, - - _mouseDrag: function( event ) { - var i, item, itemElement, intersection, - o = this.options, - scrolled = false; - - //Compute the helpers position - this.position = this._generatePosition( event ); - this.positionAbs = this._convertPositionTo( "absolute" ); - - if ( !this.lastPositionAbs ) { - this.lastPositionAbs = this.positionAbs; - } - - //Do scrolling - if ( this.options.scroll ) { - if ( this.scrollParent[ 0 ] !== this.document[ 0 ] && - this.scrollParent[ 0 ].tagName !== "HTML" ) { - - if ( ( this.overflowOffset.top + this.scrollParent[ 0 ].offsetHeight ) - - event.pageY < o.scrollSensitivity ) { - this.scrollParent[ 0 ].scrollTop = - scrolled = this.scrollParent[ 0 ].scrollTop + o.scrollSpeed; - } else if ( event.pageY - this.overflowOffset.top < o.scrollSensitivity ) { - this.scrollParent[ 0 ].scrollTop = - scrolled = this.scrollParent[ 0 ].scrollTop - o.scrollSpeed; - } - - if ( ( this.overflowOffset.left + this.scrollParent[ 0 ].offsetWidth ) - - event.pageX < o.scrollSensitivity ) { - this.scrollParent[ 0 ].scrollLeft = scrolled = - this.scrollParent[ 0 ].scrollLeft + o.scrollSpeed; - } else if ( event.pageX - this.overflowOffset.left < o.scrollSensitivity ) { - this.scrollParent[ 0 ].scrollLeft = scrolled = - this.scrollParent[ 0 ].scrollLeft - o.scrollSpeed; - } - - } else { - - if ( event.pageY - this.document.scrollTop() < o.scrollSensitivity ) { - scrolled = this.document.scrollTop( this.document.scrollTop() - o.scrollSpeed ); - } else if ( this.window.height() - ( event.pageY - this.document.scrollTop() ) < - o.scrollSensitivity ) { - scrolled = this.document.scrollTop( this.document.scrollTop() + o.scrollSpeed ); - } - - if ( event.pageX - this.document.scrollLeft() < o.scrollSensitivity ) { - scrolled = this.document.scrollLeft( - this.document.scrollLeft() - o.scrollSpeed - ); - } else if ( this.window.width() - ( event.pageX - this.document.scrollLeft() ) < - o.scrollSensitivity ) { - scrolled = this.document.scrollLeft( - this.document.scrollLeft() + o.scrollSpeed - ); - } - - } - - if ( scrolled !== false && $.ui.ddmanager && !o.dropBehaviour ) { - $.ui.ddmanager.prepareOffsets( this, event ); - } - } - - //Regenerate the absolute position used for position checks - this.positionAbs = this._convertPositionTo( "absolute" ); - - //Set the helper position - if ( !this.options.axis || this.options.axis !== "y" ) { - this.helper[ 0 ].style.left = this.position.left + "px"; - } - if ( !this.options.axis || this.options.axis !== "x" ) { - this.helper[ 0 ].style.top = this.position.top + "px"; - } - - //Rearrange - for ( i = this.items.length - 1; i >= 0; i-- ) { - - //Cache variables and intersection, continue if no intersection - item = this.items[ i ]; - itemElement = item.item[ 0 ]; - intersection = this._intersectsWithPointer( item ); - if ( !intersection ) { - continue; - } - - // Only put the placeholder inside the current Container, skip all - // items from other containers. This works because when moving - // an item from one container to another the - // currentContainer is switched before the placeholder is moved. - // - // Without this, moving items in "sub-sortables" can cause - // the placeholder to jitter between the outer and inner container. - if ( item.instance !== this.currentContainer ) { - continue; - } - - // Cannot intersect with itself - // no useless actions that have been done before - // no action if the item moved is the parent of the item checked - if ( itemElement !== this.currentItem[ 0 ] && - this.placeholder[ intersection === 1 ? "next" : "prev" ]()[ 0 ] !== itemElement && - !$.contains( this.placeholder[ 0 ], itemElement ) && - ( this.options.type === "semi-dynamic" ? - !$.contains( this.element[ 0 ], itemElement ) : - true - ) - ) { - - this.direction = intersection === 1 ? "down" : "up"; - - if ( this.options.tolerance === "pointer" || this._intersectsWithSides( item ) ) { - this._rearrange( event, item ); - } else { - break; - } - - this._trigger( "change", event, this._uiHash() ); - break; - } - } - - //Post events to containers - this._contactContainers( event ); - - //Interconnect with droppables - if ( $.ui.ddmanager ) { - $.ui.ddmanager.drag( this, event ); - } - - //Call callbacks - this._trigger( "sort", event, this._uiHash() ); - - this.lastPositionAbs = this.positionAbs; - return false; - - }, - - _mouseStop: function( event, noPropagation ) { - - if ( !event ) { - return; - } - - //If we are using droppables, inform the manager about the drop - if ( $.ui.ddmanager && !this.options.dropBehaviour ) { - $.ui.ddmanager.drop( this, event ); - } - - if ( this.options.revert ) { - var that = this, - cur = this.placeholder.offset(), - axis = this.options.axis, - animation = {}; - - if ( !axis || axis === "x" ) { - animation.left = cur.left - this.offset.parent.left - this.margins.left + - ( this.offsetParent[ 0 ] === this.document[ 0 ].body ? - 0 : - this.offsetParent[ 0 ].scrollLeft - ); - } - if ( !axis || axis === "y" ) { - animation.top = cur.top - this.offset.parent.top - this.margins.top + - ( this.offsetParent[ 0 ] === this.document[ 0 ].body ? - 0 : - this.offsetParent[ 0 ].scrollTop - ); - } - this.reverting = true; - $( this.helper ).animate( - animation, - parseInt( this.options.revert, 10 ) || 500, - function() { - that._clear( event ); - } - ); - } else { - this._clear( event, noPropagation ); - } - - return false; - - }, - - cancel: function() { - - if ( this.dragging ) { - - this._mouseUp( new $.Event( "mouseup", { target: null } ) ); - - if ( this.options.helper === "original" ) { - this.currentItem.css( this._storedCSS ); - this._removeClass( this.currentItem, "ui-sortable-helper" ); - } else { - this.currentItem.show(); - } - - //Post deactivating events to containers - for ( var i = this.containers.length - 1; i >= 0; i-- ) { - this.containers[ i ]._trigger( "deactivate", null, this._uiHash( this ) ); - if ( this.containers[ i ].containerCache.over ) { - this.containers[ i ]._trigger( "out", null, this._uiHash( this ) ); - this.containers[ i ].containerCache.over = 0; - } - } - - } - - if ( this.placeholder ) { - - //$(this.placeholder[0]).remove(); would have been the jQuery way - unfortunately, - // it unbinds ALL events from the original node! - if ( this.placeholder[ 0 ].parentNode ) { - this.placeholder[ 0 ].parentNode.removeChild( this.placeholder[ 0 ] ); - } - if ( this.options.helper !== "original" && this.helper && - this.helper[ 0 ].parentNode ) { - this.helper.remove(); - } - - $.extend( this, { - helper: null, - dragging: false, - reverting: false, - _noFinalSort: null - } ); - - if ( this.domPosition.prev ) { - $( this.domPosition.prev ).after( this.currentItem ); - } else { - $( this.domPosition.parent ).prepend( this.currentItem ); - } - } - - return this; - - }, - - serialize: function( o ) { - - var items = this._getItemsAsjQuery( o && o.connected ), - str = []; - o = o || {}; - - $( items ).each( function() { - var res = ( $( o.item || this ).attr( o.attribute || "id" ) || "" ) - .match( o.expression || ( /(.+)[\-=_](.+)/ ) ); - if ( res ) { - str.push( - ( o.key || res[ 1 ] + "[]" ) + - "=" + ( o.key && o.expression ? res[ 1 ] : res[ 2 ] ) ); - } - } ); - - if ( !str.length && o.key ) { - str.push( o.key + "=" ); - } - - return str.join( "&" ); - - }, - - toArray: function( o ) { - - var items = this._getItemsAsjQuery( o && o.connected ), - ret = []; - - o = o || {}; - - items.each( function() { - ret.push( $( o.item || this ).attr( o.attribute || "id" ) || "" ); - } ); - return ret; - - }, - - /* Be careful with the following core functions */ - _intersectsWith: function( item ) { - - var x1 = this.positionAbs.left, - x2 = x1 + this.helperProportions.width, - y1 = this.positionAbs.top, - y2 = y1 + this.helperProportions.height, - l = item.left, - r = l + item.width, - t = item.top, - b = t + item.height, - dyClick = this.offset.click.top, - dxClick = this.offset.click.left, - isOverElementHeight = ( this.options.axis === "x" ) || ( ( y1 + dyClick ) > t && - ( y1 + dyClick ) < b ), - isOverElementWidth = ( this.options.axis === "y" ) || ( ( x1 + dxClick ) > l && - ( x1 + dxClick ) < r ), - isOverElement = isOverElementHeight && isOverElementWidth; - - if ( this.options.tolerance === "pointer" || - this.options.forcePointerForContainers || - ( this.options.tolerance !== "pointer" && - this.helperProportions[ this.floating ? "width" : "height" ] > - item[ this.floating ? "width" : "height" ] ) - ) { - return isOverElement; - } else { - - return ( l < x1 + ( this.helperProportions.width / 2 ) && // Right Half - x2 - ( this.helperProportions.width / 2 ) < r && // Left Half - t < y1 + ( this.helperProportions.height / 2 ) && // Bottom Half - y2 - ( this.helperProportions.height / 2 ) < b ); // Top Half - - } - }, - - _intersectsWithPointer: function( item ) { - var verticalDirection, horizontalDirection, - isOverElementHeight = ( this.options.axis === "x" ) || - this._isOverAxis( - this.positionAbs.top + this.offset.click.top, item.top, item.height ), - isOverElementWidth = ( this.options.axis === "y" ) || - this._isOverAxis( - this.positionAbs.left + this.offset.click.left, item.left, item.width ), - isOverElement = isOverElementHeight && isOverElementWidth; - - if ( !isOverElement ) { - return false; - } - - verticalDirection = this._getDragVerticalDirection(); - horizontalDirection = this._getDragHorizontalDirection(); - - return this.floating ? - ( ( horizontalDirection === "right" || verticalDirection === "down" ) ? 2 : 1 ) - : ( verticalDirection && ( verticalDirection === "down" ? 2 : 1 ) ); - - }, - - _intersectsWithSides: function( item ) { - - var isOverBottomHalf = this._isOverAxis( this.positionAbs.top + - this.offset.click.top, item.top + ( item.height / 2 ), item.height ), - isOverRightHalf = this._isOverAxis( this.positionAbs.left + - this.offset.click.left, item.left + ( item.width / 2 ), item.width ), - verticalDirection = this._getDragVerticalDirection(), - horizontalDirection = this._getDragHorizontalDirection(); - - if ( this.floating && horizontalDirection ) { - return ( ( horizontalDirection === "right" && isOverRightHalf ) || - ( horizontalDirection === "left" && !isOverRightHalf ) ); - } else { - return verticalDirection && ( ( verticalDirection === "down" && isOverBottomHalf ) || - ( verticalDirection === "up" && !isOverBottomHalf ) ); - } - - }, - - _getDragVerticalDirection: function() { - var delta = this.positionAbs.top - this.lastPositionAbs.top; - return delta !== 0 && ( delta > 0 ? "down" : "up" ); - }, - - _getDragHorizontalDirection: function() { - var delta = this.positionAbs.left - this.lastPositionAbs.left; - return delta !== 0 && ( delta > 0 ? "right" : "left" ); - }, - - refresh: function( event ) { - this._refreshItems( event ); - this._setHandleClassName(); - this.refreshPositions(); - return this; - }, - - _connectWith: function() { - var options = this.options; - return options.connectWith.constructor === String ? - [ options.connectWith ] : - options.connectWith; - }, - - _getItemsAsjQuery: function( connected ) { - - var i, j, cur, inst, - items = [], - queries = [], - connectWith = this._connectWith(); - - if ( connectWith && connected ) { - for ( i = connectWith.length - 1; i >= 0; i-- ) { - cur = $( connectWith[ i ], this.document[ 0 ] ); - for ( j = cur.length - 1; j >= 0; j-- ) { - inst = $.data( cur[ j ], this.widgetFullName ); - if ( inst && inst !== this && !inst.options.disabled ) { - queries.push( [ $.isFunction( inst.options.items ) ? - inst.options.items.call( inst.element ) : - $( inst.options.items, inst.element ) - .not( ".ui-sortable-helper" ) - .not( ".ui-sortable-placeholder" ), inst ] ); - } - } - } - } - - queries.push( [ $.isFunction( this.options.items ) ? - this.options.items - .call( this.element, null, { options: this.options, item: this.currentItem } ) : - $( this.options.items, this.element ) - .not( ".ui-sortable-helper" ) - .not( ".ui-sortable-placeholder" ), this ] ); - - function addItems() { - items.push( this ); - } - for ( i = queries.length - 1; i >= 0; i-- ) { - queries[ i ][ 0 ].each( addItems ); - } - - return $( items ); - - }, - - _removeCurrentsFromItems: function() { - - var list = this.currentItem.find( ":data(" + this.widgetName + "-item)" ); - - this.items = $.grep( this.items, function( item ) { - for ( var j = 0; j < list.length; j++ ) { - if ( list[ j ] === item.item[ 0 ] ) { - return false; - } - } - return true; - } ); - - }, - - _refreshItems: function( event ) { - - this.items = []; - this.containers = [ this ]; - - var i, j, cur, inst, targetData, _queries, item, queriesLength, - items = this.items, - queries = [ [ $.isFunction( this.options.items ) ? - this.options.items.call( this.element[ 0 ], event, { item: this.currentItem } ) : - $( this.options.items, this.element ), this ] ], - connectWith = this._connectWith(); - - //Shouldn't be run the first time through due to massive slow-down - if ( connectWith && this.ready ) { - for ( i = connectWith.length - 1; i >= 0; i-- ) { - cur = $( connectWith[ i ], this.document[ 0 ] ); - for ( j = cur.length - 1; j >= 0; j-- ) { - inst = $.data( cur[ j ], this.widgetFullName ); - if ( inst && inst !== this && !inst.options.disabled ) { - queries.push( [ $.isFunction( inst.options.items ) ? - inst.options.items - .call( inst.element[ 0 ], event, { item: this.currentItem } ) : - $( inst.options.items, inst.element ), inst ] ); - this.containers.push( inst ); - } - } - } - } - - for ( i = queries.length - 1; i >= 0; i-- ) { - targetData = queries[ i ][ 1 ]; - _queries = queries[ i ][ 0 ]; - - for ( j = 0, queriesLength = _queries.length; j < queriesLength; j++ ) { - item = $( _queries[ j ] ); - - // Data for target checking (mouse manager) - item.data( this.widgetName + "-item", targetData ); - - items.push( { - item: item, - instance: targetData, - width: 0, height: 0, - left: 0, top: 0 - } ); - } - } - - }, - - refreshPositions: function( fast ) { - - // Determine whether items are being displayed horizontally - this.floating = this.items.length ? - this.options.axis === "x" || this._isFloating( this.items[ 0 ].item ) : - false; - - //This has to be redone because due to the item being moved out/into the offsetParent, - // the offsetParent's position will change - if ( this.offsetParent && this.helper ) { - this.offset.parent = this._getParentOffset(); - } - - var i, item, t, p; - - for ( i = this.items.length - 1; i >= 0; i-- ) { - item = this.items[ i ]; - - //We ignore calculating positions of all connected containers when we're not over them - if ( item.instance !== this.currentContainer && this.currentContainer && - item.item[ 0 ] !== this.currentItem[ 0 ] ) { - continue; - } - - t = this.options.toleranceElement ? - $( this.options.toleranceElement, item.item ) : - item.item; - - if ( !fast ) { - item.width = t.outerWidth(); - item.height = t.outerHeight(); - } - - p = t.offset(); - item.left = p.left; - item.top = p.top; - } - - if ( this.options.custom && this.options.custom.refreshContainers ) { - this.options.custom.refreshContainers.call( this ); - } else { - for ( i = this.containers.length - 1; i >= 0; i-- ) { - p = this.containers[ i ].element.offset(); - this.containers[ i ].containerCache.left = p.left; - this.containers[ i ].containerCache.top = p.top; - this.containers[ i ].containerCache.width = - this.containers[ i ].element.outerWidth(); - this.containers[ i ].containerCache.height = - this.containers[ i ].element.outerHeight(); - } - } - - return this; - }, - - _createPlaceholder: function( that ) { - that = that || this; - var className, - o = that.options; - - if ( !o.placeholder || o.placeholder.constructor === String ) { - className = o.placeholder; - o.placeholder = { - element: function() { - - var nodeName = that.currentItem[ 0 ].nodeName.toLowerCase(), - element = $( "<" + nodeName + ">", that.document[ 0 ] ); - - that._addClass( element, "ui-sortable-placeholder", - className || that.currentItem[ 0 ].className ) - ._removeClass( element, "ui-sortable-helper" ); - - if ( nodeName === "tbody" ) { - that._createTrPlaceholder( - that.currentItem.find( "tr" ).eq( 0 ), - $( "", that.document[ 0 ] ).appendTo( element ) - ); - } else if ( nodeName === "tr" ) { - that._createTrPlaceholder( that.currentItem, element ); - } else if ( nodeName === "img" ) { - element.attr( "src", that.currentItem.attr( "src" ) ); - } - - if ( !className ) { - element.css( "visibility", "hidden" ); - } - - return element; - }, - update: function( container, p ) { - - // 1. If a className is set as 'placeholder option, we don't force sizes - - // the class is responsible for that - // 2. The option 'forcePlaceholderSize can be enabled to force it even if a - // class name is specified - if ( className && !o.forcePlaceholderSize ) { - return; - } - - //If the element doesn't have a actual height by itself (without styles coming - // from a stylesheet), it receives the inline height from the dragged item - if ( !p.height() ) { - p.height( - that.currentItem.innerHeight() - - parseInt( that.currentItem.css( "paddingTop" ) || 0, 10 ) - - parseInt( that.currentItem.css( "paddingBottom" ) || 0, 10 ) ); - } - if ( !p.width() ) { - p.width( - that.currentItem.innerWidth() - - parseInt( that.currentItem.css( "paddingLeft" ) || 0, 10 ) - - parseInt( that.currentItem.css( "paddingRight" ) || 0, 10 ) ); - } - } - }; - } - - //Create the placeholder - that.placeholder = $( o.placeholder.element.call( that.element, that.currentItem ) ); - - //Append it after the actual current item - that.currentItem.after( that.placeholder ); - - //Update the size of the placeholder (TODO: Logic to fuzzy, see line 316/317) - o.placeholder.update( that, that.placeholder ); - - }, - - _createTrPlaceholder: function( sourceTr, targetTr ) { - var that = this; - - sourceTr.children().each( function() { - $( " ", that.document[ 0 ] ) - .attr( "colspan", $( this ).attr( "colspan" ) || 1 ) - .appendTo( targetTr ); - } ); - }, - - _contactContainers: function( event ) { - var i, j, dist, itemWithLeastDistance, posProperty, sizeProperty, cur, nearBottom, - floating, axis, - innermostContainer = null, - innermostIndex = null; - - // Get innermost container that intersects with item - for ( i = this.containers.length - 1; i >= 0; i-- ) { - - // Never consider a container that's located within the item itself - if ( $.contains( this.currentItem[ 0 ], this.containers[ i ].element[ 0 ] ) ) { - continue; - } - - if ( this._intersectsWith( this.containers[ i ].containerCache ) ) { - - // If we've already found a container and it's more "inner" than this, then continue - if ( innermostContainer && - $.contains( - this.containers[ i ].element[ 0 ], - innermostContainer.element[ 0 ] ) ) { - continue; - } - - innermostContainer = this.containers[ i ]; - innermostIndex = i; - - } else { - - // container doesn't intersect. trigger "out" event if necessary - if ( this.containers[ i ].containerCache.over ) { - this.containers[ i ]._trigger( "out", event, this._uiHash( this ) ); - this.containers[ i ].containerCache.over = 0; - } - } - - } - - // If no intersecting containers found, return - if ( !innermostContainer ) { - return; - } - - // Move the item into the container if it's not there already - if ( this.containers.length === 1 ) { - if ( !this.containers[ innermostIndex ].containerCache.over ) { - this.containers[ innermostIndex ]._trigger( "over", event, this._uiHash( this ) ); - this.containers[ innermostIndex ].containerCache.over = 1; - } - } else { - - // When entering a new container, we will find the item with the least distance and - // append our item near it - dist = 10000; - itemWithLeastDistance = null; - floating = innermostContainer.floating || this._isFloating( this.currentItem ); - posProperty = floating ? "left" : "top"; - sizeProperty = floating ? "width" : "height"; - axis = floating ? "pageX" : "pageY"; - - for ( j = this.items.length - 1; j >= 0; j-- ) { - if ( !$.contains( - this.containers[ innermostIndex ].element[ 0 ], this.items[ j ].item[ 0 ] ) - ) { - continue; - } - if ( this.items[ j ].item[ 0 ] === this.currentItem[ 0 ] ) { - continue; - } - - cur = this.items[ j ].item.offset()[ posProperty ]; - nearBottom = false; - if ( event[ axis ] - cur > this.items[ j ][ sizeProperty ] / 2 ) { - nearBottom = true; - } - - if ( Math.abs( event[ axis ] - cur ) < dist ) { - dist = Math.abs( event[ axis ] - cur ); - itemWithLeastDistance = this.items[ j ]; - this.direction = nearBottom ? "up" : "down"; - } - } - - //Check if dropOnEmpty is enabled - if ( !itemWithLeastDistance && !this.options.dropOnEmpty ) { - return; - } - - if ( this.currentContainer === this.containers[ innermostIndex ] ) { - if ( !this.currentContainer.containerCache.over ) { - this.containers[ innermostIndex ]._trigger( "over", event, this._uiHash() ); - this.currentContainer.containerCache.over = 1; - } - return; - } - - itemWithLeastDistance ? - this._rearrange( event, itemWithLeastDistance, null, true ) : - this._rearrange( event, null, this.containers[ innermostIndex ].element, true ); - this._trigger( "change", event, this._uiHash() ); - this.containers[ innermostIndex ]._trigger( "change", event, this._uiHash( this ) ); - this.currentContainer = this.containers[ innermostIndex ]; - - //Update the placeholder - this.options.placeholder.update( this.currentContainer, this.placeholder ); - - this.containers[ innermostIndex ]._trigger( "over", event, this._uiHash( this ) ); - this.containers[ innermostIndex ].containerCache.over = 1; - } - - }, - - _createHelper: function( event ) { - - var o = this.options, - helper = $.isFunction( o.helper ) ? - $( o.helper.apply( this.element[ 0 ], [ event, this.currentItem ] ) ) : - ( o.helper === "clone" ? this.currentItem.clone() : this.currentItem ); - - //Add the helper to the DOM if that didn't happen already - if ( !helper.parents( "body" ).length ) { - $( o.appendTo !== "parent" ? - o.appendTo : - this.currentItem[ 0 ].parentNode )[ 0 ].appendChild( helper[ 0 ] ); - } - - if ( helper[ 0 ] === this.currentItem[ 0 ] ) { - this._storedCSS = { - width: this.currentItem[ 0 ].style.width, - height: this.currentItem[ 0 ].style.height, - position: this.currentItem.css( "position" ), - top: this.currentItem.css( "top" ), - left: this.currentItem.css( "left" ) - }; - } - - if ( !helper[ 0 ].style.width || o.forceHelperSize ) { - helper.width( this.currentItem.width() ); - } - if ( !helper[ 0 ].style.height || o.forceHelperSize ) { - helper.height( this.currentItem.height() ); - } - - return helper; - - }, - - _adjustOffsetFromHelper: function( obj ) { - if ( typeof obj === "string" ) { - obj = obj.split( " " ); - } - if ( $.isArray( obj ) ) { - obj = { left: +obj[ 0 ], top: +obj[ 1 ] || 0 }; - } - if ( "left" in obj ) { - this.offset.click.left = obj.left + this.margins.left; - } - if ( "right" in obj ) { - this.offset.click.left = this.helperProportions.width - obj.right + this.margins.left; - } - if ( "top" in obj ) { - this.offset.click.top = obj.top + this.margins.top; - } - if ( "bottom" in obj ) { - this.offset.click.top = this.helperProportions.height - obj.bottom + this.margins.top; - } - }, - - _getParentOffset: function() { - - //Get the offsetParent and cache its position - this.offsetParent = this.helper.offsetParent(); - var po = this.offsetParent.offset(); - - // This is a special case where we need to modify a offset calculated on start, since the - // following happened: - // 1. The position of the helper is absolute, so it's position is calculated based on the - // next positioned parent - // 2. The actual offset parent is a child of the scroll parent, and the scroll parent isn't - // the document, which means that the scroll is included in the initial calculation of the - // offset of the parent, and never recalculated upon drag - if ( this.cssPosition === "absolute" && this.scrollParent[ 0 ] !== this.document[ 0 ] && - $.contains( this.scrollParent[ 0 ], this.offsetParent[ 0 ] ) ) { - po.left += this.scrollParent.scrollLeft(); - po.top += this.scrollParent.scrollTop(); - } - - // This needs to be actually done for all browsers, since pageX/pageY includes this - // information with an ugly IE fix - if ( this.offsetParent[ 0 ] === this.document[ 0 ].body || - ( this.offsetParent[ 0 ].tagName && - this.offsetParent[ 0 ].tagName.toLowerCase() === "html" && $.ui.ie ) ) { - po = { top: 0, left: 0 }; - } - - return { - top: po.top + ( parseInt( this.offsetParent.css( "borderTopWidth" ), 10 ) || 0 ), - left: po.left + ( parseInt( this.offsetParent.css( "borderLeftWidth" ), 10 ) || 0 ) - }; - - }, - - _getRelativeOffset: function() { - - if ( this.cssPosition === "relative" ) { - var p = this.currentItem.position(); - return { - top: p.top - ( parseInt( this.helper.css( "top" ), 10 ) || 0 ) + - this.scrollParent.scrollTop(), - left: p.left - ( parseInt( this.helper.css( "left" ), 10 ) || 0 ) + - this.scrollParent.scrollLeft() - }; - } else { - return { top: 0, left: 0 }; - } - - }, - - _cacheMargins: function() { - this.margins = { - left: ( parseInt( this.currentItem.css( "marginLeft" ), 10 ) || 0 ), - top: ( parseInt( this.currentItem.css( "marginTop" ), 10 ) || 0 ) - }; - }, - - _cacheHelperProportions: function() { - this.helperProportions = { - width: this.helper.outerWidth(), - height: this.helper.outerHeight() - }; - }, - - _setContainment: function() { - - var ce, co, over, - o = this.options; - if ( o.containment === "parent" ) { - o.containment = this.helper[ 0 ].parentNode; - } - if ( o.containment === "document" || o.containment === "window" ) { - this.containment = [ - 0 - this.offset.relative.left - this.offset.parent.left, - 0 - this.offset.relative.top - this.offset.parent.top, - o.containment === "document" ? - this.document.width() : - this.window.width() - this.helperProportions.width - this.margins.left, - ( o.containment === "document" ? - ( this.document.height() || document.body.parentNode.scrollHeight ) : - this.window.height() || this.document[ 0 ].body.parentNode.scrollHeight - ) - this.helperProportions.height - this.margins.top - ]; - } - - if ( !( /^(document|window|parent)$/ ).test( o.containment ) ) { - ce = $( o.containment )[ 0 ]; - co = $( o.containment ).offset(); - over = ( $( ce ).css( "overflow" ) !== "hidden" ); - - this.containment = [ - co.left + ( parseInt( $( ce ).css( "borderLeftWidth" ), 10 ) || 0 ) + - ( parseInt( $( ce ).css( "paddingLeft" ), 10 ) || 0 ) - this.margins.left, - co.top + ( parseInt( $( ce ).css( "borderTopWidth" ), 10 ) || 0 ) + - ( parseInt( $( ce ).css( "paddingTop" ), 10 ) || 0 ) - this.margins.top, - co.left + ( over ? Math.max( ce.scrollWidth, ce.offsetWidth ) : ce.offsetWidth ) - - ( parseInt( $( ce ).css( "borderLeftWidth" ), 10 ) || 0 ) - - ( parseInt( $( ce ).css( "paddingRight" ), 10 ) || 0 ) - - this.helperProportions.width - this.margins.left, - co.top + ( over ? Math.max( ce.scrollHeight, ce.offsetHeight ) : ce.offsetHeight ) - - ( parseInt( $( ce ).css( "borderTopWidth" ), 10 ) || 0 ) - - ( parseInt( $( ce ).css( "paddingBottom" ), 10 ) || 0 ) - - this.helperProportions.height - this.margins.top - ]; - } - - }, - - _convertPositionTo: function( d, pos ) { - - if ( !pos ) { - pos = this.position; - } - var mod = d === "absolute" ? 1 : -1, - scroll = this.cssPosition === "absolute" && - !( this.scrollParent[ 0 ] !== this.document[ 0 ] && - $.contains( this.scrollParent[ 0 ], this.offsetParent[ 0 ] ) ) ? - this.offsetParent : - this.scrollParent, - scrollIsRootNode = ( /(html|body)/i ).test( scroll[ 0 ].tagName ); - - return { - top: ( - - // The absolute mouse position - pos.top + - - // Only for relative positioned nodes: Relative offset from element to offset parent - this.offset.relative.top * mod + - - // The offsetParent's offset without borders (offset + border) - this.offset.parent.top * mod - - ( ( this.cssPosition === "fixed" ? - -this.scrollParent.scrollTop() : - ( scrollIsRootNode ? 0 : scroll.scrollTop() ) ) * mod ) - ), - left: ( - - // The absolute mouse position - pos.left + - - // Only for relative positioned nodes: Relative offset from element to offset parent - this.offset.relative.left * mod + - - // The offsetParent's offset without borders (offset + border) - this.offset.parent.left * mod - - ( ( this.cssPosition === "fixed" ? - -this.scrollParent.scrollLeft() : scrollIsRootNode ? 0 : - scroll.scrollLeft() ) * mod ) - ) - }; - - }, - - _generatePosition: function( event ) { - - var top, left, - o = this.options, - pageX = event.pageX, - pageY = event.pageY, - scroll = this.cssPosition === "absolute" && - !( this.scrollParent[ 0 ] !== this.document[ 0 ] && - $.contains( this.scrollParent[ 0 ], this.offsetParent[ 0 ] ) ) ? - this.offsetParent : - this.scrollParent, - scrollIsRootNode = ( /(html|body)/i ).test( scroll[ 0 ].tagName ); - - // This is another very weird special case that only happens for relative elements: - // 1. If the css position is relative - // 2. and the scroll parent is the document or similar to the offset parent - // we have to refresh the relative offset during the scroll so there are no jumps - if ( this.cssPosition === "relative" && !( this.scrollParent[ 0 ] !== this.document[ 0 ] && - this.scrollParent[ 0 ] !== this.offsetParent[ 0 ] ) ) { - this.offset.relative = this._getRelativeOffset(); - } - - /* - * - Position constraining - - * Constrain the position to a mix of grid, containment. - */ - - if ( this.originalPosition ) { //If we are not dragging yet, we won't check for options - - if ( this.containment ) { - if ( event.pageX - this.offset.click.left < this.containment[ 0 ] ) { - pageX = this.containment[ 0 ] + this.offset.click.left; - } - if ( event.pageY - this.offset.click.top < this.containment[ 1 ] ) { - pageY = this.containment[ 1 ] + this.offset.click.top; - } - if ( event.pageX - this.offset.click.left > this.containment[ 2 ] ) { - pageX = this.containment[ 2 ] + this.offset.click.left; - } - if ( event.pageY - this.offset.click.top > this.containment[ 3 ] ) { - pageY = this.containment[ 3 ] + this.offset.click.top; - } - } - - if ( o.grid ) { - top = this.originalPageY + Math.round( ( pageY - this.originalPageY ) / - o.grid[ 1 ] ) * o.grid[ 1 ]; - pageY = this.containment ? - ( ( top - this.offset.click.top >= this.containment[ 1 ] && - top - this.offset.click.top <= this.containment[ 3 ] ) ? - top : - ( ( top - this.offset.click.top >= this.containment[ 1 ] ) ? - top - o.grid[ 1 ] : top + o.grid[ 1 ] ) ) : - top; - - left = this.originalPageX + Math.round( ( pageX - this.originalPageX ) / - o.grid[ 0 ] ) * o.grid[ 0 ]; - pageX = this.containment ? - ( ( left - this.offset.click.left >= this.containment[ 0 ] && - left - this.offset.click.left <= this.containment[ 2 ] ) ? - left : - ( ( left - this.offset.click.left >= this.containment[ 0 ] ) ? - left - o.grid[ 0 ] : left + o.grid[ 0 ] ) ) : - left; - } - - } - - return { - top: ( - - // The absolute mouse position - pageY - - - // Click offset (relative to the element) - this.offset.click.top - - - // Only for relative positioned nodes: Relative offset from element to offset parent - this.offset.relative.top - - - // The offsetParent's offset without borders (offset + border) - this.offset.parent.top + - ( ( this.cssPosition === "fixed" ? - -this.scrollParent.scrollTop() : - ( scrollIsRootNode ? 0 : scroll.scrollTop() ) ) ) - ), - left: ( - - // The absolute mouse position - pageX - - - // Click offset (relative to the element) - this.offset.click.left - - - // Only for relative positioned nodes: Relative offset from element to offset parent - this.offset.relative.left - - - // The offsetParent's offset without borders (offset + border) - this.offset.parent.left + - ( ( this.cssPosition === "fixed" ? - -this.scrollParent.scrollLeft() : - scrollIsRootNode ? 0 : scroll.scrollLeft() ) ) - ) - }; - - }, - - _rearrange: function( event, i, a, hardRefresh ) { - - a ? a[ 0 ].appendChild( this.placeholder[ 0 ] ) : - i.item[ 0 ].parentNode.insertBefore( this.placeholder[ 0 ], - ( this.direction === "down" ? i.item[ 0 ] : i.item[ 0 ].nextSibling ) ); - - //Various things done here to improve the performance: - // 1. we create a setTimeout, that calls refreshPositions - // 2. on the instance, we have a counter variable, that get's higher after every append - // 3. on the local scope, we copy the counter variable, and check in the timeout, - // if it's still the same - // 4. this lets only the last addition to the timeout stack through - this.counter = this.counter ? ++this.counter : 1; - var counter = this.counter; - - this._delay( function() { - if ( counter === this.counter ) { - - //Precompute after each DOM insertion, NOT on mousemove - this.refreshPositions( !hardRefresh ); - } - } ); - - }, - - _clear: function( event, noPropagation ) { - - this.reverting = false; - - // We delay all events that have to be triggered to after the point where the placeholder - // has been removed and everything else normalized again - var i, - delayedTriggers = []; - - // We first have to update the dom position of the actual currentItem - // Note: don't do it if the current item is already removed (by a user), or it gets - // reappended (see #4088) - if ( !this._noFinalSort && this.currentItem.parent().length ) { - this.placeholder.before( this.currentItem ); - } - this._noFinalSort = null; - - if ( this.helper[ 0 ] === this.currentItem[ 0 ] ) { - for ( i in this._storedCSS ) { - if ( this._storedCSS[ i ] === "auto" || this._storedCSS[ i ] === "static" ) { - this._storedCSS[ i ] = ""; - } - } - this.currentItem.css( this._storedCSS ); - this._removeClass( this.currentItem, "ui-sortable-helper" ); - } else { - this.currentItem.show(); - } - - if ( this.fromOutside && !noPropagation ) { - delayedTriggers.push( function( event ) { - this._trigger( "receive", event, this._uiHash( this.fromOutside ) ); - } ); - } - if ( ( this.fromOutside || - this.domPosition.prev !== - this.currentItem.prev().not( ".ui-sortable-helper" )[ 0 ] || - this.domPosition.parent !== this.currentItem.parent()[ 0 ] ) && !noPropagation ) { - - // Trigger update callback if the DOM position has changed - delayedTriggers.push( function( event ) { - this._trigger( "update", event, this._uiHash() ); - } ); - } - - // Check if the items Container has Changed and trigger appropriate - // events. - if ( this !== this.currentContainer ) { - if ( !noPropagation ) { - delayedTriggers.push( function( event ) { - this._trigger( "remove", event, this._uiHash() ); - } ); - delayedTriggers.push( ( function( c ) { - return function( event ) { - c._trigger( "receive", event, this._uiHash( this ) ); - }; - } ).call( this, this.currentContainer ) ); - delayedTriggers.push( ( function( c ) { - return function( event ) { - c._trigger( "update", event, this._uiHash( this ) ); - }; - } ).call( this, this.currentContainer ) ); - } - } - - //Post events to containers - function delayEvent( type, instance, container ) { - return function( event ) { - container._trigger( type, event, instance._uiHash( instance ) ); - }; - } - for ( i = this.containers.length - 1; i >= 0; i-- ) { - if ( !noPropagation ) { - delayedTriggers.push( delayEvent( "deactivate", this, this.containers[ i ] ) ); - } - if ( this.containers[ i ].containerCache.over ) { - delayedTriggers.push( delayEvent( "out", this, this.containers[ i ] ) ); - this.containers[ i ].containerCache.over = 0; - } - } - - //Do what was originally in plugins - if ( this.storedCursor ) { - this.document.find( "body" ).css( "cursor", this.storedCursor ); - this.storedStylesheet.remove(); - } - if ( this._storedOpacity ) { - this.helper.css( "opacity", this._storedOpacity ); - } - if ( this._storedZIndex ) { - this.helper.css( "zIndex", this._storedZIndex === "auto" ? "" : this._storedZIndex ); - } - - this.dragging = false; - - if ( !noPropagation ) { - this._trigger( "beforeStop", event, this._uiHash() ); - } - - //$(this.placeholder[0]).remove(); would have been the jQuery way - unfortunately, - // it unbinds ALL events from the original node! - this.placeholder[ 0 ].parentNode.removeChild( this.placeholder[ 0 ] ); - - if ( !this.cancelHelperRemoval ) { - if ( this.helper[ 0 ] !== this.currentItem[ 0 ] ) { - this.helper.remove(); - } - this.helper = null; - } - - if ( !noPropagation ) { - for ( i = 0; i < delayedTriggers.length; i++ ) { - - // Trigger all delayed events - delayedTriggers[ i ].call( this, event ); - } - this._trigger( "stop", event, this._uiHash() ); - } - - this.fromOutside = false; - return !this.cancelHelperRemoval; - - }, - - _trigger: function() { - if ( $.Widget.prototype._trigger.apply( this, arguments ) === false ) { - this.cancel(); - } - }, - - _uiHash: function( _inst ) { - var inst = _inst || this; - return { - helper: inst.helper, - placeholder: inst.placeholder || $( [] ), - position: inst.position, - originalPosition: inst.originalPosition, - offset: inst.positionAbs, - item: inst.currentItem, - sender: _inst ? _inst.element : null - }; - } - -} ); - - -/*! - * jQuery UI Accordion 1.12.1 - * http://jqueryui.com - * - * Copyright jQuery Foundation and other contributors - * Released under the MIT license. - * http://jquery.org/license - */ - -//>>label: Accordion -//>>group: Widgets -// jscs:disable maximumLineLength -//>>description: Displays collapsible content panels for presenting information in a limited amount of space. -// jscs:enable maximumLineLength -//>>docs: http://api.jqueryui.com/accordion/ -//>>demos: http://jqueryui.com/accordion/ -//>>css.structure: ../../themes/base/core.css -//>>css.structure: ../../themes/base/accordion.css -//>>css.theme: ../../themes/base/theme.css - - - -var widgetsAccordion = $.widget( "ui.accordion", { - version: "1.12.1", - options: { - active: 0, - animate: {}, - classes: { - "ui-accordion-header": "ui-corner-top", - "ui-accordion-header-collapsed": "ui-corner-all", - "ui-accordion-content": "ui-corner-bottom" - }, - collapsible: false, - event: "click", - header: "> li > :first-child, > :not(li):even", - heightStyle: "auto", - icons: { - activeHeader: "ui-icon-triangle-1-s", - header: "ui-icon-triangle-1-e" - }, - - // Callbacks - activate: null, - beforeActivate: null - }, - - hideProps: { - borderTopWidth: "hide", - borderBottomWidth: "hide", - paddingTop: "hide", - paddingBottom: "hide", - height: "hide" - }, - - showProps: { - borderTopWidth: "show", - borderBottomWidth: "show", - paddingTop: "show", - paddingBottom: "show", - height: "show" - }, - - _create: function() { - var options = this.options; - - this.prevShow = this.prevHide = $(); - this._addClass( "ui-accordion", "ui-widget ui-helper-reset" ); - this.element.attr( "role", "tablist" ); - - // Don't allow collapsible: false and active: false / null - if ( !options.collapsible && ( options.active === false || options.active == null ) ) { - options.active = 0; - } - - this._processPanels(); - - // handle negative values - if ( options.active < 0 ) { - options.active += this.headers.length; - } - this._refresh(); - }, - - _getCreateEventData: function() { - return { - header: this.active, - panel: !this.active.length ? $() : this.active.next() - }; - }, - - _createIcons: function() { - var icon, children, - icons = this.options.icons; - - if ( icons ) { - icon = $( "" ); - this._addClass( icon, "ui-accordion-header-icon", "ui-icon " + icons.header ); - icon.prependTo( this.headers ); - children = this.active.children( ".ui-accordion-header-icon" ); - this._removeClass( children, icons.header ) - ._addClass( children, null, icons.activeHeader ) - ._addClass( this.headers, "ui-accordion-icons" ); - } - }, - - _destroyIcons: function() { - this._removeClass( this.headers, "ui-accordion-icons" ); - this.headers.children( ".ui-accordion-header-icon" ).remove(); - }, - - _destroy: function() { - var contents; - - // Clean up main element - this.element.removeAttr( "role" ); - - // Clean up headers - this.headers - .removeAttr( "role aria-expanded aria-selected aria-controls tabIndex" ) - .removeUniqueId(); - - this._destroyIcons(); - - // Clean up content panels - contents = this.headers.next() - .css( "display", "" ) - .removeAttr( "role aria-hidden aria-labelledby" ) - .removeUniqueId(); - - if ( this.options.heightStyle !== "content" ) { - contents.css( "height", "" ); - } - }, - - _setOption: function( key, value ) { - if ( key === "active" ) { - - // _activate() will handle invalid values and update this.options - this._activate( value ); - return; - } - - if ( key === "event" ) { - if ( this.options.event ) { - this._off( this.headers, this.options.event ); - } - this._setupEvents( value ); - } - - this._super( key, value ); - - // Setting collapsible: false while collapsed; open first panel - if ( key === "collapsible" && !value && this.options.active === false ) { - this._activate( 0 ); - } - - if ( key === "icons" ) { - this._destroyIcons(); - if ( value ) { - this._createIcons(); - } - } - }, - - _setOptionDisabled: function( value ) { - this._super( value ); - - this.element.attr( "aria-disabled", value ); - - // Support: IE8 Only - // #5332 / #6059 - opacity doesn't cascade to positioned elements in IE - // so we need to add the disabled class to the headers and panels - this._toggleClass( null, "ui-state-disabled", !!value ); - this._toggleClass( this.headers.add( this.headers.next() ), null, "ui-state-disabled", - !!value ); - }, - - _keydown: function( event ) { - if ( event.altKey || event.ctrlKey ) { - return; - } - - var keyCode = $.ui.keyCode, - length = this.headers.length, - currentIndex = this.headers.index( event.target ), - toFocus = false; - - switch ( event.keyCode ) { - case keyCode.RIGHT: - case keyCode.DOWN: - toFocus = this.headers[ ( currentIndex + 1 ) % length ]; - break; - case keyCode.LEFT: - case keyCode.UP: - toFocus = this.headers[ ( currentIndex - 1 + length ) % length ]; - break; - case keyCode.SPACE: - case keyCode.ENTER: - this._eventHandler( event ); - break; - case keyCode.HOME: - toFocus = this.headers[ 0 ]; - break; - case keyCode.END: - toFocus = this.headers[ length - 1 ]; - break; - } - - if ( toFocus ) { - $( event.target ).attr( "tabIndex", -1 ); - $( toFocus ).attr( "tabIndex", 0 ); - $( toFocus ).trigger( "focus" ); - event.preventDefault(); - } - }, - - _panelKeyDown: function( event ) { - if ( event.keyCode === $.ui.keyCode.UP && event.ctrlKey ) { - $( event.currentTarget ).prev().trigger( "focus" ); - } - }, - - refresh: function() { - var options = this.options; - this._processPanels(); - - // Was collapsed or no panel - if ( ( options.active === false && options.collapsible === true ) || - !this.headers.length ) { - options.active = false; - this.active = $(); - - // active false only when collapsible is true - } else if ( options.active === false ) { - this._activate( 0 ); - - // was active, but active panel is gone - } else if ( this.active.length && !$.contains( this.element[ 0 ], this.active[ 0 ] ) ) { - - // all remaining panel are disabled - if ( this.headers.length === this.headers.find( ".ui-state-disabled" ).length ) { - options.active = false; - this.active = $(); - - // activate previous panel - } else { - this._activate( Math.max( 0, options.active - 1 ) ); - } - - // was active, active panel still exists - } else { - - // make sure active index is correct - options.active = this.headers.index( this.active ); - } - - this._destroyIcons(); - - this._refresh(); - }, - - _processPanels: function() { - var prevHeaders = this.headers, - prevPanels = this.panels; - - this.headers = this.element.find( this.options.header ); - this._addClass( this.headers, "ui-accordion-header ui-accordion-header-collapsed", - "ui-state-default" ); - - this.panels = this.headers.next().filter( ":not(.ui-accordion-content-active)" ).hide(); - this._addClass( this.panels, "ui-accordion-content", "ui-helper-reset ui-widget-content" ); - - // Avoid memory leaks (#10056) - if ( prevPanels ) { - this._off( prevHeaders.not( this.headers ) ); - this._off( prevPanels.not( this.panels ) ); - } - }, - - _refresh: function() { - var maxHeight, - options = this.options, - heightStyle = options.heightStyle, - parent = this.element.parent(); - - this.active = this._findActive( options.active ); - this._addClass( this.active, "ui-accordion-header-active", "ui-state-active" ) - ._removeClass( this.active, "ui-accordion-header-collapsed" ); - this._addClass( this.active.next(), "ui-accordion-content-active" ); - this.active.next().show(); - - this.headers - .attr( "role", "tab" ) - .each( function() { - var header = $( this ), - headerId = header.uniqueId().attr( "id" ), - panel = header.next(), - panelId = panel.uniqueId().attr( "id" ); - header.attr( "aria-controls", panelId ); - panel.attr( "aria-labelledby", headerId ); - } ) - .next() - .attr( "role", "tabpanel" ); - - this.headers - .not( this.active ) - .attr( { - "aria-selected": "false", - "aria-expanded": "false", - tabIndex: -1 - } ) - .next() - .attr( { - "aria-hidden": "true" - } ) - .hide(); - - // Make sure at least one header is in the tab order - if ( !this.active.length ) { - this.headers.eq( 0 ).attr( "tabIndex", 0 ); - } else { - this.active.attr( { - "aria-selected": "true", - "aria-expanded": "true", - tabIndex: 0 - } ) - .next() - .attr( { - "aria-hidden": "false" - } ); - } - - this._createIcons(); - - this._setupEvents( options.event ); - - if ( heightStyle === "fill" ) { - maxHeight = parent.height(); - this.element.siblings( ":visible" ).each( function() { - var elem = $( this ), - position = elem.css( "position" ); - - if ( position === "absolute" || position === "fixed" ) { - return; - } - maxHeight -= elem.outerHeight( true ); - } ); - - this.headers.each( function() { - maxHeight -= $( this ).outerHeight( true ); - } ); - - this.headers.next() - .each( function() { - $( this ).height( Math.max( 0, maxHeight - - $( this ).innerHeight() + $( this ).height() ) ); - } ) - .css( "overflow", "auto" ); - } else if ( heightStyle === "auto" ) { - maxHeight = 0; - this.headers.next() - .each( function() { - var isVisible = $( this ).is( ":visible" ); - if ( !isVisible ) { - $( this ).show(); - } - maxHeight = Math.max( maxHeight, $( this ).css( "height", "" ).height() ); - if ( !isVisible ) { - $( this ).hide(); - } - } ) - .height( maxHeight ); - } - }, - - _activate: function( index ) { - var active = this._findActive( index )[ 0 ]; - - // Trying to activate the already active panel - if ( active === this.active[ 0 ] ) { - return; - } - - // Trying to collapse, simulate a click on the currently active header - active = active || this.active[ 0 ]; - - this._eventHandler( { - target: active, - currentTarget: active, - preventDefault: $.noop - } ); - }, - - _findActive: function( selector ) { - return typeof selector === "number" ? this.headers.eq( selector ) : $(); - }, - - _setupEvents: function( event ) { - var events = { - keydown: "_keydown" - }; - if ( event ) { - $.each( event.split( " " ), function( index, eventName ) { - events[ eventName ] = "_eventHandler"; - } ); - } - - this._off( this.headers.add( this.headers.next() ) ); - this._on( this.headers, events ); - this._on( this.headers.next(), { keydown: "_panelKeyDown" } ); - this._hoverable( this.headers ); - this._focusable( this.headers ); - }, - - _eventHandler: function( event ) { - var activeChildren, clickedChildren, - options = this.options, - active = this.active, - clicked = $( event.currentTarget ), - clickedIsActive = clicked[ 0 ] === active[ 0 ], - collapsing = clickedIsActive && options.collapsible, - toShow = collapsing ? $() : clicked.next(), - toHide = active.next(), - eventData = { - oldHeader: active, - oldPanel: toHide, - newHeader: collapsing ? $() : clicked, - newPanel: toShow - }; - - event.preventDefault(); - - if ( - - // click on active header, but not collapsible - ( clickedIsActive && !options.collapsible ) || - - // allow canceling activation - ( this._trigger( "beforeActivate", event, eventData ) === false ) ) { - return; - } - - options.active = collapsing ? false : this.headers.index( clicked ); - - // When the call to ._toggle() comes after the class changes - // it causes a very odd bug in IE 8 (see #6720) - this.active = clickedIsActive ? $() : clicked; - this._toggle( eventData ); - - // Switch classes - // corner classes on the previously active header stay after the animation - this._removeClass( active, "ui-accordion-header-active", "ui-state-active" ); - if ( options.icons ) { - activeChildren = active.children( ".ui-accordion-header-icon" ); - this._removeClass( activeChildren, null, options.icons.activeHeader ) - ._addClass( activeChildren, null, options.icons.header ); - } - - if ( !clickedIsActive ) { - this._removeClass( clicked, "ui-accordion-header-collapsed" ) - ._addClass( clicked, "ui-accordion-header-active", "ui-state-active" ); - if ( options.icons ) { - clickedChildren = clicked.children( ".ui-accordion-header-icon" ); - this._removeClass( clickedChildren, null, options.icons.header ) - ._addClass( clickedChildren, null, options.icons.activeHeader ); - } - - this._addClass( clicked.next(), "ui-accordion-content-active" ); - } - }, - - _toggle: function( data ) { - var toShow = data.newPanel, - toHide = this.prevShow.length ? this.prevShow : data.oldPanel; - - // Handle activating a panel during the animation for another activation - this.prevShow.add( this.prevHide ).stop( true, true ); - this.prevShow = toShow; - this.prevHide = toHide; - - if ( this.options.animate ) { - this._animate( toShow, toHide, data ); - } else { - toHide.hide(); - toShow.show(); - this._toggleComplete( data ); - } - - toHide.attr( { - "aria-hidden": "true" - } ); - toHide.prev().attr( { - "aria-selected": "false", - "aria-expanded": "false" - } ); - - // if we're switching panels, remove the old header from the tab order - // if we're opening from collapsed state, remove the previous header from the tab order - // if we're collapsing, then keep the collapsing header in the tab order - if ( toShow.length && toHide.length ) { - toHide.prev().attr( { - "tabIndex": -1, - "aria-expanded": "false" - } ); - } else if ( toShow.length ) { - this.headers.filter( function() { - return parseInt( $( this ).attr( "tabIndex" ), 10 ) === 0; - } ) - .attr( "tabIndex", -1 ); - } - - toShow - .attr( "aria-hidden", "false" ) - .prev() - .attr( { - "aria-selected": "true", - "aria-expanded": "true", - tabIndex: 0 - } ); - }, - - _animate: function( toShow, toHide, data ) { - var total, easing, duration, - that = this, - adjust = 0, - boxSizing = toShow.css( "box-sizing" ), - down = toShow.length && - ( !toHide.length || ( toShow.index() < toHide.index() ) ), - animate = this.options.animate || {}, - options = down && animate.down || animate, - complete = function() { - that._toggleComplete( data ); - }; - - if ( typeof options === "number" ) { - duration = options; - } - if ( typeof options === "string" ) { - easing = options; - } - - // fall back from options to animation in case of partial down settings - easing = easing || options.easing || animate.easing; - duration = duration || options.duration || animate.duration; - - if ( !toHide.length ) { - return toShow.animate( this.showProps, duration, easing, complete ); - } - if ( !toShow.length ) { - return toHide.animate( this.hideProps, duration, easing, complete ); - } - - total = toShow.show().outerHeight(); - toHide.animate( this.hideProps, { - duration: duration, - easing: easing, - step: function( now, fx ) { - fx.now = Math.round( now ); - } - } ); - toShow - .hide() - .animate( this.showProps, { - duration: duration, - easing: easing, - complete: complete, - step: function( now, fx ) { - fx.now = Math.round( now ); - if ( fx.prop !== "height" ) { - if ( boxSizing === "content-box" ) { - adjust += fx.now; - } - } else if ( that.options.heightStyle !== "content" ) { - fx.now = Math.round( total - toHide.outerHeight() - adjust ); - adjust = 0; - } - } - } ); - }, - - _toggleComplete: function( data ) { - var toHide = data.oldPanel, - prev = toHide.prev(); - - this._removeClass( toHide, "ui-accordion-content-active" ); - this._removeClass( prev, "ui-accordion-header-active" ) - ._addClass( prev, "ui-accordion-header-collapsed" ); - - // Work around for rendering bug in IE (#5421) - if ( toHide.length ) { - toHide.parent()[ 0 ].className = toHide.parent()[ 0 ].className; - } - this._trigger( "activate", null, data ); - } -} ); - - -/*! - * jQuery UI Menu 1.12.1 - * http://jqueryui.com - * - * Copyright jQuery Foundation and other contributors - * Released under the MIT license. - * http://jquery.org/license - */ - -//>>label: Menu -//>>group: Widgets -//>>description: Creates nestable menus. -//>>docs: http://api.jqueryui.com/menu/ -//>>demos: http://jqueryui.com/menu/ -//>>css.structure: ../../themes/base/core.css -//>>css.structure: ../../themes/base/menu.css -//>>css.theme: ../../themes/base/theme.css - - - -var widgetsMenu = $.widget( "ui.menu", { - version: "1.12.1", - defaultElement: "