From ec67586cbc586402bd7d315e4651a9794d8d87f2 Mon Sep 17 00:00:00 2001 From: Rick van der Zwet Date: Tue, 5 Sep 2023 21:20:08 +0000 Subject: [PATCH 01/14] Fix update priority on task does not apply to taskwrapper The task object priority is servering asl 'dummy' for normal tasks. Related taskwrapper object should be updated as well, since the 'real' priority scheduling is done using the Taskwrapper object. Fixes: #989 --- ci/apiv2/test_task.py | 14 +++++++++++++- src/inc/apiv2/model/tasks.routes.php | 7 +++++++ 2 files changed, 20 insertions(+), 1 deletion(-) diff --git a/ci/apiv2/test_task.py b/ci/apiv2/test_task.py index 09c99f6d3..61b9fc0f2 100644 --- a/ci/apiv2/test_task.py +++ b/ci/apiv2/test_task.py @@ -1,4 +1,4 @@ -from hashtopolis import Task +from hashtopolis import Task, TaskWrapper from utils import BaseTest @@ -75,3 +75,15 @@ def test_task_with_file(self): task = self.create_task(hashlist, extra_payload=extra_payload) obj = Task.objects.get(pk=task.id, expand='files') self.assertListEqual([x.id for x in files], [x.id for x in obj.files_set]) + + def test_task_update_priority(self): + task = self.create_test_object() + obj = TaskWrapper.objects.get(pk=task.taskWrapperId) + self.assertEqual(task.priority, obj.priority) + + new_priority = task.priority + 1234 + task.priority = new_priority + task.save() + + obj = TaskWrapper.objects.get(pk=task.taskWrapperId) + self.assertEqual(new_priority, obj.priority) \ No newline at end of file diff --git a/src/inc/apiv2/model/tasks.routes.php b/src/inc/apiv2/model/tasks.routes.php index 24f7986cd..8199d6cf2 100644 --- a/src/inc/apiv2/model/tasks.routes.php +++ b/src/inc/apiv2/model/tasks.routes.php @@ -104,6 +104,13 @@ public function updateObject(object $object, $data, $processed = []): void { TaskUtils::archiveTask($object->getId(), $this->getCurrentUser()); } + /* Update connected TaskWrapper priority as well */ + $key = Task::PRIORITY; + if (array_key_exists($key, $data)) { + array_push($processed, $key); + TaskUtils::updatePriority($object->getId(), $data[Task::PRIORITY], $this->getCurrentUser()); + } + parent::updateObject($object, $data, $processed); } } From 40d73c7ba5dbf57b3e5775a17fdf6982a8062921 Mon Sep 17 00:00:00 2001 From: Rick van der Zwet Date: Mon, 2 Oct 2023 11:26:51 +0000 Subject: [PATCH 02/14] Sync used mysql instance from prod to dev Production is using mysql:8.0 --- .devcontainer/docker-compose.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.devcontainer/docker-compose.yml b/.devcontainer/docker-compose.yml index 826c1aa22..fbbd36ad1 100644 --- a/.devcontainer/docker-compose.yml +++ b/.devcontainer/docker-compose.yml @@ -27,7 +27,7 @@ services: - hashtopolis_dev hashtopolis-db-dev: container_name: hashtopolis-db-dev - image: mysql:5.7 + image: mysql:8.0 restart: always ports: - "3306:3306" From 398ba315b718ca5f5624443c33dcaf62755a0939 Mon Sep 17 00:00:00 2001 From: Romke van Dijk Date: Tue, 24 Oct 2023 10:08:33 +0200 Subject: [PATCH 03/14] Fixes maxAgent also being set when patching Task when not supertask Reenabling skipped tests --- ci/apiv2/test_filter_and_ordering.py | 3 --- ci/apiv2/test_task.py | 14 +++++++++++++- src/inc/apiv2/model/tasks.routes.php | 7 +++++++ 3 files changed, 20 insertions(+), 4 deletions(-) diff --git a/ci/apiv2/test_filter_and_ordering.py b/ci/apiv2/test_filter_and_ordering.py index ebd1217c5..5597e35a5 100644 --- a/ci/apiv2/test_filter_and_ordering.py +++ b/ci/apiv2/test_filter_and_ordering.py @@ -24,7 +24,6 @@ def test_filter(self): [x.id for x in model_objs], [x.id for x in objs]) - @pytest.mark.skip(reason="Broken due to bug https://github.com/hashtopolis/server/issues/968") def test_filter__contains(self): search_token = "SHA" objs = HashType.objects.filter(description__contains=search_token) @@ -33,7 +32,6 @@ def test_filter__contains(self): [x.id for x in all_objs if search_token in x.description], [x.id for x in objs]) - @pytest.mark.skip(reason="Broken due to bug https://github.com/hashtopolis/server/issues/968") def test_filter__endswith(self): search_token = 'sha512' objs = HashType.objects.filter(description__endswith=search_token) @@ -108,7 +106,6 @@ def test_filter__ne(self): [x.id for x in all_objs if x.hashTypeId != 100], [x.id for x in objs]) - @pytest.mark.skip(reason="Broken due to bug https://github.com/hashtopolis/server/issues/968") def test_filter__startswith(self): objs = HashType.objects.filter(description__startswith="net") all_objs = HashType.objects.all() diff --git a/ci/apiv2/test_task.py b/ci/apiv2/test_task.py index 61b9fc0f2..ab3b9fb96 100644 --- a/ci/apiv2/test_task.py +++ b/ci/apiv2/test_task.py @@ -86,4 +86,16 @@ def test_task_update_priority(self): task.save() obj = TaskWrapper.objects.get(pk=task.taskWrapperId) - self.assertEqual(new_priority, obj.priority) \ No newline at end of file + self.assertEqual(new_priority, obj.priority) + + def test_task_update_maxagent(self): + task = self.create_test_object() + obj = TaskWrapper.objects.get(pk=task.taskWrapperId) + self.assertEqual(task.maxAgents, obj.maxAgents) + + new_maxagent = task.maxAgents + 1234 + task.maxAgents = new_maxagent + task.save() + + obj = TaskWrapper.objects.get(pk=task.taskWrapperId) + self.assertEqual(new_maxagent, obj.maxAgents) diff --git a/src/inc/apiv2/model/tasks.routes.php b/src/inc/apiv2/model/tasks.routes.php index 8199d6cf2..bab40a859 100644 --- a/src/inc/apiv2/model/tasks.routes.php +++ b/src/inc/apiv2/model/tasks.routes.php @@ -111,6 +111,13 @@ public function updateObject(object $object, $data, $processed = []): void { TaskUtils::updatePriority($object->getId(), $data[Task::PRIORITY], $this->getCurrentUser()); } + /* Update connected TaskWrapper maxAgents as well */ + $key = Task::MAX_AGENTS; + if (array_key_exists($key, $data)) { + array_push($processed, $key); + TaskUtils::updateMaxAgents($object->getId(), $data[Task::MAX_AGENTS], $this->getCurrentUser()); + } + parent::updateObject($object, $data, $processed); } } From d642b3094774ab04fa126c984d6dbb0d2729bc9f Mon Sep 17 00:00:00 2001 From: Rick van der Zwet Date: Wed, 25 Oct 2023 10:56:10 +0000 Subject: [PATCH 04/14] Add PoC for dummy data generation Usefull when developing feature which require large sets. Like pagination and expansion. --- ci/apiv2/generate_dummy_data.py | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) create mode 100644 ci/apiv2/generate_dummy_data.py diff --git a/ci/apiv2/generate_dummy_data.py b/ci/apiv2/generate_dummy_data.py new file mode 100644 index 000000000..4677b306b --- /dev/null +++ b/ci/apiv2/generate_dummy_data.py @@ -0,0 +1,17 @@ +#!/usr/bin/env python +""" +Generate dummy data for development/debugging purposes +""" +from utils import do_create_hashlist + + +def generate_dummy_data(): + for _ in range(1000): + # TODO: Generate unique hashlists + do_create_hashlist() + + +# TODO: Generate different objects like users/tasks/crackerbinaries/etc +if __name__ == '__main__': + # TODO: Use seed to generate an predictable 'random' test dataset + generate_dummy_data() From a97071ae69548e329469a7fc3dca6182812046a1 Mon Sep 17 00:00:00 2001 From: Rick van der Zwet Date: Wed, 25 Oct 2023 11:17:55 +0000 Subject: [PATCH 05/14] Add optimalisation on fetching related objects Instead of fetching the related object one object at the time, requested all related objects ,via fetchExpandObjects, for given list of objects. This will reduce DB queries required for expansion from O( * ) to O() Added bonus we do not fetch duplicated objects anymore, reducing the amount of objects which requires processing. Fixes #1017 --- ci/apiv2/HACKING.md | 8 +- .../apiv2/common/AbstractBaseAPI.class.php | 64 +++---- .../apiv2/common/AbstractModelAPI.class.php | 157 +++++++++++++++++- src/inc/apiv2/model/accessgroups.routes.php | 35 ++-- .../apiv2/model/agentassignments.routes.php | 28 +++- src/inc/apiv2/model/agents.routes.php | 31 ++-- src/inc/apiv2/model/agentstats.routes.php | 2 - src/inc/apiv2/model/chunks.routes.php | 18 +- src/inc/apiv2/model/configs.routes.php | 20 ++- src/inc/apiv2/model/configsections.routes.php | 2 - src/inc/apiv2/model/crackers.routes.php | 20 ++- src/inc/apiv2/model/crackertypes.routes.php | 19 ++- src/inc/apiv2/model/files.routes.php | 19 ++- .../model/globalpermissiongroups.routes.php | 21 ++- src/inc/apiv2/model/hashes.routes.php | 34 ++-- src/inc/apiv2/model/hashlists.routes.php | 63 +++++-- src/inc/apiv2/model/hashtypes.routes.php | 5 - .../apiv2/model/healthcheckagents.routes.php | 31 +++- src/inc/apiv2/model/healthchecks.routes.php | 30 +++- src/inc/apiv2/model/notifications.routes.php | 20 ++- src/inc/apiv2/model/pretasks.routes.php | 22 ++- src/inc/apiv2/model/speeds.routes.php | 29 +++- src/inc/apiv2/model/supertasks.routes.php | 23 ++- src/inc/apiv2/model/tasks.routes.php | 86 ++++++---- src/inc/apiv2/model/taskwrappers.routes.php | 28 +++- src/inc/apiv2/model/users.routes.php | 32 +++- 26 files changed, 632 insertions(+), 215 deletions(-) diff --git a/ci/apiv2/HACKING.md b/ci/apiv2/HACKING.md index 7fce5a5ce..079b3e1a5 100644 --- a/ci/apiv2/HACKING.md +++ b/ci/apiv2/HACKING.md @@ -14,7 +14,13 @@ curl --header "Content-Type: application/json" -X GET --header "Authorization: B Access database: ``` -mysql -u $HASHTOPOLIS_DB_USER -p $HASHTOPOLIS_DB_PASS -h $HASHTOPOLIS_DB_HOST -D $HASHTOPOLIS_DB_DATABASE +mysql -u $HASHTOPOLIS_DB_USER -p$HASHTOPOLIS_DB_PASS -h $HASHTOPOLIS_DB_HOST -D $HASHTOPOLIS_DB_DATABASE +``` + +Enable query logging: +``` +docker exec $(docker ps -aqf "ancestor=mysql:8.0") mysql -u root -phashtopolis -e "SET global log_output = 'FILE'; SET global general_log_file='/tmp/mysql_all.log'; SET global general_log = 1;" +docker exec $(docker ps -aqf "ancestor=mysql:8.0") tail -f /tmp/mysql_all.log ``` ### paper flipchart scribbles diff --git a/src/inc/apiv2/common/AbstractBaseAPI.class.php b/src/inc/apiv2/common/AbstractBaseAPI.class.php index 4d97c6d82..424a1bd6f 100644 --- a/src/inc/apiv2/common/AbstractBaseAPI.class.php +++ b/src/inc/apiv2/common/AbstractBaseAPI.class.php @@ -1,5 +1,4 @@ getFormFields() as $key => $feature) { /* Innitate default values */ - $features[$key] = $feature + ['null' => False, 'protected' => False, 'private' => False, 'choices' => "unset"]; + $features[$key] = $feature + ['null' => False, 'protected' => False, 'private' => False, 'choices' => "unset", 'pk' => False]; if (!array_key_exists('alias', $feature)) { $features[$key]['alias'] = $key; } @@ -147,13 +142,13 @@ public function getExpandables(): array { return []; } - /** - * Process $expand on $object + * Fetch objects for $expand on $objects */ - protected function doExpand(object $object, string $expand): mixed { + protected function fetchExpandObjects(array $objects, string $expand): mixed { } + protected static function getModelFactory(string $model): object { switch($model) { case AccessGroup::class: @@ -490,34 +485,43 @@ protected function filterQuery(mixed $objFactory, DBA\QueryFilter $qF): array return $ret; } + + protected function applyExpansions(object $object, array $expands, array $expandResult): array { + $newObject = $this->obj2Array($object); + foreach ($expands as $expand) { + if (array_key_exists($object->getId(), $expandResult[$expand]) == false) { + $newObject[$expand] = []; + continue; + } + + $expandObject = $expandResult[$expand][$object->getId()]; + if (is_array($expandObject)) { + $newObject[$expand] = array_map(function($object) { return $this->obj2Array($object); }, $expandObject); + } else { + $newObject[$expand] = $this->obj2Array($expandObject); + } + } + + /* Ensure sorted, for easy debugging of fields */ + ksort($newObject); + + return $newObject; + } + + /** * Expands object items */ protected function object2Array(object $object, array $expands = []): array { - $item = $this->obj2Array($object); - + $expandResult = []; foreach ($expands as $expand) { $apiClass = $this->container->get('classMapper')->get(get_class($object)); - $item[$expand] = $apiClass::doExpand($object, $expand); - if (is_null($item[$expand])) { - throw new BadFunctionCallException("Internal error: Expansion '$expand' not implemented!"); + $expandResult[$expand] = $apiClass::fetchExpandObjects([$object], $expand); } - }; - - $expandLeft = array_diff($expands, array_keys($item)); - if (sizeof($expandLeft) > 0) { - /* This should never happen, since valid parameter checking is done pre-flight - * in makeExpandables and assignment should be done for every expansion - */ - throw new BadFunctionCallException("Internal error: Expansion(s) '" . join(',', $expandLeft) . "' not implemented!"); - } - /* Ensure sorted, for easy debugging of fields */ - ksort($item); - - return $item; + return $this->applyExpansions($object, $expands, $expandResult); } @@ -626,7 +630,7 @@ protected function validateData(array $data, array $features) /** * Validate incoming parameter keys */ - protected function validateParameters($data, $allFeatures): void { + protected function validateParameters(array $data, array $allFeatures): void { // Features which MAY be present $validFeatures = []; // Features which MUST be present @@ -703,7 +707,7 @@ protected function makeExpandables(Request $request, array $validExpandables): a /** * Find primary key for DBA object */ - private function getPrimaryKey(): string + protected function getPrimaryKey(): string { $features = $this->getFeatures(); # Word-around required since getPrimaryKey is not static in dba/models/*.php diff --git a/src/inc/apiv2/common/AbstractModelAPI.class.php b/src/inc/apiv2/common/AbstractModelAPI.class.php index a2d48a193..a9a5bad15 100644 --- a/src/inc/apiv2/common/AbstractModelAPI.class.php +++ b/src/inc/apiv2/common/AbstractModelAPI.class.php @@ -6,8 +6,13 @@ use Slim\Exception\HttpNotFoundException; -use DBA\Factory; +use DBA\AbstractModelFactory; +use DBA\JoinFilter; +use DBA\Factory; +use DBA\ContainFilter; +use DBA\OrderFilter; +use DBA\QueryFilter; use Middlewares\Utils\HttpErrorException; @@ -31,6 +36,142 @@ final protected function getFeatures(): array ); } + /** + * Retrieve ForeignKey Relation + * + * @param array $objects Objects Fetch relation for selected Objects + * @param string $objectField Field to use as base for $objects + * @param object $factory Factory used to retrieve objects + * @param string $filterField Filter field of $field to filter against $objects field + * + * @return array + */ + final protected static function getForeignKeyRelation( + array $objects, + string $objectField, + object $factory, + string $filterField + ): array { + assert($factory instanceof AbstractModelFactory); + $retval = array(); + + /* Fetch required objects */ + $objectIds = []; + foreach($objects as $object) { + $kv = $object->getKeyValueDict(); + $objectIds[] = $kv[$objectField]; + } + $qF = new ContainFilter($filterField, $objectIds, $factory); + $hO = $factory->filter([Factory::FILTER => $qF]); + + /* Objects are uniquely identified by fields, create mapping to speed-up further processing */ + $f2o = []; + foreach ($hO as $relationObject) { + $f2o[$relationObject->getKeyValueDict()[$filterField]] = $relationObject; + }; + + /* Map objects */ + foreach ($objects as $object) { + $fieldId = $object->getKeyValueDict()[$objectField]; + if (array_key_exists($fieldId, $f2o) == true) { + $retval[$object->getId()] = $f2o[$fieldId]; + } + } + + return $retval; + } + + /** + * Retrieve ManyToOneRelation (reverse ForeignKey) + * + * @param array $objects Objects Fetch relation for selected Objects + * @param string $objectField Field to use as base for $objects + * @param object $factory Factory used to retrieve objects + * @param string $filterField Filter field of $field to filter against $objects field + * + * @return array + */ + final protected static function getManyToOneRelation( + array $objects, + string $objectField, + object $factory, + string $filterField + ): array { + assert($factory instanceof AbstractModelFactory); + $retval = array(); + + /* Fetch required objects */ + $objectIds = []; + foreach($objects as $object) { + $kv = $object->getKeyValueDict(); + $objectIds[] = $kv[$objectField]; + } + $qF = new ContainFilter($filterField, $objectIds, $factory); + $hO = $factory->filter([Factory::FILTER => $qF]); + + /* Map (multiple) objects to base objects */ + foreach ($hO as $relationObject) { + $kv = $relationObject->getKeyValueDict(); + $retval[$kv[$filterField]][] = $relationObject; + } + + return $retval; + } + + + /** + * Retrieve ManyToOne relalation for $objects ('parents') of type $targetFactory via 'intermidate' + * of $intermediateFactory joining on $joinField (between 'intermediate' and 'target'). Filtered by + * $filterField at $intermediateFactory. + * + * @param array $objects Objects Fetch relation for selected Objects + * @param string $objectField Field to use as base for $objects + * @param object $intermediateFactory Factory used as intermediate between parentObject and targetObject + * @param string $filterField Filter field of intermadiateObject to filter against $objects field + * @param object $targetFactory Object properties of objects returned + * @param string $joinField Field to connect 'intermediate' to 'target' + + * @return array + */ + final protected static function getManyToOneRelationViaIntermediate( + array $objects, + string $objectField, + object $intermediateFactory, + string $filterField, + object $targetFactory, + string $joinField, + ): array { + assert($intermediateFactory instanceof AbstractModelFactory); + assert($targetFactory instanceof AbstractModelFactory); + $retval = array(); + + + /* Retrieve Parent -> Intermediate -> Target objects */ + $objectIds = []; + foreach($objects as $object) { + $kv = $object->getKeyValueDict(); + $objectIds[] = $kv[$objectField]; + } + $qF = new ContainFilter($filterField, $objectIds, $intermediateFactory); + $jF = new JoinFilter($intermediateFactory, $joinField, $joinField); + $hO = $targetFactory->filter([Factory::FILTER => $qF, Factory::JOIN => $jF]); + + /* Build mapping Parent -> Intermediate */ + $i2p = []; + foreach($hO[$intermediateFactory->getModelName()] as $intermidiateObject) { + $kv = $intermidiateObject->getKeyValueDict(); + $i2p[$kv[$joinField]] = $kv[$filterField]; + } + + /* Associate Target -> Parent (via Intermediate) */ + foreach($hO[$targetFactory->getModelName()] as $targetObject) { + $parent = $i2p[$targetObject->getKeyValueDict()[$joinField]]; + $retval[$parent][] = $targetObject; + } + + return $retval; + } + /** * Retrieve permissions based on class and method requested */ @@ -128,16 +269,26 @@ public function get(Request $request, Response $response, array $args): Response $allFilters[Factory::ORDER] = $oFs; } - // TODO: Optimize code, should only fetch subsection of database, when pagination is in play + /* Request objects */ $objects = $factory->filter($allFilters); + /* Resolve all expandables */ + $expandResult = []; + foreach ($expands as $expand) { + // mapping from $objectId -> result objects in + $expandResult[$expand] = $this->fetchExpandObjects($objects, $expand); + } + + /* Convert objects to JSON */ $lists = []; foreach ($objects as $object) { - $lists[] = $this->object2Array($object, $expands); + $newObject = $this->applyExpansions($object, $expands, $expandResult); + $lists[] = $newObject; } // TODO: Implement actual expanding $total = count($objects); + $ret = [ "_expandable" => join(",", $expandable), "startAt" => $startAt, diff --git a/src/inc/apiv2/model/accessgroups.routes.php b/src/inc/apiv2/model/accessgroups.routes.php index 922c9111d..50d8a4fa5 100644 --- a/src/inc/apiv2/model/accessgroups.routes.php +++ b/src/inc/apiv2/model/accessgroups.routes.php @@ -1,7 +1,5 @@ getId(), "=", Factory::getAccessGroupUserFactory()); - $jF = new JoinFilter(Factory::getAccessGroupUserFactory(), User::USER_ID, AccessGroupUser::USER_ID); - return $this->joinQuery(Factory::getUserFactory(), $qF, $jF); + return $this->getManyToOneRelationViaIntermediate( + $objects, + AccessGroup::ACCESS_GROUP_ID, + Factory::getAccessGroupUserFactory(), + AccessGroupUser::ACCESS_GROUP_ID, + Factory::getUserFactory(), + User::USER_ID + ); case 'agentMembers': - $qF = new QueryFilter(AccessGroupAgent::ACCESS_GROUP_ID, $object->getId(), "=", Factory::getAccessGroupAgentFactory()); - $jF = new JoinFilter(Factory::getAccessGroupAgentFactory(), Agent::AGENT_ID, AccessGroupAgent::AGENT_ID); - return $this->joinQuery(Factory::getAgentFactory(), $qF, $jF); + return $this->getManyToOneRelationViaIntermediate( + $objects, + AccessGroup::ACCESS_GROUP_ID, + Factory::getAccessGroupAgentFactory(), + AccessGroupAgent::ACCESS_GROUP_ID, + Factory::getAgentFactory(), + Agent::AGENT_ID + ); + default: + throw new BadFunctionCallException("Internal error: Expansion '$expand' not implemented!"); } - } + } protected function createObject(array $data): int { $object = AccessGroupUtils::createGroup($data[AccessGroup::GROUP_NAME]); diff --git a/src/inc/apiv2/model/agentassignments.routes.php b/src/inc/apiv2/model/agentassignments.routes.php index c395bb68e..d66a3dfa6 100644 --- a/src/inc/apiv2/model/agentassignments.routes.php +++ b/src/inc/apiv2/model/agentassignments.routes.php @@ -3,7 +3,9 @@ use DBA\QueryFilter; use DBA\OrderFilter; +use DBA\Agent; use DBA\Assignment; +use DBA\Task; require_once(dirname(__FILE__) . "/../common/AbstractModelAPI.class.php"); @@ -25,18 +27,30 @@ public function getExpandables(): array { return ["task", "agent"]; } - protected function doExpand(object $object, string $expand): mixed { - assert($object instanceof Assignment); + protected function fetchExpandObjects(array $objects, string $expand): mixed { + /* Ensure we receive the proper type */ + array_walk($objects, function($obj) { assert($obj instanceof Assignment); }); + /* Expand requested section */ switch($expand) { case 'task': - $obj = Factory::getTaskFactory()->get($object->getTaskId()); - return $this->obj2Array($obj); + return $this->getForeignKeyRelation( + $objects, + Assignment::TASK_ID, + Factory::getTaskFactory(), + Task::TASK_ID + ); case 'agent': - $obj = Factory::getAgentFactory()->get($object->getAgentId()); - return $this->obj2Array($obj); + return $this->getForeignKeyRelation( + $objects, + Assignment::AGENT_ID, + Factory::getAgentFactory(), + Agent::AGENT_ID + ); + default: + throw new BadFunctionCallException("Internal error: Expansion '$expand' not implemented!"); } - } + } protected function createObject(array $data): int { AgentUtils::assign($data[Assignment::AGENT_ID], $data[Assignment::TASK_ID], $this->getCurrentUser()); diff --git a/src/inc/apiv2/model/agents.routes.php b/src/inc/apiv2/model/agents.routes.php index 0d993580b..b46a4779b 100644 --- a/src/inc/apiv2/model/agents.routes.php +++ b/src/inc/apiv2/model/agents.routes.php @@ -1,8 +1,5 @@ getId(), "=", Factory::getAccessGroupAgentFactory()); - $jF = new JoinFilter(Factory::getAccessGroupAgentFactory(), AccessGroup::ACCESS_GROUP_ID, AccessGroupAgent::ACCESS_GROUP_ID); - return $this->joinQuery(Factory::getAccessGroupFactory(), $qF, $jF); + return $this->getManyToOneRelationViaIntermediate( + $objects, + Agent::AGENT_ID, + Factory::getAccessGroupAgentFactory(), + AccessGroupAgent::AGENT_ID, + Factory::getAccessGroupFactory(), + AccessGroup::ACCESS_GROUP_ID + ); case 'agentstats': - $qF = new QueryFilter(AgentStat::AGENT_ID, $object->getId(), "="); - return $this->filterQuery(Factory::getAgentStatFactory(), $qF); + return $this->getManyToOneRelation( + $objects, + Agent::AGENT_ID, + Factory::getAgentStatFactory(), + AgentStat::AGENT_ID + ); + default: + throw new BadFunctionCallException("Internal error: Expansion '$expand' not implemented!"); } } diff --git a/src/inc/apiv2/model/agentstats.routes.php b/src/inc/apiv2/model/agentstats.routes.php index 8ccecae17..11c9a4c3a 100644 --- a/src/inc/apiv2/model/agentstats.routes.php +++ b/src/inc/apiv2/model/agentstats.routes.php @@ -1,7 +1,5 @@ get($object->getTaskId()); - return $this->obj2Array($obj); + return $this->getForeignKeyRelation( + $objects, + Chunk::TASK_ID, + Factory::getTaskFactory(), + Task::TASK_ID + ); + default: + throw new BadFunctionCallException("Internal error: Expansion '$expand' not implemented!"); } } diff --git a/src/inc/apiv2/model/configs.routes.php b/src/inc/apiv2/model/configs.routes.php index 02f536038..94a6a216f 100644 --- a/src/inc/apiv2/model/configs.routes.php +++ b/src/inc/apiv2/model/configs.routes.php @@ -2,6 +2,7 @@ use DBA\Factory; use DBA\Config; +use DBA\ConfigSection; require_once(dirname(__FILE__) . "/../common/AbstractModelAPI.class.php"); @@ -23,14 +24,23 @@ public function getExpandables(): array { return ['configSection']; } - protected function doExpand(object $object, string $expand): mixed { - assert($object instanceof Config); + protected function fetchExpandObjects(array $objects, string $expand): mixed { + /* Ensure we receive the proper type */ + array_walk($objects, function($obj) { assert($obj instanceof Config); }); + + /* Expand requested section */ switch($expand) { case 'configSection': - $obj = Factory::getConfigSectionFactory()->get($object->getConfigSectionId()); - return $this->obj2Array($obj); + return $this->getForeignKeyRelation( + $objects, + Config::CONFIG_SECTION_ID, + Factory::getConfigSectionFactory(), + ConfigSection::CONFIG_SECTION_ID, + ); + default: + throw new BadFunctionCallException("Internal error: Expansion '$expand' not implemented!"); } - } + } protected function createObject(array $data): int { /* Dummy code to implement abstract functions */ diff --git a/src/inc/apiv2/model/configsections.routes.php b/src/inc/apiv2/model/configsections.routes.php index 5c31c4b78..b9f21ee79 100644 --- a/src/inc/apiv2/model/configsections.routes.php +++ b/src/inc/apiv2/model/configsections.routes.php @@ -1,6 +1,4 @@ get($object->getCrackerBinaryTypeId()); - return $this->obj2Array($obj); + return $this->getForeignKeyRelation( + $objects, + CrackerBinary::CRACKER_BINARY_TYPE_ID, + Factory::getCrackerBinaryTypeFactory(), + CrackerBinaryType::CRACKER_BINARY_TYPE_ID, + ); + default: + throw new BadFunctionCallException("Internal error: Expansion '$expand' not implemented!"); } - } + } protected function createObject(array $data): int { CrackerUtils::createBinary( diff --git a/src/inc/apiv2/model/crackertypes.routes.php b/src/inc/apiv2/model/crackertypes.routes.php index 9b42824a0..6e7a730dc 100644 --- a/src/inc/apiv2/model/crackertypes.routes.php +++ b/src/inc/apiv2/model/crackertypes.routes.php @@ -22,14 +22,23 @@ public function getExpandables(): array { return ["crackerVersions"]; } - protected function doExpand(object $object, string $expand): mixed { - assert($object instanceof CrackerBinaryType); + protected function fetchExpandObjects(array $objects, string $expand): mixed { + /* Ensure we receive the proper type */ + array_walk($objects, function($obj) { assert($obj instanceof CrackerBinaryType); }); + + /* Expand requested section */ switch($expand) { case 'crackerVersions': - $qF = new QueryFilter(CrackerBinary::CRACKER_BINARY_TYPE_ID, $object->getId(), "="); - return $this->filterQuery(Factory::getCrackerBinaryFactory(), $qF); + return $this->getManyToOneRelation( + $objects, + CrackerBinaryType::CRACKER_BINARY_TYPE_ID, + Factory::getCrackerBinaryFactory(), + CrackerBinary::CRACKER_BINARY_TYPE_ID + ); + default: + throw new BadFunctionCallException("Internal error: Expansion '$expand' not implemented!"); } - } + } protected function createObject(array $data): int { CrackerUtils::createBinaryType($data[CrackerBinaryType::TYPE_NAME]); diff --git a/src/inc/apiv2/model/files.routes.php b/src/inc/apiv2/model/files.routes.php index de9299a5b..8926c1fed 100644 --- a/src/inc/apiv2/model/files.routes.php +++ b/src/inc/apiv2/model/files.routes.php @@ -1,4 +1,6 @@ get($object->getAccessGroupId()); - return $this->obj2Array($obj); + return $this->getForeignKeyRelation( + $objects, + File::ACCESS_GROUP_ID, + Factory::getAccessGroupFactory(), + AccessGroup::ACCESS_GROUP_ID + ); + default: + throw new BadFunctionCallException("Internal error: Expansion '$expand' not implemented!"); } } diff --git a/src/inc/apiv2/model/globalpermissiongroups.routes.php b/src/inc/apiv2/model/globalpermissiongroups.routes.php index 94cf65aff..e99c69868 100644 --- a/src/inc/apiv2/model/globalpermissiongroups.routes.php +++ b/src/inc/apiv2/model/globalpermissiongroups.routes.php @@ -1,7 +1,5 @@ getId(), "="); - return $this->filterQuery(Factory::getUserFactory(), $qF); + return $this->getManyToOneRelation( + $objects, + RightGroup::RIGHT_GROUP_ID, + Factory::getUserFactory(), + User::RIGHT_GROUP_ID + ); + default: + throw new BadFunctionCallException("Internal error: Expansion '$expand' not implemented!"); } - } + } /** * Rewrite permissions DB values to CRUD field values diff --git a/src/inc/apiv2/model/hashes.routes.php b/src/inc/apiv2/model/hashes.routes.php index c247be112..97d44cce7 100644 --- a/src/inc/apiv2/model/hashes.routes.php +++ b/src/inc/apiv2/model/hashes.routes.php @@ -1,7 +1,9 @@ get($object->getHashlistId()); - return $this->obj2Array($obj); + return $this->getForeignKeyRelation( + $objects, + Hash::HASHLIST_ID, + Factory::getHashListFactory(), + HashList::HASHLIST_ID + ); case 'chunk': - if (is_null($object->getChunkId())) { - /* Chunk expansions are optional, hence the chunk object could be empty */ - return []; - } else { - $obj = Factory::getChunkFactory()->get($object->getChunkId()); - return $this->obj2Array($obj); - } + return $this->getForeignKeyRelation( + $objects, + Hash::CHUNK_ID, + Factory::getChunkFactory(), + Chunk::CHUNK_ID + ); + default: + throw new BadFunctionCallException("Internal error: Expansion '$expand' not implemented!"); } - } + } protected function createObject(array $data): int { /* Dummy code to implement abstract functions */ diff --git a/src/inc/apiv2/model/hashlists.routes.php b/src/inc/apiv2/model/hashlists.routes.php index 2c519ceb0..159b4eeef 100644 --- a/src/inc/apiv2/model/hashlists.routes.php +++ b/src/inc/apiv2/model/hashlists.routes.php @@ -1,10 +1,11 @@ get($object->getAccessGroupId()); - return $this->obj2Array($obj); + return $this->getForeignKeyRelation( + $objects, + Hashlist::ACCESS_GROUP_ID, + Factory::getAccessGroupFactory(), + AccessGroup::ACCESS_GROUP_ID + ); case 'hashType': - $obj = Factory::getHashTypeFactory()->get($object->getHashTypeId()); - return $this->obj2Array($obj); + return $this->getForeignKeyRelation( + $objects, + Hashlist::HASH_TYPE_ID, + Factory::getHashTypeFactory(), + HashType::HASH_TYPE_ID + ); case 'hashes': - $qF = new QueryFilter(Hash::HASHLIST_ID, $object->getId(), "="); - return $this->filterQuery(Factory::getHashFactory(), $qF); + return $this->getManyToOneRelation( + $objects, + Hashlist::HASHLIST_ID, + Factory::getHashFactory(), + Hash::HASHLIST_ID + ); case 'hashlists': - $qF = new QueryFilter(HashlistHashlist::PARENT_HASHLIST_ID, $object->getId(), "=", Factory::getHashlistHashlistFactory()); - $jF = new JoinFilter(Factory::getHashlistHashlistFactory(), Hashlist::HASHLIST_ID, HashlistHashlist::HASHLIST_ID); - return $this->joinQuery(Factory::getHashlistFactory(), $qF, $jF); + /* PARENT_HASHLIST_ID in use in intermediate table */ + return $this->getManyToOneRelationViaIntermediate( + $objects, + Hashlist::HASHLIST_ID, + Factory::getHashlistHashlistFactory(), + HashlistHashlist::PARENT_HASHLIST_ID, + Factory::getHashlistFactory(), + Hashlist::HASHLIST_ID, + ); case 'tasks': - $qF = new QueryFilter(TaskWrapper::HASHLIST_ID, $object->getHashTypeId(), "=", Factory::getTaskWrapperFactory()); - $jF = new JoinFilter(Factory::getTaskWrapperFactory(), Task::TASK_WRAPPER_ID, TaskWrapper::TASK_WRAPPER_ID); - return $this->joinQuery(Factory::getTaskFactory(), $qF, $jF); + return $this->getManyToOneRelationViaIntermediate( + $objects, + Hashlist::HASHLIST_ID, + Factory::getTaskWrapperFactory(), + TaskWrapper::HASHLIST_ID, + Factory::getTaskFactory(), + Task::TASK_WRAPPER_ID, + ); + default: + throw new BadFunctionCallException("Internal error: Expansion '$expand' not implemented!"); } - } + } protected function getFilterACL(): array { return [new ContainFilter(Hashlist::ACCESS_GROUP_ID, Util::arrayOfIds(AccessUtils::getAccessGroupsOfUser($this->getCurrentUser())))]; diff --git a/src/inc/apiv2/model/hashtypes.routes.php b/src/inc/apiv2/model/hashtypes.routes.php index 8d414b306..5d10b8285 100644 --- a/src/inc/apiv2/model/hashtypes.routes.php +++ b/src/inc/apiv2/model/hashtypes.routes.php @@ -1,9 +1,4 @@ get($object->getAgentId()); - return $this->obj2Array($obj); - case 'healthCheck': - $obj = Factory::getHealthCheckFactory()->get($object->getHealthCheckId()); - return $this->obj2Array($obj); + return $this->getForeignKeyRelation( + $objects, + HealthCheckAgent::AGENT_ID, + Factory::getAgentFactory(), + Agent::AGENT_ID + ); + case 'healthCheck': + return $this->getForeignKeyRelation( + $objects, + HealthCheckAgent::HEALTH_CHECK_ID, + Factory::getHealthCheckFactory(), + HealthCheck::HEALTH_CHECK_ID + ); + default: + throw new BadFunctionCallException("Internal error: Expansion '$expand' not implemented!"); } - } + } protected function createObject(array $object): int { /* Dummy code to implement abstract functions */ diff --git a/src/inc/apiv2/model/healthchecks.routes.php b/src/inc/apiv2/model/healthchecks.routes.php index 383ccd45f..0a608d3a4 100644 --- a/src/inc/apiv2/model/healthchecks.routes.php +++ b/src/inc/apiv2/model/healthchecks.routes.php @@ -1,8 +1,9 @@ get($object->getCrackerBinaryId()); - return $this->obj2Array($obj); + return $this->getForeignKeyRelation( + $objects, + HealthCheck::CRACKER_BINARY_ID, + Factory::getCrackerBinaryFactory(), + CrackerBinary::CRACKER_BINARY_ID + ); case 'healthCheckAgents': - $qF = new QueryFilter(HealthCheck::HEALTH_CHECK_ID, $object->getId(), "="); - return $this->filterQuery(Factory::getHealthCheckAgentFactory(), $qF); + return $this->getManyToOneRelation( + $objects, + HealthCheck::HEALTH_CHECK_ID, + Factory::getHealthCheckAgentFactory(), + HealthCheckAgent::HEALTH_CHECK_ID + ); + default: + throw new BadFunctionCallException("Internal error: Expansion '$expand' not implemented!"); } - } + } protected function createObject(array $data): int { $obj = HealthUtils::createHealthCheck( diff --git a/src/inc/apiv2/model/notifications.routes.php b/src/inc/apiv2/model/notifications.routes.php index 4bab9b886..a3a262ce9 100644 --- a/src/inc/apiv2/model/notifications.routes.php +++ b/src/inc/apiv2/model/notifications.routes.php @@ -4,6 +4,7 @@ use DBA\QueryFilter; use DBA\NotificationSetting; +use DBA\User; require_once(dirname(__FILE__) . "/../common/AbstractModelAPI.class.php"); @@ -20,14 +21,23 @@ public function getExpandables(): array { return ['user']; } - protected function doExpand(object $object, string $expand): mixed { - assert($object instanceof NotificationSetting); + protected function fetchExpandObjects(array $objects, string $expand): mixed { + /* Ensure we receive the proper type */ + array_walk($objects, function($obj) { assert($obj instanceof NotificationSetting); }); + + /* Expand requested section */ switch($expand) { case 'user': - $obj = Factory::getUserFactory()->get($object->getUserId()); - return $this->obj2Array($obj); + return $this->getForeignKeyRelation( + $objects, + NotificationSetting::USER_ID, + Factory::getUserFactory(), + User::USER_ID + ); + default: + throw new BadFunctionCallException("Internal error: Expansion '$expand' not implemented!"); } - } + } public function getFormFields(): array { return ['actionFilter' => ['type' => 'str(256)']]; diff --git a/src/inc/apiv2/model/pretasks.routes.php b/src/inc/apiv2/model/pretasks.routes.php index d78a86f31..89a091c7f 100644 --- a/src/inc/apiv2/model/pretasks.routes.php +++ b/src/inc/apiv2/model/pretasks.routes.php @@ -24,15 +24,25 @@ public function getExpandables(): array { return ["pretaskFiles"]; } - protected function doExpand(object $object, string $expand): mixed { - assert($object instanceof PreTask); + protected function fetchExpandObjects(array $objects, string $expand): mixed { + /* Ensure we receive the proper type */ + array_walk($objects, function($obj) { assert($obj instanceof PreTask); }); + + /* Expand requested section */ switch($expand) { case 'pretaskFiles': - $qF = new QueryFilter(FilePretask::PRETASK_ID, $object->getId(), "=", Factory::getFilePretaskFactory()); - $jF = new JoinFilter(Factory::getFilePretaskFactory(), File::FILE_ID, FilePretask::FILE_ID); - return $this->joinQuery(Factory::getFileFactory(), $qF, $jF); + return $this->getManyToOneRelationViaIntermediate( + $objects, + Pretask::PRETASK_ID, + Factory::getFilePretaskFactory(), + FilePretask::PRETASK_ID, + Factory::getFileFactory(), + File::FILE_ID + ); + default: + throw new BadFunctionCallException("Internal error: Expansion '$expand' not implemented!"); } - } + } public function getFormFields(): array { // TODO Form declarations in more generic class to allow auto-generated OpenAPI specifications diff --git a/src/inc/apiv2/model/speeds.routes.php b/src/inc/apiv2/model/speeds.routes.php index 13f9cd394..3a2761bae 100644 --- a/src/inc/apiv2/model/speeds.routes.php +++ b/src/inc/apiv2/model/speeds.routes.php @@ -1,7 +1,9 @@ get($object->getAgentId()); - return $this->obj2Array($obj); + return $this->getForeignKeyRelation( + $objects, + Speed::AGENT_ID, + Factory::getAgentFactory(), + Agent::AGENT_ID + ); case 'task': - $obj = Factory::getTaskFactory()->get($object->getTaskId()); - return $this->obj2Array($obj); + return $this->getForeignKeyRelation( + $objects, + Speed::TASK_ID, + Factory::getTaskFactory(), + Task::TASK_ID + ); + default: + throw new BadFunctionCallException("Internal error: Expansion '$expand' not implemented!"); } - } + } protected function createObject(array $data): int { assert(False, "Speeds cannot be created via API"); diff --git a/src/inc/apiv2/model/supertasks.routes.php b/src/inc/apiv2/model/supertasks.routes.php index e0980929c..deec64475 100644 --- a/src/inc/apiv2/model/supertasks.routes.php +++ b/src/inc/apiv2/model/supertasks.routes.php @@ -1,6 +1,5 @@ getId(), "=", Factory::getSupertaskPretaskFactory()); - $jF = new JoinFilter(Factory::getSupertaskPretaskFactory(), Pretask::PRETASK_ID, SupertaskPretask::PRETASK_ID); - return $this->joinQuery(Factory::getPretaskFactory(), $qF, $jF); + return $this->getManyToOneRelationViaIntermediate( + $objects, + Supertask::SUPERTASK_ID, + Factory::getSupertaskPretaskFactory(), + SupertaskPretask::SUPERTASK_ID, + Factory::getPretaskFactory(), + Pretask::PRETASK_ID + ); + default: + throw new BadFunctionCallException("Internal error: Expansion '$expand' not implemented!"); } - } + } public function getFormFields(): array { return [ diff --git a/src/inc/apiv2/model/tasks.routes.php b/src/inc/apiv2/model/tasks.routes.php index bab40a859..4244ac679 100644 --- a/src/inc/apiv2/model/tasks.routes.php +++ b/src/inc/apiv2/model/tasks.routes.php @@ -1,10 +1,10 @@ getId(), "=", Factory::getAssignmentFactory()); - $jF = new JoinFilter(Factory::getAssignmentFactory(), Agent::AGENT_ID, Assignment::AGENT_ID); - return $this->joinQuery(Factory::getAgentFactory(), $qF, $jF); + return $this->getManyToOneRelationViaIntermediate( + $objects, + Task::TASK_ID, + Factory::getAssignmentFactory(), + Assignment::TASK_ID, + Factory::getAgentFactory(), + Agent::AGENT_ID + ); case 'crackerBinary': - $obj = Factory::getCrackerBinaryFactory()->get($object->getCrackerBinaryId()); - return $this->obj2Array($obj); + return $this->getForeignKeyRelation( + $objects, + Task::CRACKER_BINARY_ID, + Factory::getCrackerBinaryFactory(), + CrackerBinary::CRACKER_BINARY_ID + ); case 'crackerBinaryType': - $obj = Factory::getCrackerBinaryTypeFactory()->get($object->getCrackerBinaryTypeId()); - return $this->obj2Array($obj); + return $this->getForeignKeyRelation( + $objects, + Task::CRACKER_BINARY_TYPE_ID, + Factory::getCrackerBinaryTypeFactory(), + CrackerBinaryType::CRACKER_BINARY_TYPE_ID + ); case 'hashlist': - // Tasks are bit of a special case, as in the task the hashlist is not directly available. - // To get this information we need to join the task with the Hashlist and the TaskWrapper to get the Hashlist. - $qF = new QueryFilter(TaskWrapper::TASK_WRAPPER_ID, $object->getTaskWrapperId(), "=", Factory::getTaskWrapperFactory()); - $jF = new JoinFilter(Factory::getTaskWrapperFactory(), Hashlist::HASHLIST_ID, TaskWrapper::HASHLIST_ID); - return $this->joinQuery(Factory::getHashlistFactory(), $qF, $jF); + return $this->getManyToOneRelationViaIntermediate( + $objects, + Task::TASK_WRAPPER_ID, + Factory::getTaskWrapperFactory(), + TaskWrapper::TASK_WRAPPER_ID, + Factory::getHashlistFactory(), + Hashlist::HASHLIST_ID + ); case 'speeds': - $qF = new QueryFilter(Speed::TASK_ID, $object->getId(), "="); - return $this->filterQuery(Factory::getSpeedFactory(), $qF); + return $this->getManyToOneRelation( + $objects, + Task::TASK_ID, + Factory::getSpeedFactory(), + Speed::TASK_ID + ); case 'files': - $qF = new QueryFilter(FileTask::TASK_ID, $object->getId(), "=", Factory::getFileTaskFactory()); - $jF = new JoinFilter(Factory::getFileTaskFactory(), File::FILE_ID, FileTask::FILE_ID); - return $this->joinQuery(Factory::getFileFactory(), $qF, $jF); + return $this->getManyToOneRelationViaIntermediate( + $objects, + Task::TASK_ID, + Factory::getFileTaskFactory(), + FileTask::TASK_ID, + Factory::getFileFactory(), + File::FILE_ID + ); + default: + throw new BadFunctionCallException("Internal error: Expansion '$expand' not implemented!"); } - } - + } + public function getFormFields(): array { - // TODO Form declarations in more generic class to allow auto-generated OpenAPI specifications - return [ - "hashlistId" => ['type' => 'int'], - "files" => ['type' => 'array', 'subtype' => 'int'], - ]; + // TODO Form declarations in more generic class to allow auto-generated OpenAPI specifications + return [ + "hashlistId" => ['type' => 'int'], + "files" => ['type' => 'array', 'subtype' => 'int'], + ]; } protected function createObject(array $data): int { diff --git a/src/inc/apiv2/model/taskwrappers.routes.php b/src/inc/apiv2/model/taskwrappers.routes.php index c8a3f8b39..9d03f0007 100644 --- a/src/inc/apiv2/model/taskwrappers.routes.php +++ b/src/inc/apiv2/model/taskwrappers.routes.php @@ -1,4 +1,6 @@ get($object->getAccessGroupId()); - return $this->obj2Array($obj); + return $this->getForeignKeyRelation( + $objects, + TaskWrapper::ACCESS_GROUP_ID, + Factory::getAccessGroupFactory(), + AccessGroup::ACCESS_GROUP_ID + ); case 'tasks': - $qF = new QueryFilter(TaskWrapper::TASK_WRAPPER_ID, $object->getId(), "=", Factory::getTaskWrapperFactory()); - $jF = new JoinFilter(Factory::getTaskWrapperFactory(), Task::TASK_WRAPPER_ID, TaskWrapper::TASK_WRAPPER_ID); - return $this->joinQuery(Factory::getTaskFactory(), $qF, $jF); + return $this->getManyToOneRelation( + $objects, + TaskWrapper::TASK_WRAPPER_ID, + Factory::getTaskFactory(), + Task::TASK_WRAPPER_ID + ); + default: + throw new BadFunctionCallException("Internal error: Expansion '$expand' not implemented!"); } } diff --git a/src/inc/apiv2/model/users.routes.php b/src/inc/apiv2/model/users.routes.php index 817e89504..d1214fe5a 100644 --- a/src/inc/apiv2/model/users.routes.php +++ b/src/inc/apiv2/model/users.routes.php @@ -1,11 +1,11 @@ getId(), "=", Factory::getAccessGroupUserFactory()); - $jF = new JoinFilter(Factory::getAccessGroupUserFactory(), AccessGroup::ACCESS_GROUP_ID, AccessGroupUser::ACCESS_GROUP_ID); - return $this->joinQuery(Factory::getAccessGroupFactory(), $qF, $jF); - case 'globalPermissionGroup': - $obj = Factory::getRightGroupFactory()->get($object->getRightGroupId()); - return $this->obj2Array($obj); + return $this->getManyToOneRelationViaIntermediate( + $objects, + User::USER_ID, + Factory::getAccessGroupUserFactory(), + AccessGroupUser::USER_ID, + Factory::getAccessGroupFactory(), + AccessGroup::ACCESS_GROUP_ID + ); + case 'globalPermissionGroup': + return $this->getForeignKeyRelation( + $objects, + User::RIGHT_GROUP_ID, + Factory::getRightGroupFactory(), + RightGroup::RIGHT_GROUP_ID + ); + default: + throw new BadFunctionCallException("Internal error: Expansion '$expand' not implemented!"); } } From bbaf64159410ba1d0719ca6b02b9adfe86799c2b Mon Sep 17 00:00:00 2001 From: Sein Coray Date: Tue, 13 Feb 2024 16:15:22 +0100 Subject: [PATCH 06/14] implemented test for the exportCrackedHashes helper implemented exportCrackedHashes helper on new API --- ci/apiv2/hashtopolis.py | 7 ++++ ci/apiv2/test_hashlist.py | 12 +++++- src/api/v2/index.php | 1 + .../helper/exportCrackedHashes.routes.php | 39 +++++++++++++++++++ 4 files changed, 58 insertions(+), 1 deletion(-) create mode 100644 src/inc/apiv2/helper/exportCrackedHashes.routes.php diff --git a/ci/apiv2/hashtopolis.py b/ci/apiv2/hashtopolis.py index 627f93303..7e1098c5c 100644 --- a/ci/apiv2/hashtopolis.py +++ b/ci/apiv2/hashtopolis.py @@ -683,3 +683,10 @@ def purge_task(self, task): 'taskId': task.id, } return self._helper_request("purgeTask", payload) + + def export_cracked_hashes(self, hashlist): + payload = { + 'hashlistId': hashlist.id, + } + response = self._helper_request("exportCrackedHashes", payload) + return File(**response['data']) diff --git a/ci/apiv2/test_hashlist.py b/ci/apiv2/test_hashlist.py index a2d5225fe..e305a8e4d 100644 --- a/ci/apiv2/test_hashlist.py +++ b/ci/apiv2/test_hashlist.py @@ -1,4 +1,4 @@ -from hashtopolis import Hashlist, Helper +from hashtopolis import Hashlist, Helper, File from utils import BaseTest @@ -38,6 +38,16 @@ def test_create_alternative_hashtype(self): model_obj = self.create_test_object(file_id='003') self._test_create(model_obj) + def test_export_cracked_hashes(self): + model_obj = self.create_test_object(file_id='001') + + helper = Helper() + file = helper.export_cracked_hashes(model_obj) + + obj = File.objects.get(fileId=file.id) + self.assertEqual(int(file.id), obj.id) + self.assertIn('Pre-cracked_', obj.filename) + def test_helper_create_superhashlist(self): hashlists = [self.create_test_object() for _ in range(2)] diff --git a/src/api/v2/index.php b/src/api/v2/index.php index c02c7e82a..1d22b6b74 100644 --- a/src/api/v2/index.php +++ b/src/api/v2/index.php @@ -269,6 +269,7 @@ public function process(Request $request, RequestHandler $handler): Response { require __DIR__ . "/../../inc/apiv2/helper/abortChunk.routes.php"; require __DIR__ . "/../../inc/apiv2/helper/createSupertask.routes.php"; require __DIR__ . "/../../inc/apiv2/helper/createSuperHashlist.routes.php"; +require __DIR__ . "/../../inc/apiv2/helper/exportCrackedHashes.routes.php"; require __DIR__ . "/../../inc/apiv2/helper/importFile.routes.php"; require __DIR__ . "/../../inc/apiv2/helper/purgeTask.routes.php"; require __DIR__ . "/../../inc/apiv2/helper/resetChunk.routes.php"; diff --git a/src/inc/apiv2/helper/exportCrackedHashes.routes.php b/src/inc/apiv2/helper/exportCrackedHashes.routes.php new file mode 100644 index 000000000..6e4def21c --- /dev/null +++ b/src/inc/apiv2/helper/exportCrackedHashes.routes.php @@ -0,0 +1,39 @@ + ["type" => "int"], + ]; + } + + public function actionPost($data): array|null { + $hashlist = self::getHashlist($data[Hashlist::HASHLIST_ID]); + + $file = HashlistUtils::export($hashlist->getId(), $this->getCurrentUser()); + return $this->object2Array($file); + } +} + +ExportCrackedHashesHelperAPI::register($app); \ No newline at end of file From e095b43409fffc913ef7f4e90938333dbb14308c Mon Sep 17 00:00:00 2001 From: Sein Coray Date: Tue, 13 Feb 2024 17:17:16 +0100 Subject: [PATCH 07/14] lastInsertId() from MySQL returns the new ID as a string instead of an int which causes issues with the new API not sending an int where an int is expected. as the autoincremented IDs in MySQL always will be of type int, we cast it after retrieving --- src/dba/AbstractModelFactory.class.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/dba/AbstractModelFactory.class.php b/src/dba/AbstractModelFactory.class.php index 875881528..c6c596e84 100755 --- a/src/dba/AbstractModelFactory.class.php +++ b/src/dba/AbstractModelFactory.class.php @@ -114,7 +114,7 @@ public function save($model) { $stmt = $dbh->prepare($query); $stmt->execute($vals); - $id = $dbh->lastInsertId(); + $id = intval($dbh->lastInsertId()); if ($id != 0) { $model->setId($id); return $model; From 955b99c374d26f54468db533c0bc34358bdcecee Mon Sep 17 00:00:00 2001 From: Sein Coray Date: Sat, 17 Feb 2024 22:24:43 +0100 Subject: [PATCH 08/14] implemented importCrackedHashes helper added tests to test different import scenarios --- ci/apiv2/hashtopolis.py | 9 +++ ci/apiv2/test_hashlist.py | 58 +++++++++++++++++++ src/api/v2/index.php | 1 + .../helper/importCrackedHashes.routes.php | 46 +++++++++++++++ 4 files changed, 114 insertions(+) create mode 100644 src/inc/apiv2/helper/importCrackedHashes.routes.php diff --git a/ci/apiv2/hashtopolis.py b/ci/apiv2/hashtopolis.py index 7e1098c5c..8b7db8ed8 100644 --- a/ci/apiv2/hashtopolis.py +++ b/ci/apiv2/hashtopolis.py @@ -690,3 +690,12 @@ def export_cracked_hashes(self, hashlist): } response = self._helper_request("exportCrackedHashes", payload) return File(**response['data']) + + def import_cracked_hashes(self, hashlist, source_data, separator): + payload = { + 'hashlistId': hashlist.id, + 'sourceData': source_data, + 'separator': separator, + } + response = self._helper_request("importCrackedHashes", payload) + return response['data'] diff --git a/ci/apiv2/test_hashlist.py b/ci/apiv2/test_hashlist.py index e305a8e4d..63743a176 100644 --- a/ci/apiv2/test_hashlist.py +++ b/ci/apiv2/test_hashlist.py @@ -48,6 +48,64 @@ def test_export_cracked_hashes(self): self.assertEqual(int(file.id), obj.id) self.assertIn('Pre-cracked_', obj.filename) + def test_import_cracked_hashes(self): + model_obj = self.create_test_object(file_id='001') + + cracked = "cc03e747a6afbbcbf8be7668acfebee5:test123" + + helper = Helper() + result = helper.import_cracked_hashes(model_obj, cracked, ':') + + self.assertEqual(result['totalLines'], 1) + self.assertEqual(result['newCracked'], 1) + + obj = Hashlist.objects.get(hashlistId=model_obj.id) + self.assertEqual(obj.cracked, 1) + + def test_import_cracked_hashes_invalid(self): + model_obj = self.create_test_object(file_id='001') + + cracked = "cc03e747a6afbbcbf8be7668acfebee5__test123" + + helper = Helper() + result = helper.import_cracked_hashes(model_obj, cracked, ':') + + self.assertEqual(result['totalLines'], 1) + self.assertEqual(result['invalid'], 1) + + obj = Hashlist.objects.get(hashlistId=model_obj.id) + self.assertEqual(obj.cracked, 0) + + def test_import_cracked_hashes_notfound(self): + model_obj = self.create_test_object(file_id='001') + + cracked = "ffffffffffffffffffffffffffffffff:test123" + + helper = Helper() + result = helper.import_cracked_hashes(model_obj, cracked, ':') + + self.assertEqual(result['totalLines'], 1) + self.assertEqual(result['notFound'], 1) + + obj = Hashlist.objects.get(hashlistId=model_obj.id) + self.assertEqual(obj.cracked, 0) + + def test_import_cracked_hashes_already_cracked(self): + model_obj = self.create_test_object(file_id='001') + + cracked = "cc03e747a6afbbcbf8be7668acfebee5:test123" + + helper = Helper() + helper.import_cracked_hashes(model_obj, cracked, ':') + + result = helper.import_cracked_hashes(model_obj, cracked, ':') + + self.assertEqual(result['totalLines'], 1) + self.assertEqual(result['alreadyCracked'], 1) + + obj = Hashlist.objects.get(hashlistId=model_obj.id) + self.assertEqual(obj.cracked, 1) + def test_helper_create_superhashlist(self): hashlists = [self.create_test_object() for _ in range(2)] diff --git a/src/api/v2/index.php b/src/api/v2/index.php index 1d22b6b74..f47e95064 100644 --- a/src/api/v2/index.php +++ b/src/api/v2/index.php @@ -270,6 +270,7 @@ public function process(Request $request, RequestHandler $handler): Response { require __DIR__ . "/../../inc/apiv2/helper/createSupertask.routes.php"; require __DIR__ . "/../../inc/apiv2/helper/createSuperHashlist.routes.php"; require __DIR__ . "/../../inc/apiv2/helper/exportCrackedHashes.routes.php"; +require __DIR__ . "/../../inc/apiv2/helper/importCrackedHashes.routes.php"; require __DIR__ . "/../../inc/apiv2/helper/importFile.routes.php"; require __DIR__ . "/../../inc/apiv2/helper/purgeTask.routes.php"; require __DIR__ . "/../../inc/apiv2/helper/resetChunk.routes.php"; diff --git a/src/inc/apiv2/helper/importCrackedHashes.routes.php b/src/inc/apiv2/helper/importCrackedHashes.routes.php new file mode 100644 index 000000000..5be6033e6 --- /dev/null +++ b/src/inc/apiv2/helper/importCrackedHashes.routes.php @@ -0,0 +1,46 @@ + ["type" => "int"], + "sourceData" => ['type' => 'str'], + "separator" => ['type' => 'str'], + ]; + } + + public function actionPost($data): array|null { + $hashlist = self::getHashlist($data[Hashlist::HASHLIST_ID]); + + $result = HashlistUtils::processZap($hashlist->getId(), $data["separator"], "paste", ["hashfield" => $data["sourceData"]], [], $this->getCurrentUser()); + + return [ + "totalLines" => $result[0], + "newCracked" => $result[1], + "alreadyCracked" => $result[2], + "invalid" => $result[3], + "notFound" => $result[4], + "processTime" => $result[5], + "tooLongPlaintexts" => $result[6], + ]; + } +} + +ImportCrackedHashesHelperAPI::register($app); \ No newline at end of file From c1912c7f6c6be5092edbe40d3c6642539ff96691 Mon Sep 17 00:00:00 2001 From: Sein Coray Date: Sat, 17 Feb 2024 22:37:29 +0100 Subject: [PATCH 09/14] implemented the two remaining hashlist helpers to export wordlist and left lists for hashlists added tests for both helper actions --- ci/apiv2/hashtopolis.py | 14 +++++++ ci/apiv2/test_hashlist.py | 24 ++++++++++++ src/api/v2/index.php | 2 + .../helper/exportCrackedHashes.routes.php | 2 - .../apiv2/helper/exportLeftHashes.routes.php | 38 +++++++++++++++++++ .../apiv2/helper/exportWordlist.routes.php | 38 +++++++++++++++++++ src/inc/utils/HashlistUtils.class.php | 5 ++- 7 files changed, 119 insertions(+), 4 deletions(-) create mode 100644 src/inc/apiv2/helper/exportLeftHashes.routes.php create mode 100644 src/inc/apiv2/helper/exportWordlist.routes.php diff --git a/ci/apiv2/hashtopolis.py b/ci/apiv2/hashtopolis.py index 8b7db8ed8..a300fae4e 100644 --- a/ci/apiv2/hashtopolis.py +++ b/ci/apiv2/hashtopolis.py @@ -691,6 +691,20 @@ def export_cracked_hashes(self, hashlist): response = self._helper_request("exportCrackedHashes", payload) return File(**response['data']) + def export_left_hashes(self, hashlist): + payload = { + 'hashlistId': hashlist.id, + } + response = self._helper_request("exportLeftHashes", payload) + return File(**response['data']) + + def export_wordlist(self, hashlist): + payload = { + 'hashlistId': hashlist.id, + } + response = self._helper_request("exportWordlist", payload) + return File(**response['data']) + def import_cracked_hashes(self, hashlist, source_data, separator): payload = { 'hashlistId': hashlist.id, diff --git a/ci/apiv2/test_hashlist.py b/ci/apiv2/test_hashlist.py index 63743a176..b3323fee2 100644 --- a/ci/apiv2/test_hashlist.py +++ b/ci/apiv2/test_hashlist.py @@ -48,6 +48,30 @@ def test_export_cracked_hashes(self): self.assertEqual(int(file.id), obj.id) self.assertIn('Pre-cracked_', obj.filename) + def test_export_left_hashes(self): + model_obj = self.create_test_object(file_id='001') + + helper = Helper() + file = helper.export_left_hashes(model_obj) + + obj = File.objects.get(fileId=file.id) + self.assertEqual(int(file.id), obj.id) + self.assertIn('Leftlist_', obj.filename) + + def test_export_wordlist(self): + model_obj = self.create_test_object(file_id='001') + + cracked = "cc03e747a6afbbcbf8be7668acfebee5:test123" + + helper = Helper() + helper.import_cracked_hashes(model_obj, cracked, ':') + + file = helper.export_wordlist(model_obj) + + obj = File.objects.get(fileId=file.id) + self.assertEqual(int(file.id), obj.id) + self.assertIn('Wordlist_', obj.filename) + def test_import_cracked_hashes(self): model_obj = self.create_test_object(file_id='001') diff --git a/src/api/v2/index.php b/src/api/v2/index.php index f47e95064..25bdecb83 100644 --- a/src/api/v2/index.php +++ b/src/api/v2/index.php @@ -270,6 +270,8 @@ public function process(Request $request, RequestHandler $handler): Response { require __DIR__ . "/../../inc/apiv2/helper/createSupertask.routes.php"; require __DIR__ . "/../../inc/apiv2/helper/createSuperHashlist.routes.php"; require __DIR__ . "/../../inc/apiv2/helper/exportCrackedHashes.routes.php"; +require __DIR__ . "/../../inc/apiv2/helper/exportLeftHashes.routes.php"; +require __DIR__ . "/../../inc/apiv2/helper/exportWordlist.routes.php"; require __DIR__ . "/../../inc/apiv2/helper/importCrackedHashes.routes.php"; require __DIR__ . "/../../inc/apiv2/helper/importFile.routes.php"; require __DIR__ . "/../../inc/apiv2/helper/purgeTask.routes.php"; diff --git a/src/inc/apiv2/helper/exportCrackedHashes.routes.php b/src/inc/apiv2/helper/exportCrackedHashes.routes.php index 6e4def21c..9721b82c6 100644 --- a/src/inc/apiv2/helper/exportCrackedHashes.routes.php +++ b/src/inc/apiv2/helper/exportCrackedHashes.routes.php @@ -1,9 +1,7 @@ ["type" => "int"], + ]; + } + + public function actionPost($data): array|null { + $hashlist = self::getHashlist($data[Hashlist::HASHLIST_ID]); + + $file = HashlistUtils::leftlist($hashlist->getId(), $this->getCurrentUser()); + + return $this->object2Array($file); + } +} + +ExportLeftHashesHelperAPI::register($app); \ No newline at end of file diff --git a/src/inc/apiv2/helper/exportWordlist.routes.php b/src/inc/apiv2/helper/exportWordlist.routes.php new file mode 100644 index 000000000..9f53e1d83 --- /dev/null +++ b/src/inc/apiv2/helper/exportWordlist.routes.php @@ -0,0 +1,38 @@ + ["type" => "int"], + ]; + } + + public function actionPost($data): array|null { + $hashlist = self::getHashlist($data[Hashlist::HASHLIST_ID]); + + $arr = HashlistUtils::createWordlists($hashlist->getId(), $this->getCurrentUser()); + + return $this->object2Array($arr[2]); + } +} + +ExportWordlistHelperAPI::register($app); \ No newline at end of file diff --git a/src/inc/utils/HashlistUtils.class.php b/src/inc/utils/HashlistUtils.class.php index 3fc163af8..489c24747 100644 --- a/src/inc/utils/HashlistUtils.class.php +++ b/src/inc/utils/HashlistUtils.class.php @@ -212,8 +212,9 @@ public static function createWordlists($hashlistId, $user) { fclose($wordlistFile); //add file to files list - $file = new File(null, $wordlistName, Util::filesize($wordlistFilename), $hashlist->getIsSecret(), 0, $hashlist->getAccessGroupId(), null); - Factory::getFileFactory()->save($file); + $file = new File(null, $wordlistName, Util::filesize($wordlistFilename), $hashlist->getIsSecret(), 0, $hashlist->getAccessGroupId(), $wordCount); + $file = Factory::getFileFactory()->save($file); + # TODO: returning wordCount and wordlistName are not really required here as the name and the count are already given in the file object return [$wordCount, $wordlistName, $file]; } From 422e9404bcbe13226319f1699ca42e744eefb619 Mon Sep 17 00:00:00 2001 From: Sein Coray Date: Sat, 17 Feb 2024 22:46:22 +0100 Subject: [PATCH 10/14] make sure exported files get deleted after tests --- ci/apiv2/test_hashlist.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/ci/apiv2/test_hashlist.py b/ci/apiv2/test_hashlist.py index b3323fee2..5ddd36d1a 100644 --- a/ci/apiv2/test_hashlist.py +++ b/ci/apiv2/test_hashlist.py @@ -48,6 +48,8 @@ def test_export_cracked_hashes(self): self.assertEqual(int(file.id), obj.id) self.assertIn('Pre-cracked_', obj.filename) + self.delete_after_test(obj) + def test_export_left_hashes(self): model_obj = self.create_test_object(file_id='001') @@ -58,6 +60,8 @@ def test_export_left_hashes(self): self.assertEqual(int(file.id), obj.id) self.assertIn('Leftlist_', obj.filename) + self.delete_after_test(obj) + def test_export_wordlist(self): model_obj = self.create_test_object(file_id='001') @@ -72,6 +76,8 @@ def test_export_wordlist(self): self.assertEqual(int(file.id), obj.id) self.assertIn('Wordlist_', obj.filename) + self.delete_after_test(obj) + def test_import_cracked_hashes(self): model_obj = self.create_test_object(file_id='001') From 3163f6d2129664a4f7edbfbd84bba066ffe1a9ec Mon Sep 17 00:00:00 2001 From: s3in!c Date: Wed, 21 Feb 2024 08:17:24 +0000 Subject: [PATCH 11/14] added TODO message for non-standard reply on helper --- src/inc/apiv2/helper/importCrackedHashes.routes.php | 1 + 1 file changed, 1 insertion(+) diff --git a/src/inc/apiv2/helper/importCrackedHashes.routes.php b/src/inc/apiv2/helper/importCrackedHashes.routes.php index 5be6033e6..ee375d2d3 100644 --- a/src/inc/apiv2/helper/importCrackedHashes.routes.php +++ b/src/inc/apiv2/helper/importCrackedHashes.routes.php @@ -31,6 +31,7 @@ public function actionPost($data): array|null { $result = HashlistUtils::processZap($hashlist->getId(), $data["separator"], "paste", ["hashfield" => $data["sourceData"]], [], $this->getCurrentUser()); + # TODO: Check how to handle custom return messages that are not object, probably we want that to be in some kind of standardized form. return [ "totalLines" => $result[0], "newCracked" => $result[1], From be80815f12eccf789668574347780a62c66fb42a Mon Sep 17 00:00:00 2001 From: Sein Coray Date: Sat, 24 Feb 2024 12:20:53 +0100 Subject: [PATCH 12/14] added helper functions for assigning and unassigning agents from tasks (#1044) * added helper functions for assigning and unassigning agents from tasks * pass delete=True for test object creation * call tearDown for other test class manually to clean up --- ci/apiv2/hashtopolis.py | 15 ++++++++ ci/apiv2/test_agent.py | 21 ++++++++++- src/api/v2/index.php | 2 ++ src/inc/apiv2/helper/assignAgent.routes.php | 36 +++++++++++++++++++ src/inc/apiv2/helper/unassignAgent.routes.php | 35 ++++++++++++++++++ 5 files changed, 108 insertions(+), 1 deletion(-) create mode 100644 src/inc/apiv2/helper/assignAgent.routes.php create mode 100644 src/inc/apiv2/helper/unassignAgent.routes.php diff --git a/ci/apiv2/hashtopolis.py b/ci/apiv2/hashtopolis.py index a300fae4e..94e122f36 100644 --- a/ci/apiv2/hashtopolis.py +++ b/ci/apiv2/hashtopolis.py @@ -713,3 +713,18 @@ def import_cracked_hashes(self, hashlist, source_data, separator): } response = self._helper_request("importCrackedHashes", payload) return response['data'] + + def unassign_agent(self, agent): + payload = { + 'agentId': agent.id, + } + response = self._helper_request("unassignAgent", payload) + return response['data'] + + def assign_agent(self, agent, task): + payload = { + 'agentId': agent.id, + 'taskId': task.id, + } + response = self._helper_request("assignAgent", payload) + return response['data'] diff --git a/ci/apiv2/test_agent.py b/ci/apiv2/test_agent.py index e8af4c02d..1aae2c315 100644 --- a/ci/apiv2/test_agent.py +++ b/ci/apiv2/test_agent.py @@ -1,4 +1,5 @@ -from hashtopolis import Agent +from test_task import TaskTest +from hashtopolis import Agent, Helper from hashtopolis import HashtopolisError from utils import BaseTest @@ -28,3 +29,21 @@ def test_expandables(self): model_obj = self.create_test_object() expandables = ['accessGroups', 'agentstats'] self._test_expandables(model_obj, expandables) + + def test_assign_unassign_agent(self): + agent_obj = self.create_test_object() + + task_test = TaskTest() + task_obj = task_test.create_test_object(delete=True) + + helper = Helper() + + result = helper.assign_agent(agent=agent_obj, task=task_obj) + + self.assertEqual(result['assign'], 'success') + + result = helper.unassign_agent(agent=agent_obj) + + self.assertEqual(result['unassign'], 'success') + + task_test.tearDown() diff --git a/src/api/v2/index.php b/src/api/v2/index.php index 25bdecb83..d73fda8ba 100644 --- a/src/api/v2/index.php +++ b/src/api/v2/index.php @@ -267,6 +267,7 @@ public function process(Request $request, RequestHandler $handler): Response { require __DIR__ . "/../../inc/apiv2/model/vouchers.routes.php"; require __DIR__ . "/../../inc/apiv2/helper/abortChunk.routes.php"; +require __DIR__ . "/../../inc/apiv2/helper/assignAgent.routes.php"; require __DIR__ . "/../../inc/apiv2/helper/createSupertask.routes.php"; require __DIR__ . "/../../inc/apiv2/helper/createSuperHashlist.routes.php"; require __DIR__ . "/../../inc/apiv2/helper/exportCrackedHashes.routes.php"; @@ -277,5 +278,6 @@ public function process(Request $request, RequestHandler $handler): Response { require __DIR__ . "/../../inc/apiv2/helper/purgeTask.routes.php"; require __DIR__ . "/../../inc/apiv2/helper/resetChunk.routes.php"; require __DIR__ . "/../../inc/apiv2/helper/setUserPassword.routes.php"; +require __DIR__ . "/../../inc/apiv2/helper/unassignAgent.routes.php"; $app->run(); diff --git a/src/inc/apiv2/helper/assignAgent.routes.php b/src/inc/apiv2/helper/assignAgent.routes.php new file mode 100644 index 000000000..7597412b0 --- /dev/null +++ b/src/inc/apiv2/helper/assignAgent.routes.php @@ -0,0 +1,36 @@ + ["type" => "int"], + Task::TASK_ID => ["type" => "int"], + ]; + } + + public function actionPost($data): array|null { + AgentUtils::assign($data[Agent::AGENT_ID], $data[Task::TASK_ID], $this->getCurrentUser()); + + # TODO: Check how to handle custom return messages that are not object, probably we want that to be in some kind of standardized form. + return ["assign" => "success"]; + } +} + +AssignAgentHelperAPI::register($app); \ No newline at end of file diff --git a/src/inc/apiv2/helper/unassignAgent.routes.php b/src/inc/apiv2/helper/unassignAgent.routes.php new file mode 100644 index 000000000..cfdda8080 --- /dev/null +++ b/src/inc/apiv2/helper/unassignAgent.routes.php @@ -0,0 +1,35 @@ + ["type" => "int"], + ]; + } + + public function actionPost($data): array|null { + AgentUtils::assign($data[Agent::AGENT_ID], 0, $this->getCurrentUser()); + + # TODO: Check how to handle custom return messages that are not object, probably we want that to be in some kind of standardized form. + return ["unassign" => "success"]; + } +} + +UnassignAgentHelperAPI::register($app); \ No newline at end of file From 6a7836f5c98e528cb9974b2c7b00e0724eec57ae Mon Sep 17 00:00:00 2001 From: Sein Coray Date: Sat, 24 Feb 2024 12:26:29 +0100 Subject: [PATCH 13/14] implemented helper for recounting the lines in files (#1045) --- ci/apiv2/hashtopolis.py | 8 ++++ ci/apiv2/test_file.py | 13 ++++++- src/api/v2/index.php | 1 + .../apiv2/helper/recountFileLines.routes.php | 37 +++++++++++++++++++ 4 files changed, 58 insertions(+), 1 deletion(-) create mode 100644 src/inc/apiv2/helper/recountFileLines.routes.php diff --git a/ci/apiv2/hashtopolis.py b/ci/apiv2/hashtopolis.py index 94e122f36..600fa7abb 100644 --- a/ci/apiv2/hashtopolis.py +++ b/ci/apiv2/hashtopolis.py @@ -714,6 +714,14 @@ def import_cracked_hashes(self, hashlist, source_data, separator): response = self._helper_request("importCrackedHashes", payload) return response['data'] + + def recount_file_lines(self, file): + payload = { + 'fileId': file.id, + } + response = self._helper_request("recountFileLines", payload) + return File(**response['data']) + def unassign_agent(self, agent): payload = { 'agentId': agent.id, diff --git a/ci/apiv2/test_file.py b/ci/apiv2/test_file.py index 3de09f18a..2262ed6cd 100644 --- a/ci/apiv2/test_file.py +++ b/ci/apiv2/test_file.py @@ -1,4 +1,4 @@ -from hashtopolis import File +from hashtopolis import File, Helper from utils import BaseTest @@ -28,3 +28,14 @@ def test_expandables(self): def test_create_binary(self): model_obj = self.create_test_object(compress=True) self._test_create(model_obj) + + def test_recount_wordlist(self): + # Note: After the object creation, the line count is already updated, but afterward it is immutable on the API. + # There the test just check that the API function is callable and returns the file, but the count is + # already the same beforehand. + model_obj = self.create_test_object() + + helper = Helper() + file = helper.recount_file_lines(file=model_obj) + + self.assertEqual(file.lineCount, 3) diff --git a/src/api/v2/index.php b/src/api/v2/index.php index d73fda8ba..ef3a6b0ab 100644 --- a/src/api/v2/index.php +++ b/src/api/v2/index.php @@ -276,6 +276,7 @@ public function process(Request $request, RequestHandler $handler): Response { require __DIR__ . "/../../inc/apiv2/helper/importCrackedHashes.routes.php"; require __DIR__ . "/../../inc/apiv2/helper/importFile.routes.php"; require __DIR__ . "/../../inc/apiv2/helper/purgeTask.routes.php"; +require __DIR__ . "/../../inc/apiv2/helper/recountFileLines.routes.php"; require __DIR__ . "/../../inc/apiv2/helper/resetChunk.routes.php"; require __DIR__ . "/../../inc/apiv2/helper/setUserPassword.routes.php"; require __DIR__ . "/../../inc/apiv2/helper/unassignAgent.routes.php"; diff --git a/src/inc/apiv2/helper/recountFileLines.routes.php b/src/inc/apiv2/helper/recountFileLines.routes.php new file mode 100644 index 000000000..f737b984e --- /dev/null +++ b/src/inc/apiv2/helper/recountFileLines.routes.php @@ -0,0 +1,37 @@ + ["type" => "int"], + ]; + } + + public function actionPost($data): array|null { + // first retrieve the file, as fileCountLines does not check any permissions, therfore to be sure call getFile() first, even if it is not required technically + FileUtils::getFile($data[File::FILE_ID], $this->getCurrentUser()); + + FileUtils::fileCountLines($data[File::FILE_ID]); + + return $this->object2Array(FileUtils::getFile($data[File::FILE_ID], $this->getCurrentUser())); + } +} + +RecountFileFilesHelperAPI::register($app); \ No newline at end of file From ddb3cc949b4e5c643e55f72f449b1621e077ebbb Mon Sep 17 00:00:00 2001 From: Sein Coray Date: Tue, 19 Mar 2024 20:11:32 +0100 Subject: [PATCH 14/14] updated agent to version 0.7.2 --- src/bin/hashtopolis.zip | Bin 28972 -> 28980 bytes src/install/hashtopolis.sql | 2 +- ...v0.14.1.php => update_v0.14.x_v0.14.2.php} | 7 ++++++- 3 files changed, 7 insertions(+), 2 deletions(-) rename src/install/updates/{update_v0.14.0_v0.14.1.php => update_v0.14.x_v0.14.2.php} (71%) diff --git a/src/bin/hashtopolis.zip b/src/bin/hashtopolis.zip index fe5a7bf443e4524b7d4d4265be7538842cc6d615..cec2fba8de948f3b89f14c83eed03dd8c4ae6657 100644 GIT binary patch delta 9707 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"ALTER TABLE `TaskWrapper` ADD `maxAgents` INT(11) NOT NULL;"); } $EXECUTED["v0.14.x_maxAgents_taskwrapper"] = true; -} \ No newline at end of file +} + +if (!isset($PRESENT["v0.14.x_agentBinaries"])) { + Util::checkAgentVersion("python", "0.7.2", true); + $EXECUTED["v0.14.x_agentBinaries"] = true; +}