From 0eebfa727db33d09080cf47bcc3632de05ace73f Mon Sep 17 00:00:00 2001 From: s3inlc Date: Tue, 16 Jun 2026 08:40:53 +0200 Subject: [PATCH 01/10] added crackingTime aggregated fieldset for agent to allow the frontend to query this efficiently --- src/inc/apiv2/model/AgentAPI.php | 36 ++++++++++++++++--- src/inc/apiv2/model/ApiTokenAPI.php | 2 +- src/inc/apiv2/model/PreTaskAPI.php | 2 +- src/inc/apiv2/model/TaskAPI.php | 2 +- src/inc/apiv2/model/TaskWrapperDisplayAPI.php | 2 +- 5 files changed, 36 insertions(+), 8 deletions(-) diff --git a/src/inc/apiv2/model/AgentAPI.php b/src/inc/apiv2/model/AgentAPI.php index 917eeac53..b6c832d8e 100644 --- a/src/inc/apiv2/model/AgentAPI.php +++ b/src/inc/apiv2/model/AgentAPI.php @@ -2,6 +2,8 @@ namespace Hashtopolis\inc\apiv2\model; +use Exception; +use Hashtopolis\dba\Aggregation; use Hashtopolis\inc\utils\AccessUtils; use Hashtopolis\inc\utils\AgentUtils; use Hashtopolis\inc\defines\DHashcatStatus; @@ -45,16 +47,27 @@ protected function getUpdateHandlers($id, $current_user): array { ]; } + public function getAggregateFieldsets(): array { + return [ + 'agent' => [ + 'crackingTime', + ] + ]; + } + /** * Overridable function to aggregate data in the object. active chunk of agent is appended to * $included_data. * * @param object $object the agent object were data is aggregated from - * @param array &$included_data + * @param array &$includedData * @param array|null $aggregateFieldsets * @return array not used here + * @throws Exception */ - function aggregateData(object $object, array &$included_data = [], ?array $aggregateFieldsets = null): array { + function aggregateData(object $object, array &$includedData = [], ?array $aggregateFieldsets = null): array { + $aggregatedData = []; + $agentId = $object->getId(); $qFs = []; $qFs[] = new QueryFilter(Chunk::AGENT_ID, $agentId, "="); @@ -62,10 +75,25 @@ function aggregateData(object $object, array &$included_data = [], ?array $aggre $active_chunk = Factory::getChunkFactory()->filter([Factory::FILTER => $qFs], true); if ($active_chunk !== NULL) { - $included_data["chunks"][$agentId] = [$active_chunk]; + $includedData["chunks"][$agentId] = [$active_chunk]; + } + + if (array_key_exists('agent', $aggregateFieldsets)) { + $aggregateFieldsets['agent'] = explode(",", $aggregateFieldsets['agent']); + + if (in_array("crackingTime", $aggregateFieldsets['agent'])) { + // in order to make sense of the diff, we need to make sure that both values solve time and dispatch time are set (i.e. >0). + $qF1 = new QueryFilter(Chunk::AGENT_ID, $agentId, "="); + $qF2 = new QueryFilter(Chunk::SOLVE_TIME, ">", 0); + $qF3 = new QueryFilter(Chunk::DISPATCH_TIME, ">", 0); + $agg1 = new Aggregation(Chunk::SOLVE_TIME, Aggregation::SUM); + $agg2 = new Aggregation(Chunk::DISPATCH_TIME, Aggregation::SUM); + $results = Factory::getChunkFactory()->multicolAggregationFilter([Factory::FILTER => [$qF1, $qF2, $qF3]], [$agg1, $agg2]); + $aggregatedData["crackingTime"] = $results[$agg1->getName()] - $results[$agg2->getName()]; + } } - return []; + return $aggregatedData; } protected function getSingleACL(User $user, object $object): bool { diff --git a/src/inc/apiv2/model/ApiTokenAPI.php b/src/inc/apiv2/model/ApiTokenAPI.php index 3b9e6a624..883a5886e 100644 --- a/src/inc/apiv2/model/ApiTokenAPI.php +++ b/src/inc/apiv2/model/ApiTokenAPI.php @@ -108,7 +108,7 @@ protected function createObject(array $data): int { return $token->getId(); } - function aggregateData(object $object, array &$included_data = [], ?array $aggregateFieldsets = null): array { + function aggregateData(object $object, array &$includedData = [], ?array $aggregateFieldsets = null): array { // $token is only set in POST, this way the actual token is only returned after creation. $aggregatedData = []; $token = $this->getJwtToken(); diff --git a/src/inc/apiv2/model/PreTaskAPI.php b/src/inc/apiv2/model/PreTaskAPI.php index 17b87cc72..39369724e 100644 --- a/src/inc/apiv2/model/PreTaskAPI.php +++ b/src/inc/apiv2/model/PreTaskAPI.php @@ -69,7 +69,7 @@ protected function createObject(array $data): int { } //TODO make aggregate data queryable and not included by default - function aggregateData(object $object, array &$included_data = [], ?array $aggregateFieldsets = null): array { + function aggregateData(object $object, array &$includedData = [], ?array $aggregateFieldsets = null): array { $aggregatedData = []; if (is_null($aggregateFieldsets) || array_key_exists('pretask', $aggregateFieldsets)) { diff --git a/src/inc/apiv2/model/TaskAPI.php b/src/inc/apiv2/model/TaskAPI.php index 124cfa495..4d301e4c7 100644 --- a/src/inc/apiv2/model/TaskAPI.php +++ b/src/inc/apiv2/model/TaskAPI.php @@ -183,7 +183,7 @@ protected function createObject(array $data): int { } //TODO make aggregate data queryable and not included by default - function aggregateData(object $object, array &$included_data = [], ?array $aggregateFieldsets = null): array { + function aggregateData(object $object, array &$includedData = [], ?array $aggregateFieldsets = null): array { $aggregatedData = []; if (is_null($aggregateFieldsets) || array_key_exists('task', $aggregateFieldsets)) { diff --git a/src/inc/apiv2/model/TaskWrapperDisplayAPI.php b/src/inc/apiv2/model/TaskWrapperDisplayAPI.php index 5f4b724d4..215588609 100644 --- a/src/inc/apiv2/model/TaskWrapperDisplayAPI.php +++ b/src/inc/apiv2/model/TaskWrapperDisplayAPI.php @@ -63,7 +63,7 @@ protected function getFilterACL(): array { } //TODO make aggregate data queryable and not included by default - function aggregateData(object $object, array &$included_data = [], ?array $aggregateFieldsets = null): array { + function aggregateData(object $object, array &$includedData = [], ?array $aggregateFieldsets = null): array { $aggregatedData = []; if (is_null($aggregateFieldsets) || array_key_exists('taskwrapperdisplay', $aggregateFieldsets)) { $tasks = TaskUtils::getTasksOfWrapper($object->getId()); From 664adc11c75282b2d8a2ee25dfb8c58c5b6992a3 Mon Sep 17 00:00:00 2001 From: s3inlc Date: Tue, 16 Jun 2026 08:52:09 +0200 Subject: [PATCH 02/10] fixed small issue --- src/inc/apiv2/model/AgentAPI.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/inc/apiv2/model/AgentAPI.php b/src/inc/apiv2/model/AgentAPI.php index b6c832d8e..66abb4f74 100644 --- a/src/inc/apiv2/model/AgentAPI.php +++ b/src/inc/apiv2/model/AgentAPI.php @@ -84,8 +84,8 @@ function aggregateData(object $object, array &$includedData = [], ?array $aggreg if (in_array("crackingTime", $aggregateFieldsets['agent'])) { // in order to make sense of the diff, we need to make sure that both values solve time and dispatch time are set (i.e. >0). $qF1 = new QueryFilter(Chunk::AGENT_ID, $agentId, "="); - $qF2 = new QueryFilter(Chunk::SOLVE_TIME, ">", 0); - $qF3 = new QueryFilter(Chunk::DISPATCH_TIME, ">", 0); + $qF2 = new QueryFilter(Chunk::SOLVE_TIME, 0, ">"); + $qF3 = new QueryFilter(Chunk::DISPATCH_TIME, 0, ">"); $agg1 = new Aggregation(Chunk::SOLVE_TIME, Aggregation::SUM); $agg2 = new Aggregation(Chunk::DISPATCH_TIME, Aggregation::SUM); $results = Factory::getChunkFactory()->multicolAggregationFilter([Factory::FILTER => [$qF1, $qF2, $qF3]], [$agg1, $agg2]); From 85a51c25b6f27d93a3725b6d1912b9e1818bb4b1 Mon Sep 17 00:00:00 2001 From: s3inlc Date: Tue, 16 Jun 2026 11:39:43 +0200 Subject: [PATCH 03/10] completed all aggregations for Task and PreTask, added columnFilter with multiple columns --- ci/phpunit/dba/AbstractModelFactoryTest.php | 48 ++++++++- src/dba/AbstractModelFactory.php | 22 +++- src/inc/apiv2/model/PreTaskAPI.php | 36 ++++--- src/inc/apiv2/model/TaskAPI.php | 112 +++++++++++--------- src/inc/utils/TaskUtils.php | 51 +++++++-- 5 files changed, 189 insertions(+), 80 deletions(-) diff --git a/ci/phpunit/dba/AbstractModelFactoryTest.php b/ci/phpunit/dba/AbstractModelFactoryTest.php index ee8437505..8eb27a644 100644 --- a/ci/phpunit/dba/AbstractModelFactoryTest.php +++ b/ci/phpunit/dba/AbstractModelFactoryTest.php @@ -589,6 +589,49 @@ public function testColumnFilterSuccess(): void { $this->assertEquals([1, 125, 72], $column); } + /** + * Test receiving the column of a query with an order + * + * @return void + * @throws Exception + */ + public function testColumnFilterSuccessOrdered(): void { + $testid = uniqid(); + $this->createDatabaseObject(Factory::getHashTypeFactory(), new HashType(null, 'hashtype1' . $testid, 1, 0)); + $this->createDatabaseObject(Factory::getHashTypeFactory(), new HashType(null, 'hashtype2' . $testid, 125, 0)); + $this->createDatabaseObject(Factory::getHashTypeFactory(), new HashType(null, 'hashtype3' . $testid, 72, 0)); + + $qF = new LikeFilter(HashType::DESCRIPTION, "%" . $testid); + $oF = new OrderFilter(HashType::IS_SALTED, "ASC"); + $column = Factory::getHashTypeFactory()->columnFilter([Factory::FILTER => $qF, Factory::ORDER => $oF], HashType::IS_SALTED); + $this->assertEquals([1, 72, 125], $column); + + $oF = new OrderFilter(HashType::IS_SALTED, "DESC"); + $column = Factory::getHashTypeFactory()->columnFilter([Factory::FILTER => $qF, Factory::ORDER => $oF], HashType::IS_SALTED); + $this->assertEquals([125, 72, 1], $column); + } + + /** + * Test querying multiple columns with the column filter + * + * @return void + * @throws Exception + */ + public function testColumnFilterSuccessMultiple(): void { + $testid = uniqid(); + $this->createDatabaseObject(Factory::getHashTypeFactory(), new HashType(null, 'hashtype1' . $testid, 1, 0)); + $this->createDatabaseObject(Factory::getHashTypeFactory(), new HashType(null, 'hashtype2' . $testid, 125, 0)); + $this->createDatabaseObject(Factory::getHashTypeFactory(), new HashType(null, 'hashtype3' . $testid, 72, 1)); + + $qF = new LikeFilter(HashType::DESCRIPTION, "%" . $testid); + $columns = Factory::getHashTypeFactory()->columnFilter([Factory::FILTER => $qF], [HashType::IS_SALTED, HashType::IS_SLOW_HASH]); + $this->assertEquals([[1, 0], [125, 0], [72, 1]], $columns); + + $oF = new OrderFilter(HashType::IS_SALTED, "ASC"); + $columns = Factory::getHashTypeFactory()->columnFilter([Factory::FILTER => $qF, Factory::ORDER => $oF], [HashType::IS_SALTED, HashType::IS_SLOW_HASH]); + $this->assertEquals([[1, 0], [72, 1], [125, 0]], $columns); + } + /** * Test receiving the column of a query on a mapped column * @@ -1137,8 +1180,9 @@ public function testColumnFilter(): void { // hashlist 1 and 3 should be returned $this->assertSame([$hashlist_1->getId(), $hashlist_3->getId()], $ids); - $qF = new QueryFilter(Hashlist::CRACKED, 5000, ">"); - $ids = Factory::getHashlistFactory()->columnFilter([Factory::FILTER => $qF, Factory::ORDER => $oF], Hashlist::HASHLIST_ID); + $qF1 = new QueryFilter(Hashlist::CRACKED, 5000, ">"); + $qF2 = new LikeFilter(Hashlist::HASHLIST_NAME, "%" . $testid); + $ids = Factory::getHashlistFactory()->columnFilter([Factory::FILTER => [$qF1, $qF2], Factory::ORDER => $oF], Hashlist::HASHLIST_ID); $this->assertSame([], $ids); } diff --git a/src/dba/AbstractModelFactory.php b/src/dba/AbstractModelFactory.php index dadc16847..87ccb9797 100755 --- a/src/dba/AbstractModelFactory.php +++ b/src/dba/AbstractModelFactory.php @@ -486,13 +486,19 @@ public function multicolAggregationFilter(array $options, array $aggregations): /** * @param $options array options of query (filters and joins) - * @param $column string single column key which should be retrieved + * @param $columns array|string single column key or array of column keys which should be retrieved * @return array of the column entries returned from this query * @throws Exception */ - public function columnFilter(array $options, string $column): array { - $query = "SELECT " . Util::createPrefixedString($this->getMappedModelTable(), [self::getMappedModelKey($this->getNullObject(), $column)]); - $query = $query . " FROM " . $this->getMappedModelTable(); + public function columnFilter(array $options, array|string $columns): array { + if (!is_array($columns)) { + $columns = [$columns]; + } + $elements = []; + foreach ($columns as $column) { + $elements[] = Util::createPrefixedString($this->getMappedModelTable(), [self::getMappedModelKey($this->getNullObject(), $column)]); + } + $query = "SELECT " . join(",", $elements) . " FROM " . $this->getMappedModelTable(); $vals = array(); @@ -502,12 +508,18 @@ public function columnFilter(array $options, string $column): array { if (array_key_exists(Factory::FILTER, $options)) { $query .= $this->applyFilters($vals, $options[Factory::FILTER]); } + if (array_key_exists(Factory::ORDER, $options)) { + $query .= $this->applyOrder($options[Factory::ORDER]); + } $dbh = self::getDB(); $stmt = $dbh->prepare($query); $stmt->execute($vals); - return $stmt->fetchAll(PDO::FETCH_COLUMN); + if (sizeof($elements) == 1) { + return $stmt->fetchAll(PDO::FETCH_COLUMN); + } + return $stmt->fetchAll(PDO::FETCH_NUM); } /** diff --git a/src/inc/apiv2/model/PreTaskAPI.php b/src/inc/apiv2/model/PreTaskAPI.php index 39369724e..75a9d4872 100644 --- a/src/inc/apiv2/model/PreTaskAPI.php +++ b/src/inc/apiv2/model/PreTaskAPI.php @@ -68,24 +68,34 @@ protected function createObject(array $data): int { return $pretask->getId(); } - //TODO make aggregate data queryable and not included by default + + /** + * @param object $object + * @param array $includedData + * @param array|null $aggregateFieldsets + * @return array + */ function aggregateData(object $object, array &$includedData = [], ?array $aggregateFieldsets = null): array { $aggregatedData = []; - if (is_null($aggregateFieldsets) || array_key_exists('pretask', $aggregateFieldsets)) { - - $qF1 = new QueryFilter(FilePretask::PRETASK_ID, $object->getId(), "=", Factory::getFilePretaskFactory()); - $jF1 = new JoinFilter(Factory::getFilePretaskFactory(), File::FILE_ID, FilePretask::FILE_ID); - $files = Factory::getFileFactory()->filter([Factory::FILTER => $qF1, Factory::JOIN => $jF1]); - $files = $files[Factory::getFileFactory()->getModelName()]; + + if (array_key_exists('pretask', $aggregateFieldsets)) { + $aggregateFieldsets['pretask'] = explode(",", $aggregateFieldsets['pretask']); - $lineCountProduct = 1; - foreach ($files as $file) { - $lineCount = $file->getLineCount(); - if ($lineCount !== null) { - $lineCountProduct = $lineCountProduct * $lineCount; + if (in_array("auxiliaryKeyspace", $aggregateFieldsets['pretask'])) { + $qF1 = new QueryFilter(FilePretask::PRETASK_ID, $object->getId(), "=", Factory::getFilePretaskFactory()); + $jF1 = new JoinFilter(Factory::getFilePretaskFactory(), File::FILE_ID, FilePretask::FILE_ID); + $files = Factory::getFileFactory()->filter([Factory::FILTER => $qF1, Factory::JOIN => $jF1]); + $files = $files[Factory::getFileFactory()->getModelName()]; + + $lineCountProduct = 1; + foreach ($files as $file) { + $lineCount = $file->getLineCount(); + if ($lineCount !== null) { + $lineCountProduct = $lineCountProduct * $lineCount; + } } + $aggregatedData["auxiliaryKeyspace"] = $lineCountProduct; } - $aggregatedData["auxiliaryKeyspace"] = $lineCountProduct; } return $aggregatedData; diff --git a/src/inc/apiv2/model/TaskAPI.php b/src/inc/apiv2/model/TaskAPI.php index 4d301e4c7..e03e61bf5 100644 --- a/src/inc/apiv2/model/TaskAPI.php +++ b/src/inc/apiv2/model/TaskAPI.php @@ -2,6 +2,8 @@ namespace Hashtopolis\inc\apiv2\model; +use Exception; +use Hashtopolis\dba\OrderFilter; use Hashtopolis\inc\defines\DConfig; use Hashtopolis\inc\defines\DPrince; use Hashtopolis\inc\utils\AccessUtils; @@ -136,15 +138,19 @@ public function getFormFields(): array { "files" => ['type' => 'array', 'subtype' => 'int'], ]; } - + public function getAggregateFieldsets(): array { return [ 'task' => [ - 'assignedAgents', + 'totalAssignedAgents', 'dispatched', 'searched', - 'isActive', - 'taskExtraDetails', + 'status', + 'totalNumberOfChunks', + 'currentSpeed', + 'estimatedTime', + 'cprogress', + 'timeSpent' ] ]; } @@ -182,88 +188,88 @@ protected function createObject(array $data): int { return $task->getId(); } - //TODO make aggregate data queryable and not included by default + /** + * @param object $object + * @param array $includedData + * @param array|null $aggregateFieldsets + * @return array + * @throws Exception + */ function aggregateData(object $object, array &$includedData = [], ?array $aggregateFieldsets = null): array { + /** @var $object Task */ $aggregatedData = []; - if (is_null($aggregateFieldsets) || array_key_exists('task', $aggregateFieldsets)) { - if (!is_null($aggregateFieldsets)) { - $aggregateFieldsets['task'] = explode(",", $aggregateFieldsets['task']); - } + if (array_key_exists('task', $aggregateFieldsets)) { + $aggregateFieldsets['task'] = explode(",", $aggregateFieldsets['task']); - $assignedAgents = []; - if (is_null($aggregateFieldsets) || in_array("assignedAgents", $aggregateFieldsets['task'])) { + if (in_array("assignedAgents", $aggregateFieldsets['task'])) { $qF = new QueryFilter(Assignment::TASK_ID, $object->getId(), "="); $assignedAgents = Factory::getAssignmentFactory()->countFilter([Factory::FILTER => $qF]); $aggregatedData["totalAssignedAgents"] = $assignedAgents; } + $keyspace = $object->getKeyspace(); $keyspaceProgress = $object->getKeyspaceProgress(); - if (is_null($aggregateFieldsets) || in_array("dispatched", $aggregateFieldsets['task'])) { + if (in_array("dispatched", $aggregateFieldsets['task'])) { $aggregatedData["dispatched"] = Util::showperc($keyspaceProgress, $keyspace); } - if (is_null($aggregateFieldsets) || in_array("searched", $aggregateFieldsets['task'])) { + if (in_array("searched", $aggregateFieldsets['task'])) { $aggregatedData["searched"] = Util::showperc(TaskUtils::getTaskProgress($object), $keyspace); } - - $chunks = null; - if (is_null($aggregateFieldsets) || in_array("isActive", $aggregateFieldsets['task'])) { - $qF = new QueryFilter(Chunk::TASK_ID, $object->getId(), "="); - $chunks = Factory::getChunkFactory()->filter([Factory::FILTER => $qF]); + + if (in_array("status", $aggregateFieldsets['task'])) { + // the filter for progress is needed so we reduce the checked chunks numbers by a lot + $qF1 = new QueryFilter(Chunk::TASK_ID, $object->getId(), "="); + $qF2 = new QueryFilter(Chunk::PROGRESS, 10000, "<"); + $chunks = Factory::getChunkFactory()->filter([Factory::FILTER => [$qF1, $qF2]]); $aggregatedData["status"] = TaskUtils::getStatus($chunks, $keyspace, $keyspaceProgress); } - if (is_null($aggregateFieldsets) || in_array("totalNumberOfChunks", $aggregateFieldsets['task'])) { + + if (in_array("totalNumberOfChunks", $aggregateFieldsets['task'])) { $qF = new QueryFilter(Chunk::TASK_ID, $object->getId(), "="); $aggregatedData["totalNumberOfChunks"] = Factory::getChunkFactory()->countFilter([Factory::FILTER => $qF]); } - if (is_null($aggregateFieldsets) || in_array("taskExtraDetails", $aggregateFieldsets['task'])) { - $qF = new QueryFilter(Chunk::TASK_ID, $object->getId(), "="); - if (!isset($chunks)){ - $chunks = Factory::getChunkFactory()->filter([Factory::FILTER => $qF]); + + if (in_array("currentSpeed", $aggregateFieldsets['task'])) { + $qF1 = new QueryFilter(Chunk::TASK_ID, $object->getId(), "="); + $qF2 = new QueryFilter(Chunk::SOLVE_TIME, time() - SConfig::getInstance()->getVal(DConfig::CHUNK_TIMEOUT), ">"); + $qF3 = new QueryFilter(Chunk::PROGRESS, 10000, "<"); + $agg = new Aggregation(Chunk::SPEED, Aggregation::SUM); + $speed = Factory::getChunkFactory()->multicolAggregationFilter([Factory::FILTER => [$qF1, $qF2, $qF3]], [$agg])[$agg->getName()]; + if ($speed == null) { + $speed = 0; } + $aggregatedData["currentSpeed"] = $speed; + } + + if (in_array("estimatedTime", $aggregateFieldsets['task']) || + in_array("timeSpent", $aggregateFieldsets['task']) || + in_array("cprogress", $aggregateFieldsets['task'])) { - $currentSpeed = 0; - $cProgress = 0; - - foreach ($chunks as $chunk) { - $cProgress += $chunk->getCheckpoint() - $chunk->getSkip(); - if (time() - max($chunk->getSolveTime(), $chunk->getDispatchTime()) < SConfig::getInstance()->getVal(DConfig::CHUNK_TIMEOUT) && $chunk->getProgress() < 10000) { - $currentSpeed += $chunk->getSpeed(); - } + $cProgress = TaskUtils::getTaskProgress($object); + if (in_array("cprogress", $aggregateFieldsets['task'])) { + $aggregatedData["cprogress"] = $cProgress; } - - $timeChunks = $chunks; - usort($timeChunks, ["Hashtopolis\inc\Util", "compareChunksTime"]); - $timeSpent = 0; - $current = 0; - foreach ($timeChunks as $c) { - if ($c->getDispatchTime() > $current) { - $timeSpent += $c->getSolveTime() - $c->getDispatchTime(); - $current = $c->getSolveTime(); - } - else if ($c->getSolveTime() > $current) { - $timeSpent += $c->getSolveTime() - $current; - $current = $c->getSolveTime(); - } + $timeSpent = TaskUtils::getTimeSpentOnTask($object); + + if (in_array("timeSpent", $aggregateFieldsets['task'])) { + $aggregatedData["timeSpent"] = $timeSpent; } - - $keyspace = $object->getKeyspace(); - $estimatedTime = ($keyspace > 0 && $cProgress > 0) ? round($timeSpent / ($cProgress / $keyspace) - $timeSpent) : 0; - $aggregatedData["estimatedTime"] = $estimatedTime; - $aggregatedData["timeSpent"] = $timeSpent; - $aggregatedData["currentSpeed"] = $currentSpeed; - $aggregatedData["cprogress"] = $cProgress; + if (in_array("estimatedTime", $aggregateFieldsets['task'])) { + $aggregatedData["estimatedTime"] = ($keyspace > 0 && $cProgress > 0) ? round($timeSpent / ($cProgress / $keyspace) - $timeSpent) : 0; + } } } - + return $aggregatedData; } protected function deleteObject(object $object): void { + /** @var $object Task */ TaskUtils::deleteTask($object); } diff --git a/src/inc/utils/TaskUtils.php b/src/inc/utils/TaskUtils.php index 19b8ea501..90b70224d 100644 --- a/src/inc/utils/TaskUtils.php +++ b/src/inc/utils/TaskUtils.php @@ -2,6 +2,7 @@ namespace Hashtopolis\inc\utils; +use Exception; use Hashtopolis\inc\DataSet; use Hashtopolis\dba\models\AccessGroup; use Hashtopolis\dba\models\AccessGroupAgent; @@ -124,7 +125,7 @@ public static function editNotes($taskId, $notes, $user) { $task = TaskUtils::getTask($taskId, $user); Factory::getTaskFactory()->set($task, Task::NOTES, $notes); } - + // Function for taskwrapper api to determine based on the chunks if a task is running, idle or completed. // Status 1 is running, 2 is idle and 3 is completed. public static function getStatus($chunks, $keyspace, $keyspaceProgress) { @@ -132,10 +133,11 @@ public static function getStatus($chunks, $keyspace, $keyspaceProgress) { $status = 2; if ($keyspaceProgress >= $keyspace && $keyspaceProgress > 0) { $status = 3; - } else { + } + else { $now = time(); $chunkTimeOut = SConfig::getInstance()->getVal(DConfig::CHUNK_TIMEOUT); - + foreach ($chunks as $chunk) { if ($now - max($chunk->getSolveTime(), $chunk->getDispatchTime()) < $chunkTimeOut && $chunk->getProgress() < 10000) { $status = 1; @@ -863,7 +865,8 @@ public static function createTask($hashlistId, $name, $attackCmd, $chunkTime, $s } else if ($benchtype != 'speed' && $benchtype != 'runtime') { throw new HttpError("Invalid benchmark type!"); - } else if ($enforcePipe < 0 || $enforcePipe > 1) { + } + else if ($enforcePipe < 0 || $enforcePipe > 1) { throw new HttpError("Invalid enforce pipe value"); } $benchtype = ($benchtype == 'speed') ? 1 : 0; @@ -1390,13 +1393,47 @@ public static function isSaturatedByOtherAgents($task, $agent) { ($task->getMaxAgents() > 0 && $numAssignments >= $task->getMaxAgents()); // at least maxAgents agents are already assigned } - public static function getTaskProgress($task) { + /** + * @param $task Task + * @return mixed + * @throws Exception + */ + public static function getTaskProgress(Task $task): int { $qF1 = new QueryFilter(Chunk::TASK_ID, $task->getId(), "="); $agg1 = new Aggregation(Chunk::CHECKPOINT, Aggregation::SUM); $agg2 = new Aggregation(Chunk::SKIP, Aggregation::SUM); $results = Factory::getChunkFactory()->multicolAggregationFilter([Factory::FILTER => $qF1], [$agg1, $agg2]); - $progress = $results[$agg1->getName()] - $results[$agg2->getName()]; - return $progress; + return $results[$agg1->getName()] - $results[$agg2->getName()]; + } + + /** + * Get the time spent on a task (not including parallel running). + * + * @param Task $task + * @return int + * @throws Exception + */ + public static function getTimeSpentOnTask(Task $task): int { + $qF1 = new QueryFilter(Chunk::TASK_ID, $task->getId(), "="); + $qF2 = new QueryFilter(Chunk::SOLVE_TIME, 0, ">"); + $qF3 = new QueryFilter(Chunk::DISPATCH_TIME, 0, ">"); + $oF = new OrderFilter(Chunk::DISPATCH_TIME, "ASC"); + $columnRows = Factory::getChunkFactory()->columnFilter([Factory::FILTER => [$qF1, $qF2, $qF3], Factory::ORDER => $oF], [Chunk::DISPATCH_TIME, Chunk::SOLVE_TIME]); + + $timeSpent = 0; + $current = 0; + + foreach ($columnRows as $row) { + if ($row[0] > $current) { + $timeSpent += $row[1] - $row[0]; + $current = $row[1]; + } + else if ($row[1] > $current) { + $timeSpent += $row[1] - $current; + $current = $row[1]; + } + } + return $timeSpent; } } From 50a150b795de371034d601c98c2c2e30246e610a Mon Sep 17 00:00:00 2001 From: s3inlc Date: Tue, 16 Jun 2026 11:59:25 +0200 Subject: [PATCH 04/10] completed all aggregated functions needed --- src/inc/apiv2/model/AgentAPI.php | 2 +- src/inc/apiv2/model/PreTaskAPI.php | 2 +- src/inc/apiv2/model/TaskAPI.php | 2 +- src/inc/apiv2/model/TaskWrapperDisplayAPI.php | 155 ++++++++++-------- 4 files changed, 93 insertions(+), 68 deletions(-) diff --git a/src/inc/apiv2/model/AgentAPI.php b/src/inc/apiv2/model/AgentAPI.php index 66abb4f74..a07686faa 100644 --- a/src/inc/apiv2/model/AgentAPI.php +++ b/src/inc/apiv2/model/AgentAPI.php @@ -78,7 +78,7 @@ function aggregateData(object $object, array &$includedData = [], ?array $aggreg $includedData["chunks"][$agentId] = [$active_chunk]; } - if (array_key_exists('agent', $aggregateFieldsets)) { + if (!is_null($aggregateFieldsets) && array_key_exists('agent', $aggregateFieldsets)) { $aggregateFieldsets['agent'] = explode(",", $aggregateFieldsets['agent']); if (in_array("crackingTime", $aggregateFieldsets['agent'])) { diff --git a/src/inc/apiv2/model/PreTaskAPI.php b/src/inc/apiv2/model/PreTaskAPI.php index 75a9d4872..3ed52d839 100644 --- a/src/inc/apiv2/model/PreTaskAPI.php +++ b/src/inc/apiv2/model/PreTaskAPI.php @@ -78,7 +78,7 @@ protected function createObject(array $data): int { function aggregateData(object $object, array &$includedData = [], ?array $aggregateFieldsets = null): array { $aggregatedData = []; - if (array_key_exists('pretask', $aggregateFieldsets)) { + if (!is_null($aggregateFieldsets) && array_key_exists('pretask', $aggregateFieldsets)) { $aggregateFieldsets['pretask'] = explode(",", $aggregateFieldsets['pretask']); if (in_array("auxiliaryKeyspace", $aggregateFieldsets['pretask'])) { diff --git a/src/inc/apiv2/model/TaskAPI.php b/src/inc/apiv2/model/TaskAPI.php index e03e61bf5..3b47b9869 100644 --- a/src/inc/apiv2/model/TaskAPI.php +++ b/src/inc/apiv2/model/TaskAPI.php @@ -199,7 +199,7 @@ function aggregateData(object $object, array &$includedData = [], ?array $aggreg /** @var $object Task */ $aggregatedData = []; - if (array_key_exists('task', $aggregateFieldsets)) { + if (!is_null($aggregateFieldsets) && array_key_exists('task', $aggregateFieldsets)) { $aggregateFieldsets['task'] = explode(",", $aggregateFieldsets['task']); if (in_array("assignedAgents", $aggregateFieldsets['task'])) { diff --git a/src/inc/apiv2/model/TaskWrapperDisplayAPI.php b/src/inc/apiv2/model/TaskWrapperDisplayAPI.php index 215588609..f007afecb 100644 --- a/src/inc/apiv2/model/TaskWrapperDisplayAPI.php +++ b/src/inc/apiv2/model/TaskWrapperDisplayAPI.php @@ -2,6 +2,8 @@ namespace Hashtopolis\inc\apiv2\model; +use Exception; +use Hashtopolis\dba\Aggregation; use Hashtopolis\inc\utils\AccessUtils; use Hashtopolis\dba\ContainFilter; use Hashtopolis\dba\Factory; @@ -61,95 +63,118 @@ protected function getFilterACL(): array { ] ]; } - - //TODO make aggregate data queryable and not included by default + + public function getAggregateFieldsets(): array { + return [ + 'taskwrapperdisplay' => [ + 'totalAssignedAgents', + 'dispatched', + 'searched', + 'status', + 'currentSpeed', + 'estimatedTime', + 'cprogress', + 'timeSpent' + ] + ]; + } + + /** + * @param object $object + * @param array $includedData + * @param array|null $aggregateFieldsets + * @return array + * @throws Exception + */ function aggregateData(object $object, array &$includedData = [], ?array $aggregateFieldsets = null): array { $aggregatedData = []; - if (is_null($aggregateFieldsets) || array_key_exists('taskwrapperdisplay', $aggregateFieldsets)) { - $tasks = TaskUtils::getTasksOfWrapper($object->getId()); - $completed = 0; - $total = 0; - $status = 0; - foreach($tasks as $task) { - $qF = new QueryFilter(Chunk::TASK_ID, $task->getId(), "="); - $chunks = Factory::getChunkFactory()->filter([Factory::FILTER => $qF]); - $taskStatus = TaskUtils::getStatus($chunks, $task->getKeyspace(), $task->getKeyspaceProgress()); - // if one task of the wrapper is running, it is running - if ($taskStatus === 1) { - $status = 1; - break; + + if (!is_null($aggregateFieldsets) && array_key_exists('taskwrapperdisplay', $aggregateFieldsets)) { + $aggregateFieldsets['taskwrapperdisplay'] = explode(",", $aggregateFieldsets['taskwrapperdisplay']); + + if (in_array("status", $aggregateFieldsets['taskwrapperdisplay'])) { + // TODO: this could be optimized by only requesting taskId, keyspace and keyspaceProgress of all tasks of that wrapper (columnFilter) + $tasks = TaskUtils::getTasksOfWrapper($object->getId()); + $completed = 0; + $total = 0; + $status = 0; + foreach($tasks as $task) { + $qF1 = new QueryFilter(Chunk::TASK_ID, $task->getId(), "="); + $qF2 = new QueryFilter(Chunk::PROGRESS, 10000, "<"); + $chunks = Factory::getChunkFactory()->filter([Factory::FILTER => [$qF1, $qF2]]); + $taskStatus = TaskUtils::getStatus($chunks, $task->getKeyspace(), $task->getKeyspaceProgress()); + // if one task of the wrapper is running, it is running + if ($taskStatus === 1) { + $status = 1; + break; + } + if ($taskStatus === 3) { + $completed++; + } + $total++; } - if ($taskStatus === 3) { - $completed++; + if ($status !== 1) { + if ($total > 0 && $completed === $total) { + $status = 3; + } else { + $status = 2; + } } - $total++; + $aggregatedData['status'] = $status; } - if ($status !== 1) { - if ($total > 0 && $completed === $total) { - $status = 3; - } else { - $status = 2; - } + + if (in_array("totalAssignedAgents", $aggregateFieldsets['taskwrapperdisplay'])) { + $qF = new QueryFilter(Task::TASK_WRAPPER_ID, $object->getId(), "=", Factory::getTaskFactory()); + $jF = new JoinFilter(Factory::getTaskFactory(), Assignment::TASK_ID, Task::TASK_ID); + + $assignedAgents = Factory::getAssignmentFactory()->countFilter([Factory::FILTER => $qF, Factory::JOIN => $jF]); + $aggregatedData["totalAssignedAgents"] = $assignedAgents; } - - $aggregatedData['status'] = $status; $keyspace = $object->getKeyspace(); $keyspaceProgress = $object->getKeyspaceProgress(); if ($object->getTaskType() === DTaskTypes::NORMAL) { + $tasks = TaskUtils::getTasksOfWrapper($object->getId()); $task = $tasks[0]; - if (is_null($aggregateFieldsets) || in_array("dispatched", $aggregateFieldsets['taskwrapperdisplay'])) { + if (in_array("dispatched", $aggregateFieldsets['taskwrapperdisplay'])) { $aggregatedData["dispatched"] = Util::showperc($keyspaceProgress, $keyspace); } - if (is_null($aggregateFieldsets) || in_array("searched", $aggregateFieldsets['taskwrapperdisplay'])) { + if (in_array("searched", $aggregateFieldsets['taskwrapperdisplay'])) { $aggregatedData["searched"] = Util::showperc(TaskUtils::getTaskProgress($task), $keyspace); } - - if (!isset($chunks)){ - $qF = new QueryFilter(Chunk::TASK_ID, $task->getId(), "="); - $chunks = Factory::getChunkFactory()->filter([Factory::FILTER => $qF]); + + if (in_array("currentSpeed", $aggregateFieldsets['taskwrapperdisplay'])) { + $qF1 = new QueryFilter(Chunk::TASK_ID, $task->getId(), "="); + $qF2 = new QueryFilter(Chunk::SOLVE_TIME, time() - SConfig::getInstance()->getVal(DConfig::CHUNK_TIMEOUT), ">"); + $qF3 = new QueryFilter(Chunk::PROGRESS, 10000, "<"); + $agg = new Aggregation(Chunk::SPEED, Aggregation::SUM); + $speed = Factory::getChunkFactory()->multicolAggregationFilter([Factory::FILTER => [$qF1, $qF2, $qF3]], [$agg])[$agg->getName()]; + if ($speed == null) { + $speed = 0; + } + $aggregatedData["currentSpeed"] = $speed; } - if (is_null($aggregateFieldsets) || in_array("taskExtraDetails", $aggregateFieldsets['taskwrapperdisplay'])) { - $currentSpeed = 0; - $cProgress = 0; + if (in_array("estimatedTime", $aggregateFieldsets['taskwrapperdisplay']) || + in_array("timeSpent", $aggregateFieldsets['taskwrapperdisplay']) || + in_array("cprogress", $aggregateFieldsets['taskwrapperdisplay'])) { - foreach ($chunks as $chunk) { - $cProgress += $chunk->getCheckpoint() - $chunk->getSkip(); - if (time() - max($chunk->getSolveTime(), $chunk->getDispatchTime()) < SConfig::getInstance()->getVal(DConfig::CHUNK_TIMEOUT) && $chunk->getProgress() < 10000) { - $currentSpeed += $chunk->getSpeed(); - } + $cProgress = TaskUtils::getTaskProgress($task); + if (in_array("cprogress", $aggregateFieldsets['taskwrapperdisplay'])) { + $aggregatedData["cprogress"] = $cProgress; } - $timeChunks = $chunks; - usort($timeChunks, ["Hashtopolis\inc\Util", "compareChunksTime"]); - $timeSpent = 0; - $current = 0; - foreach ($timeChunks as $c) { - if ($c->getDispatchTime() > $current) { - $timeSpent += $c->getSolveTime() - $c->getDispatchTime(); - $current = $c->getSolveTime(); - } - else if ($c->getSolveTime() > $current) { - $timeSpent += $c->getSolveTime() - $current; - $current = $c->getSolveTime(); - } + $timeSpent = TaskUtils::getTimeSpentOnTask($task); + + if (in_array("timeSpent", $aggregateFieldsets['taskwrapperdisplay'])) { + $aggregatedData["timeSpent"] = $timeSpent; } - - $estimatedTime = ($keyspace > 0 && $cProgress > 0) ? round($timeSpent / ($cProgress / $keyspace) - $timeSpent) : 0; - $aggregatedData["estimatedTime"] = $estimatedTime; - $aggregatedData["timeSpent"] = $timeSpent; - $aggregatedData["currentSpeed"] = $currentSpeed; - $aggregatedData["cprogress"] = $cProgress; - - $assignedAgents = []; - if (is_null($aggregateFieldsets) || in_array("assignedAgents", $aggregateFieldsets['task'])) { - $qF = new QueryFilter(Assignment::TASK_ID, $task->getId(), "="); - $assignedAgents = Factory::getAssignmentFactory()->countFilter([Factory::FILTER => $qF]); - $aggregatedData["totalAssignedAgents"] = $assignedAgents; + + if (in_array("estimatedTime", $aggregateFieldsets['taskwrapperdisplay'])) { + $aggregatedData["estimatedTime"] = ($keyspace > 0 && $cProgress > 0) ? round($timeSpent / ($cProgress / $keyspace) - $timeSpent) : 0; } } } From a7e2d0ba197056287fd998fc024539b88bd31012 Mon Sep 17 00:00:00 2001 From: s3inlc Date: Tue, 16 Jun 2026 12:07:30 +0200 Subject: [PATCH 05/10] changed annotations --- src/inc/apiv2/model/TaskAPI.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/inc/apiv2/model/TaskAPI.php b/src/inc/apiv2/model/TaskAPI.php index 3b47b9869..7cd3050c6 100644 --- a/src/inc/apiv2/model/TaskAPI.php +++ b/src/inc/apiv2/model/TaskAPI.php @@ -196,7 +196,7 @@ protected function createObject(array $data): int { * @throws Exception */ function aggregateData(object $object, array &$includedData = [], ?array $aggregateFieldsets = null): array { - /** @var $object Task */ + /** @var Task $object */ $aggregatedData = []; if (!is_null($aggregateFieldsets) && array_key_exists('task', $aggregateFieldsets)) { @@ -269,7 +269,7 @@ function aggregateData(object $object, array &$includedData = [], ?array $aggreg } protected function deleteObject(object $object): void { - /** @var $object Task */ + /** @var Task $object */ TaskUtils::deleteTask($object); } From 213934629506164b45825fe721ef5d897d8bc5ae Mon Sep 17 00:00:00 2001 From: s3inlc Date: Tue, 16 Jun 2026 14:44:43 +0200 Subject: [PATCH 06/10] fixed test to use aggregation for accessing totalNumberOfChunks --- ci/apiv2/test_task.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ci/apiv2/test_task.py b/ci/apiv2/test_task.py index 00c50b9d3..27d8596b3 100644 --- a/ci/apiv2/test_task.py +++ b/ci/apiv2/test_task.py @@ -15,7 +15,7 @@ def create_test_agent_object(self, *nargs, delete=True, **kwargs): dummy_agent.send_process(progress=50) dummy_agent.send_process(progress=100, state=ProcessState.EXHAUSTED) dummy_agent.get_chunk() - return Task.objects.get(taskId=retval['task'].id) + return Task.objects.params(**{"aggregate[task]": "totalNumberOfChunks"}).get(taskId=retval['task'].id) def create_test_object(self, **kwargs): hashlist_kwargs = kwargs.copy() From b714d5706227177551214d95bc686fb57b7de361 Mon Sep 17 00:00:00 2001 From: s3inlc Date: Wed, 17 Jun 2026 08:04:30 +0200 Subject: [PATCH 07/10] check aggregation fieldsets for validity --- src/inc/apiv2/common/AbstractBaseAPI.php | 58 ++++++++++++++++-------- 1 file changed, 39 insertions(+), 19 deletions(-) diff --git a/src/inc/apiv2/common/AbstractBaseAPI.php b/src/inc/apiv2/common/AbstractBaseAPI.php index 9a651211e..0a79130af 100644 --- a/src/inc/apiv2/common/AbstractBaseAPI.php +++ b/src/inc/apiv2/common/AbstractBaseAPI.php @@ -187,7 +187,7 @@ protected function getUpdateHandlers($id, $current_user): array { public function aggregateData(object $object, array &$includedData = [], ?array $aggregateFieldsets = null): array { return []; } - + /** * Return supported aggregate fieldsets/options for this endpoint. * @@ -658,6 +658,7 @@ protected function obj2Array(object $obj): array { * Convert DB object JSON:API Resource Object * @throws NotFoundExceptionInterface * @throws ContainerExceptionInterface + * @throws HttpError */ protected function obj2Resource(object $obj, array &$expandResult = [], ?array $sparseFieldsets = null, ?array $aggregateFieldsets = null): array { // Convert values to JSON supported types @@ -702,11 +703,27 @@ protected function obj2Resource(object $obj, array &$expandResult = [], ?array $ if ($this instanceof AbstractModelAPI && get_class($obj) !== $this->getDBAClass()) { $apiClassObject = new $apiClass($this->container); - } else { + } + else { // use instance of this when the object is of the dba class of this api endpoint. // This way its possible to set object attributes in the post to be used in the aggregateData function. $apiClassObject = $this; } + + if (is_array($aggregateFieldsets)) { + $availableFieldsets = $apiClassObject->getAggregateFieldsets(); + foreach ($aggregateFieldsets as $name => $aggregateFieldset) { + if (!array_key_exists($name, $availableFieldsets)) { + throw new HttpError("Invalid aggregation object requested!"); + } + foreach ($aggregateFieldset as $field) { + if (!in_array($field, $availableFieldsets[$name])) { + throw new HttpError("Invalid aggregation requested!"); + } + } + } + } + $aggregatedData = $apiClassObject->aggregateData($obj, $expandResult, $aggregateFieldsets); $attributes = array_merge($attributes, $aggregatedData); @@ -1124,9 +1141,9 @@ protected function getPrimaryKey(): string { function getFilters(Request $request): array { return $this->getQueryParameterFamily($request, 'filter'); } - + protected static function checkJoinExists(array $joins, string $modelName) { - foreach($joins as $join) { + foreach ($joins as $join) { if ($join->getOtherFactory()->getModelName() === $modelName) { return true; } @@ -1154,13 +1171,13 @@ protected function makeFilter(array $filters, object $apiClass, array &$joinFilt $cast_key = $matches['key'] == 'id' ? array_column($features, 'alias', 'dbname')[$this->getPrimaryKey()] : $matches['key']; if (strpos($cast_key, ".")) { //When the key contains a "." it should be a relation in format: "task.taskname" where task is the relation. - $relationObject = $this->retrieveRelationKey($cast_key); - $factory = $relationObject->factory; - $cast_key = $relationObject->cast_key; - if (!self::checkJoinExists($joinFilters, $factory->getModelName())) { - $joinFilters[] = new JoinFilter($factory, $relationObject->joinKey, $relationObject->key); - } - $features = $relationObject->features_relation; + $relationObject = $this->retrieveRelationKey($cast_key); + $factory = $relationObject->factory; + $cast_key = $relationObject->cast_key; + if (!self::checkJoinExists($joinFilters, $factory->getModelName())) { + $joinFilters[] = new JoinFilter($factory, $relationObject->joinKey, $relationObject->key); + } + $features = $relationObject->features_relation; } if (!array_key_exists($cast_key, $features)) { @@ -1259,7 +1276,7 @@ protected function makeFilter(array $filters, object $apiClass, array &$joinFilt } return $qFs; } - + /** * Retrieves the relation from a sort/filter value. ex task.taskName when task is a relation for the current * Model endpoint. This works only for relations of 1 deep @@ -1277,17 +1294,19 @@ protected function retrieveRelationKey(string $value): object { $key = $relations[$relationString]['key']; $features_relation = $relationFeatures; $value = $parts[1]; - return (object) [ + return (object)[ "factory" => $factory, "joinKey" => $joinKey, "key" => $key, "features_relation" => $features_relation, "cast_key" => $value ]; - } else { + } + else { throw new HttpError("Invalid relation: " . $relationString); } - } else { + } + else { throw new HttpForbidden("Invalid key, multiple '.' found in key, but only relationships of one deep is allowed"); } } @@ -1376,7 +1395,7 @@ protected function processExpands( $expandKeys = array_keys($expandResult); $diffs = array_diff($expandKeys, $expands); $expands = array_merge($expands, $diffs); - + foreach ($expands as $expand) { if (!array_key_exists($object->getId(), $expandResult[$expand])) { continue; @@ -1417,7 +1436,7 @@ protected function validatePermissions(string $permissions, array $required_perm else { $rightgroup_perms = json_decode($permissions, true); } - + if ($aud === "user_hashtopolis") { // Validate if no undefined permissions are set in $acl_mapping for the legacy permissions assert(count(array_diff(array_keys($rightgroup_perms), array_keys(self::$acl_mapping))) == 0); @@ -1428,7 +1447,8 @@ protected function validatePermissions(string $permissions, array $required_perm $user_available_perms = array_unique(array_merge($user_available_perms, self::$acl_mapping[$rightgroup_perm])); } }; - } else { + } + else { $user_available_perms = array_keys($rightgroup_perms, true, true); } @@ -1641,7 +1661,7 @@ static function createJsonResponse(array $data = [], array $links = [], array $i * Get single Resource */ protected static function getOneResource(object $apiClass, object $object, Request $request, Response $response, int $statusCode = 200): Response { - $apiClass->preCommon($request); + $apiClass->preCommon($request); $validExpandables = $apiClass->getExpandables(); $expands = $apiClass->makeExpandables($request, $validExpandables); From b0ba6011a21e8a86f7c8839fc7e46d68872cc0cd Mon Sep 17 00:00:00 2001 From: s3inlc Date: Wed, 17 Jun 2026 08:09:19 +0200 Subject: [PATCH 08/10] fixed check --- src/inc/apiv2/common/AbstractBaseAPI.php | 1 + 1 file changed, 1 insertion(+) diff --git a/src/inc/apiv2/common/AbstractBaseAPI.php b/src/inc/apiv2/common/AbstractBaseAPI.php index 0a79130af..4de3e6614 100644 --- a/src/inc/apiv2/common/AbstractBaseAPI.php +++ b/src/inc/apiv2/common/AbstractBaseAPI.php @@ -716,6 +716,7 @@ protected function obj2Resource(object $obj, array &$expandResult = [], ?array $ if (!array_key_exists($name, $availableFieldsets)) { throw new HttpError("Invalid aggregation object requested!"); } + $aggregateFieldset = explode(",", $aggregateFieldset); foreach ($aggregateFieldset as $field) { if (!in_array($field, $availableFieldsets[$name])) { throw new HttpError("Invalid aggregation requested!"); From 11185a5d270e9a5115941a19957aa7e247212498 Mon Sep 17 00:00:00 2001 From: s3inlc Date: Wed, 17 Jun 2026 11:38:41 +0200 Subject: [PATCH 09/10] made aggregations completely generic, so we can avoid having lots of hardcoded string keys --- src/inc/apiv2/common/AbstractBaseAPI.php | 41 +-- src/inc/apiv2/common/AbstractModelAPI.php | 2 +- src/inc/apiv2/common/openAPISchema.routes.php | 4 +- src/inc/apiv2/model/AgentAPI.php | 37 ++- src/inc/apiv2/model/ApiTokenAPI.php | 39 +-- src/inc/apiv2/model/PreTaskAPI.php | 61 ++--- src/inc/apiv2/model/TaskAPI.php | 187 ++++++------- src/inc/apiv2/model/TaskWrapperDisplayAPI.php | 250 ++++++++++-------- 8 files changed, 330 insertions(+), 291 deletions(-) diff --git a/src/inc/apiv2/common/AbstractBaseAPI.php b/src/inc/apiv2/common/AbstractBaseAPI.php index 4de3e6614..c2697594a 100644 --- a/src/inc/apiv2/common/AbstractBaseAPI.php +++ b/src/inc/apiv2/common/AbstractBaseAPI.php @@ -8,9 +8,7 @@ use Hashtopolis\inc\apiv2\error\InternalError; use Hashtopolis\inc\apiv2\error\ResourceNotFoundError; use Hashtopolis\inc\utils\AccessControl; -use Hashtopolis\inc\utils\AccessUtils; use Hashtopolis\inc\defines\DAccessControl; -use Hashtopolis\inc\utils\HashlistUtils; use Hashtopolis\dba\JoinFilter; use Hashtopolis\inc\HTException; use JsonException; @@ -183,17 +181,35 @@ protected function getUpdateHandlers($id, $current_user): array { * * Implementations should use $includedData to collect related resources that should be included * in the API response, such as related entities or additional data. + * @throws HttpError */ public function aggregateData(object $object, array &$includedData = [], ?array $aggregateFieldsets = null): array { - return []; + $aggregatedData = []; + + if (is_array($aggregateFieldsets)) { + $fieldsets = $this->getAggregateFieldsets(); + foreach ($fieldsets as $name => $fieldset) { + if (array_key_exists($name, $aggregateFieldsets)) { + $aggregateFieldsets[$name] = explode(",", $aggregateFieldsets[$name]); + foreach($aggregateFieldsets[$name] as $field) { + if(!array_key_exists($field, $fieldset)) { + throw new HttpError("Invalid aggregation requested!"); + } + $aggregatedData[$field] = $fieldset[$field]($object); + } + } + } + } + return $aggregatedData; } /** - * Return supported aggregate fieldsets/options for this endpoint. + * Return supported aggregate fieldsets/options for this endpoint, providing a callback to call to actually retrieve + * this aggregation on a specific object. All callbacks expect one argument being the api object. * * Format: * [ - * 'resourceKey' => ['option1', 'option2'] + * 'resourceKey' => ['option1' => [class, function], 'option2' => [class, function]] * ] */ public function getAggregateFieldsets(): array { @@ -710,21 +726,6 @@ protected function obj2Resource(object $obj, array &$expandResult = [], ?array $ $apiClassObject = $this; } - if (is_array($aggregateFieldsets)) { - $availableFieldsets = $apiClassObject->getAggregateFieldsets(); - foreach ($aggregateFieldsets as $name => $aggregateFieldset) { - if (!array_key_exists($name, $availableFieldsets)) { - throw new HttpError("Invalid aggregation object requested!"); - } - $aggregateFieldset = explode(",", $aggregateFieldset); - foreach ($aggregateFieldset as $field) { - if (!in_array($field, $availableFieldsets[$name])) { - throw new HttpError("Invalid aggregation requested!"); - } - } - } - } - $aggregatedData = $apiClassObject->aggregateData($obj, $expandResult, $aggregateFieldsets); $attributes = array_merge($attributes, $aggregatedData); diff --git a/src/inc/apiv2/common/AbstractModelAPI.php b/src/inc/apiv2/common/AbstractModelAPI.php index bf3ef423f..d95488af8 100644 --- a/src/inc/apiv2/common/AbstractModelAPI.php +++ b/src/inc/apiv2/common/AbstractModelAPI.php @@ -772,7 +772,7 @@ public static function getManyResources(object $apiClass, Request $request, Resp // Convert objects to data resources foreach ($objects as $object) { - // Create object + // Create object $newObject = $apiClass->obj2Resource($object, $expandResult, $request->getQueryParams()['fields'] ?? null, $request->getQueryParams()['aggregate'] ?? null); $includedResources = $apiClass->processExpands($apiClass, $expands, $object, $expandResult, $includedResources, $request->getQueryParams()['fields'] ?? null, $request->getQueryParams()['aggregate'] ?? null); diff --git a/src/inc/apiv2/common/openAPISchema.routes.php b/src/inc/apiv2/common/openAPISchema.routes.php index ae59d2f9b..e77f6abda 100644 --- a/src/inc/apiv2/common/openAPISchema.routes.php +++ b/src/inc/apiv2/common/openAPISchema.routes.php @@ -699,8 +699,8 @@ if (empty($options)) { continue; } - $aggregateExamples["aggregate[" . $fieldset . "]"] = implode(",", $options); - $aggregateDescriptionParts[] = $fieldset . ": " . implode(", ", $options); + $aggregateExamples["aggregate[" . $fieldset . "]"] = implode(",", array_keys($options)); + $aggregateDescriptionParts[] = $fieldset . ": " . implode(", ", array_keys($options)); } if (!empty($aggregateExamples)) { diff --git a/src/inc/apiv2/model/AgentAPI.php b/src/inc/apiv2/model/AgentAPI.php index a07686faa..363a961b1 100644 --- a/src/inc/apiv2/model/AgentAPI.php +++ b/src/inc/apiv2/model/AgentAPI.php @@ -50,11 +50,27 @@ protected function getUpdateHandlers($id, $current_user): array { public function getAggregateFieldsets(): array { return [ 'agent' => [ - 'crackingTime', + 'crackingTime' => [$this, 'getAggregateCrackingTime'], ] ]; } + /** + * @param object $object + * @return int + * @throws Exception + */ + protected function getAggregateCrackingTime(object $object): int { + // in order to make sense of the diff, we need to make sure that both values solve time and dispatch time are set (i.e. >0). + $qF1 = new QueryFilter(Chunk::AGENT_ID, $object->getId(), "="); + $qF2 = new QueryFilter(Chunk::SOLVE_TIME, 0, ">"); + $qF3 = new QueryFilter(Chunk::DISPATCH_TIME, 0, ">"); + $agg1 = new Aggregation(Chunk::SOLVE_TIME, Aggregation::SUM); + $agg2 = new Aggregation(Chunk::DISPATCH_TIME, Aggregation::SUM); + $results = Factory::getChunkFactory()->multicolAggregationFilter([Factory::FILTER => [$qF1, $qF2, $qF3]], [$agg1, $agg2]); + return $results[$agg1->getName()] - $results[$agg2->getName()]; + } + /** * Overridable function to aggregate data in the object. active chunk of agent is appended to * $included_data. @@ -66,8 +82,6 @@ public function getAggregateFieldsets(): array { * @throws Exception */ function aggregateData(object $object, array &$includedData = [], ?array $aggregateFieldsets = null): array { - $aggregatedData = []; - $agentId = $object->getId(); $qFs = []; $qFs[] = new QueryFilter(Chunk::AGENT_ID, $agentId, "="); @@ -78,22 +92,7 @@ function aggregateData(object $object, array &$includedData = [], ?array $aggreg $includedData["chunks"][$agentId] = [$active_chunk]; } - if (!is_null($aggregateFieldsets) && array_key_exists('agent', $aggregateFieldsets)) { - $aggregateFieldsets['agent'] = explode(",", $aggregateFieldsets['agent']); - - if (in_array("crackingTime", $aggregateFieldsets['agent'])) { - // in order to make sense of the diff, we need to make sure that both values solve time and dispatch time are set (i.e. >0). - $qF1 = new QueryFilter(Chunk::AGENT_ID, $agentId, "="); - $qF2 = new QueryFilter(Chunk::SOLVE_TIME, 0, ">"); - $qF3 = new QueryFilter(Chunk::DISPATCH_TIME, 0, ">"); - $agg1 = new Aggregation(Chunk::SOLVE_TIME, Aggregation::SUM); - $agg2 = new Aggregation(Chunk::DISPATCH_TIME, Aggregation::SUM); - $results = Factory::getChunkFactory()->multicolAggregationFilter([Factory::FILTER => [$qF1, $qF2, $qF3]], [$agg1, $agg2]); - $aggregatedData["crackingTime"] = $results[$agg1->getName()] - $results[$agg2->getName()]; - } - } - - return $aggregatedData; + return parent::aggregateData($object, $includedData, $aggregateFieldsets); } protected function getSingleACL(User $user, object $object): bool { diff --git a/src/inc/apiv2/model/ApiTokenAPI.php b/src/inc/apiv2/model/ApiTokenAPI.php index 883a5886e..d2e4703b5 100644 --- a/src/inc/apiv2/model/ApiTokenAPI.php +++ b/src/inc/apiv2/model/ApiTokenAPI.php @@ -9,6 +9,8 @@ use Hashtopolis\dba\QueryFilter; use Hashtopolis\inc\apiv2\common\AbstractModelAPI; use Hashtopolis\inc\apiv2\error\HttpError; +use Hashtopolis\inc\apiv2\error\HttpForbidden; +use Hashtopolis\inc\apiv2\error\ResourceNotFoundError; use Hashtopolis\inc\StartupConfig; use Hashtopolis\inc\utils\AccessUtils; @@ -17,15 +19,15 @@ class ApiTokenAPI extends AbstractModelAPI { const API_AUD = "api_hashtopolis"; private ?string $jwtToken = null; - + private function setJwtToken(string $token): void { - $this->jwtToken = $token; + $this->jwtToken = $token; } - + private function getJwtToken(): ?string { return $this->jwtToken; } - + public static function getBaseUri(): string { return "/api/v2/ui/apiTokens"; } @@ -48,14 +50,14 @@ public static function getToOneRelationships(): array { ] ]; } - + public function getFormFields(): array { // TODO Form declarations in more generic class to allow auto-generated OpenAPI specifications return [ "scopes" => ['type' => 'array', 'subtype' => 'string'] ]; } - + protected function getSingleACL(User $user, object $object): bool { return ($object->getUserId() === $user->getId()); } @@ -68,29 +70,31 @@ protected function getFilterACL(): array { ] ]; } + /** * @throws HttpError + * @throws ResourceNotFoundError */ protected function createObject(array $data): int { //Scopes is an array of permissions in format [permFileTaskUpdate, permAgentDelete] $scopes = explode(",", $data["scopes"]); - + $userCrudPerms = AccessUtils::getPermissionArrayConverted( $this->getRightGroup($this->getCurrentUser()->getRightGroupId())->getPermissions() ); - + // Modern CRUD scope dict: true if the perm was requested AND the user has it. $requestedScopes = []; foreach ($userCrudPerms as $perm => $granted) { $requestedScopes[$perm] = $granted && in_array($perm, $scopes, true); } - + $secret = StartupConfig::getInstance()->getPepper(0); $iat = $data[JwtApiKey::START_VALID]; $expires = $data[JwtApiKey::END_VALID]; $token = JwtTokenUtils::createKey($this->getCurrentUser()->getId(), $iat, $expires); $jti = $token->getId(); - + $payload = [ "iat" => $iat, "exp" => $expires, @@ -99,15 +103,15 @@ protected function createObject(array $data): int { "scope" => json_encode($requestedScopes), "iss" => "Hashtopolis", "aud" => $this::API_AUD, - "kid" => hash("sha256", $secret) + "kid" => hash("sha256", $secret) ]; - + $tokenEncoded = JWT::encode($payload, $secret, "HS256"); $this->setJwtToken($tokenEncoded); - + return $token->getId(); } - + function aggregateData(object $object, array &$includedData = [], ?array $aggregateFieldsets = null): array { // $token is only set in POST, this way the actual token is only returned after creation. $aggregatedData = []; @@ -115,12 +119,13 @@ function aggregateData(object $object, array &$includedData = [], ?array $aggreg if ($token !== null) { $aggregatedData["token"] = $token; } - - return $aggregatedData; + + return array_merge_recursive($aggregatedData, parent::aggregateData($object, $includedData, $aggregateFieldsets)); } /** - * @throws HttpError + * @param object $object + * @throws HttpForbidden */ protected function deleteObject(object $object): void { JwtTokenUtils::deleteKey($object); diff --git a/src/inc/apiv2/model/PreTaskAPI.php b/src/inc/apiv2/model/PreTaskAPI.php index 3ed52d839..1c4c90bba 100644 --- a/src/inc/apiv2/model/PreTaskAPI.php +++ b/src/inc/apiv2/model/PreTaskAPI.php @@ -46,6 +46,34 @@ public function getFormFields(): array { ]; } + public function getAggregateFieldsets(): array { + return [ + 'pretask' => [ + 'auxiliaryKeyspace' => [$this, 'getAggregateAuxiliaryKeyspace'], + ] + ]; + } + + /** + * @param object $object + * @return int + */ + protected function getAggregateAuxiliaryKeyspace(object $object): int { + $qF1 = new QueryFilter(FilePretask::PRETASK_ID, $object->getId(), "=", Factory::getFilePretaskFactory()); + $jF1 = new JoinFilter(Factory::getFilePretaskFactory(), File::FILE_ID, FilePretask::FILE_ID); + $files = Factory::getFileFactory()->filter([Factory::FILTER => $qF1, Factory::JOIN => $jF1]); + $files = $files[Factory::getFileFactory()->getModelName()]; + + $lineCountProduct = 1; + foreach ($files as $file) { + $lineCount = $file->getLineCount(); + if ($lineCount !== null) { + $lineCountProduct = $lineCountProduct * $lineCount; + } + } + return $lineCountProduct; + } + /** * @throws HttpError */ @@ -68,39 +96,6 @@ protected function createObject(array $data): int { return $pretask->getId(); } - - /** - * @param object $object - * @param array $includedData - * @param array|null $aggregateFieldsets - * @return array - */ - function aggregateData(object $object, array &$includedData = [], ?array $aggregateFieldsets = null): array { - $aggregatedData = []; - - if (!is_null($aggregateFieldsets) && array_key_exists('pretask', $aggregateFieldsets)) { - $aggregateFieldsets['pretask'] = explode(",", $aggregateFieldsets['pretask']); - - if (in_array("auxiliaryKeyspace", $aggregateFieldsets['pretask'])) { - $qF1 = new QueryFilter(FilePretask::PRETASK_ID, $object->getId(), "=", Factory::getFilePretaskFactory()); - $jF1 = new JoinFilter(Factory::getFilePretaskFactory(), File::FILE_ID, FilePretask::FILE_ID); - $files = Factory::getFileFactory()->filter([Factory::FILTER => $qF1, Factory::JOIN => $jF1]); - $files = $files[Factory::getFileFactory()->getModelName()]; - - $lineCountProduct = 1; - foreach ($files as $file) { - $lineCount = $file->getLineCount(); - if ($lineCount !== null) { - $lineCountProduct = $lineCountProduct * $lineCount; - } - } - $aggregatedData["auxiliaryKeyspace"] = $lineCountProduct; - } - } - - return $aggregatedData; - } - protected function getUpdateHandlers($id, $current_user): array { return [ Pretask::ATTACK_CMD => fn($value) => PretaskUtils::changeAttack($id, $value), diff --git a/src/inc/apiv2/model/TaskAPI.php b/src/inc/apiv2/model/TaskAPI.php index 7cd3050c6..355d6399c 100644 --- a/src/inc/apiv2/model/TaskAPI.php +++ b/src/inc/apiv2/model/TaskAPI.php @@ -142,19 +142,108 @@ public function getFormFields(): array { public function getAggregateFieldsets(): array { return [ 'task' => [ - 'totalAssignedAgents', - 'dispatched', - 'searched', - 'status', - 'totalNumberOfChunks', - 'currentSpeed', - 'estimatedTime', - 'cprogress', - 'timeSpent' + 'totalAssignedAgents' => [$this, 'getAggregateTotalAssignedAgents'], + 'dispatched' => [$this, 'getAggregateDispatched'], + 'searched' => [$this, 'getAggregateSearched'], + 'status' => [$this, 'getAggregateStatus'], + 'totalNumberOfChunks' => [$this, 'getAggregateTotalChunks'], + 'currentSpeed' => [$this, 'getAggregateCurrentSpeed'], + 'estimatedTime' => [$this, 'getAggregateEstimatedTime'], + 'cprogress' => [$this, 'getAggregateCProgress'], + 'timeSpent' => [$this, 'getAggregateTimeSpent'], ] ]; } + /** + * @throws Exception + */ + protected function getAggregateTotalAssignedAgents(object $object): int { + $qF = new QueryFilter(Assignment::TASK_ID, $object->getId(), "="); + return Factory::getAssignmentFactory()->countFilter([Factory::FILTER => $qF]); + } + + protected function getAggregateDispatched(object $object): string { + /** @var Task $object */ + $keyspace = $object->getKeyspace(); + $keyspaceProgress = $object->getKeyspaceProgress(); + return Util::showperc($keyspaceProgress, $keyspace); + } + + /** + * @throws Exception + */ + protected function getAggregateSearched(object $object): string { + /** @var Task $object */ + $keyspace = $object->getKeyspace(); + return Util::showperc(TaskUtils::getTaskProgress($object), $keyspace); + } + + protected function getAggregateStatus(object $object): int { + /** @var Task $object */ + $keyspace = $object->getKeyspace(); + $keyspaceProgress = $object->getKeyspaceProgress(); + + // the filter for progress is needed so we reduce the checked chunks numbers by a lot + $qF1 = new QueryFilter(Chunk::TASK_ID, $object->getId(), "="); + $qF2 = new QueryFilter(Chunk::PROGRESS, 10000, "<"); + $chunks = Factory::getChunkFactory()->filter([Factory::FILTER => [$qF1, $qF2]]); + return TaskUtils::getStatus($chunks, $keyspace, $keyspaceProgress); + } + + /** + * @throws Exception + */ + protected function getAggregateTotalChunks(object $object): int { + $qF = new QueryFilter(Chunk::TASK_ID, $object->getId(), "="); + return Factory::getChunkFactory()->countFilter([Factory::FILTER => $qF]); + } + + /** + * @throws Exception + */ + protected function getAggregateCurrentSpeed(object $object): int { + $qF1 = new QueryFilter(Chunk::TASK_ID, $object->getId(), "="); + $qF2 = new QueryFilter(Chunk::SOLVE_TIME, time() - SConfig::getInstance()->getVal(DConfig::CHUNK_TIMEOUT), ">"); + $qF3 = new QueryFilter(Chunk::PROGRESS, 10000, "<"); + $agg = new Aggregation(Chunk::SPEED, Aggregation::SUM); + $speed = Factory::getChunkFactory()->multicolAggregationFilter([Factory::FILTER => [$qF1, $qF2, $qF3]], [$agg])[$agg->getName()]; + if ($speed == null) { + $speed = 0; + } + return $speed; + } + + /** + * @throws Exception + */ + protected function getAggregateEstimatedTime(object $object): int { + /** @var Task $object */ + $keyspace = $object->getKeyspace(); + + // not a 100% efficient, but we would have to break up the nice generic handling of the aggregations to deal with this + $cProgress = $this->getAggregateCProgress($object); + $timeSpent = $this->getAggregateTimeSpent($object); + + return ($keyspace > 0 && $cProgress > 0) ? round($timeSpent / ($cProgress / $keyspace) - $timeSpent) : 0; + } + + /** + * @throws Exception + */ + protected function getAggregateCProgress(object $object): int { + /** @var Task $object */ + return TaskUtils::getTaskProgress($object); + } + + /** + * @throws Exception + */ + protected function getAggregateTimeSpent(object $object): int { + /** @var Task $object */ + return TaskUtils::getTimeSpentOnTask($object); + } + /** * @throws HttpError */ @@ -188,86 +277,6 @@ protected function createObject(array $data): int { return $task->getId(); } - /** - * @param object $object - * @param array $includedData - * @param array|null $aggregateFieldsets - * @return array - * @throws Exception - */ - function aggregateData(object $object, array &$includedData = [], ?array $aggregateFieldsets = null): array { - /** @var Task $object */ - $aggregatedData = []; - - if (!is_null($aggregateFieldsets) && array_key_exists('task', $aggregateFieldsets)) { - $aggregateFieldsets['task'] = explode(",", $aggregateFieldsets['task']); - - if (in_array("assignedAgents", $aggregateFieldsets['task'])) { - $qF = new QueryFilter(Assignment::TASK_ID, $object->getId(), "="); - $assignedAgents = Factory::getAssignmentFactory()->countFilter([Factory::FILTER => $qF]); - $aggregatedData["totalAssignedAgents"] = $assignedAgents; - } - - $keyspace = $object->getKeyspace(); - $keyspaceProgress = $object->getKeyspaceProgress(); - - if (in_array("dispatched", $aggregateFieldsets['task'])) { - $aggregatedData["dispatched"] = Util::showperc($keyspaceProgress, $keyspace); - } - - if (in_array("searched", $aggregateFieldsets['task'])) { - $aggregatedData["searched"] = Util::showperc(TaskUtils::getTaskProgress($object), $keyspace); - } - - if (in_array("status", $aggregateFieldsets['task'])) { - // the filter for progress is needed so we reduce the checked chunks numbers by a lot - $qF1 = new QueryFilter(Chunk::TASK_ID, $object->getId(), "="); - $qF2 = new QueryFilter(Chunk::PROGRESS, 10000, "<"); - $chunks = Factory::getChunkFactory()->filter([Factory::FILTER => [$qF1, $qF2]]); - $aggregatedData["status"] = TaskUtils::getStatus($chunks, $keyspace, $keyspaceProgress); - } - - if (in_array("totalNumberOfChunks", $aggregateFieldsets['task'])) { - $qF = new QueryFilter(Chunk::TASK_ID, $object->getId(), "="); - $aggregatedData["totalNumberOfChunks"] = Factory::getChunkFactory()->countFilter([Factory::FILTER => $qF]); - } - - if (in_array("currentSpeed", $aggregateFieldsets['task'])) { - $qF1 = new QueryFilter(Chunk::TASK_ID, $object->getId(), "="); - $qF2 = new QueryFilter(Chunk::SOLVE_TIME, time() - SConfig::getInstance()->getVal(DConfig::CHUNK_TIMEOUT), ">"); - $qF3 = new QueryFilter(Chunk::PROGRESS, 10000, "<"); - $agg = new Aggregation(Chunk::SPEED, Aggregation::SUM); - $speed = Factory::getChunkFactory()->multicolAggregationFilter([Factory::FILTER => [$qF1, $qF2, $qF3]], [$agg])[$agg->getName()]; - if ($speed == null) { - $speed = 0; - } - $aggregatedData["currentSpeed"] = $speed; - } - - if (in_array("estimatedTime", $aggregateFieldsets['task']) || - in_array("timeSpent", $aggregateFieldsets['task']) || - in_array("cprogress", $aggregateFieldsets['task'])) { - - $cProgress = TaskUtils::getTaskProgress($object); - if (in_array("cprogress", $aggregateFieldsets['task'])) { - $aggregatedData["cprogress"] = $cProgress; - } - - $timeSpent = TaskUtils::getTimeSpentOnTask($object); - - if (in_array("timeSpent", $aggregateFieldsets['task'])) { - $aggregatedData["timeSpent"] = $timeSpent; - } - - if (in_array("estimatedTime", $aggregateFieldsets['task'])) { - $aggregatedData["estimatedTime"] = ($keyspace > 0 && $cProgress > 0) ? round($timeSpent / ($cProgress / $keyspace) - $timeSpent) : 0; - } - } - } - - return $aggregatedData; - } - protected function deleteObject(object $object): void { /** @var Task $object */ TaskUtils::deleteTask($object); diff --git a/src/inc/apiv2/model/TaskWrapperDisplayAPI.php b/src/inc/apiv2/model/TaskWrapperDisplayAPI.php index f007afecb..9246c8881 100644 --- a/src/inc/apiv2/model/TaskWrapperDisplayAPI.php +++ b/src/inc/apiv2/model/TaskWrapperDisplayAPI.php @@ -28,18 +28,19 @@ class TaskWrapperDisplayAPI extends AbstractModelAPI { public static function getBaseUri(): string { return "/api/v2/ui/taskwrapperdisplays"; } - + public static function getAvailableMethods(): array { return ['GET']; } + public function getRequiredPermissions(string $method): array { return [Task::PERM_READ, TaskWrapper::PERM_READ]; } - + public static function getDBAclass(): string { return TaskWrapperDisplay::class; } - + protected function getSingleACL(User $user, object $object): bool { $accessGroupsUser = Util::arrayOfIds(AccessUtils::getAccessGroupsOfUser($user)); @@ -49,9 +50,9 @@ protected function getSingleACL(User $user, object $object): bool { $wrappers = Factory::getTaskWrapperFactory()->filter([Factory::FILTER => [$qF1, $qF2], Factory::JOIN => $jF])[Factory::getTaskWrapperFactory()->getModelName()]; return count($wrappers) > 0; } - + protected function getFilterACL(): array { - + $accessGroups = Util::arrayOfIds(AccessUtils::getAccessGroupsOfUser($this->getCurrentUser())); return [ @@ -67,142 +68,171 @@ protected function getFilterACL(): array { public function getAggregateFieldsets(): array { return [ 'taskwrapperdisplay' => [ - 'totalAssignedAgents', - 'dispatched', - 'searched', - 'status', - 'currentSpeed', - 'estimatedTime', - 'cprogress', - 'timeSpent' + 'totalAssignedAgents' => [$this, 'getAggregateTotalAssignedAgents'], + 'dispatched' => [$this, 'getAggregateDispatched'], + 'searched' => [$this, 'getAggregateSearched'], + 'status' => [$this, 'getAggregateStatus'], + 'currentSpeed' => [$this, 'getAggregateCurrentSpeed'], + 'estimatedTime' => [$this, 'getAggregateEstimatedTime'], + 'cprogress' => [$this, 'getAggregateCProgress'], + 'timeSpent' => [$this, 'getAggregateTimeSpent'], ] ]; } /** - * @param object $object - * @param array $includedData - * @param array|null $aggregateFieldsets - * @return array * @throws Exception */ - function aggregateData(object $object, array &$includedData = [], ?array $aggregateFieldsets = null): array { - $aggregatedData = []; + protected function getAggregateTotalAssignedAgents(object $object): int { + $qF = new QueryFilter(Task::TASK_WRAPPER_ID, $object->getId(), "=", Factory::getTaskFactory()); + $jF = new JoinFilter(Factory::getTaskFactory(), Assignment::TASK_ID, Task::TASK_ID); + + return Factory::getAssignmentFactory()->countFilter([Factory::FILTER => $qF, Factory::JOIN => $jF]); + } + + /** + * @throws HttpError + */ + protected function getAggregateDispatched(object $object): string { + if ($object->getTaskType() !== DTaskTypes::NORMAL) { + throw new HttpError("Not possible to aggregate dispatched on other types than normal task!"); + } - if (!is_null($aggregateFieldsets) && array_key_exists('taskwrapperdisplay', $aggregateFieldsets)) { - $aggregateFieldsets['taskwrapperdisplay'] = explode(",", $aggregateFieldsets['taskwrapperdisplay']); - - if (in_array("status", $aggregateFieldsets['taskwrapperdisplay'])) { - // TODO: this could be optimized by only requesting taskId, keyspace and keyspaceProgress of all tasks of that wrapper (columnFilter) - $tasks = TaskUtils::getTasksOfWrapper($object->getId()); - $completed = 0; - $total = 0; - $status = 0; - foreach($tasks as $task) { - $qF1 = new QueryFilter(Chunk::TASK_ID, $task->getId(), "="); - $qF2 = new QueryFilter(Chunk::PROGRESS, 10000, "<"); - $chunks = Factory::getChunkFactory()->filter([Factory::FILTER => [$qF1, $qF2]]); - $taskStatus = TaskUtils::getStatus($chunks, $task->getKeyspace(), $task->getKeyspaceProgress()); - // if one task of the wrapper is running, it is running - if ($taskStatus === 1) { - $status = 1; - break; - } - if ($taskStatus === 3) { - $completed++; - } - $total++; - } - if ($status !== 1) { - if ($total > 0 && $completed === $total) { - $status = 3; - } else { - $status = 2; - } - } - $aggregatedData['status'] = $status; + $keyspace = $object->getKeyspace(); + $keyspaceProgress = $object->getKeyspaceProgress(); + return Util::showperc($keyspaceProgress, $keyspace); + } + + /** + * @throws HttpError + * @throws Exception + */ + protected function getAggregateSearched(object $object): string { + if ($object->getTaskType() !== DTaskTypes::NORMAL) { + throw new HttpError("Not possible to aggregate searched on other types than normal task!"); + } + + $keyspace = $object->getKeyspace(); + $task = TaskUtils::getTasksOfWrapper($object->getId())[0]; + return Util::showperc(TaskUtils::getTaskProgress($task), $keyspace); + } + + protected function getAggregateStatus(object $object): int { + // TODO: this could be optimized by only requesting taskId, keyspace and keyspaceProgress of all tasks of that wrapper (columnFilter) + $tasks = TaskUtils::getTasksOfWrapper($object->getId()); + $completed = 0; + $total = 0; + $status = 0; + foreach ($tasks as $task) { + $qF1 = new QueryFilter(Chunk::TASK_ID, $task->getId(), "="); + $qF2 = new QueryFilter(Chunk::PROGRESS, 10000, "<"); + $chunks = Factory::getChunkFactory()->filter([Factory::FILTER => [$qF1, $qF2]]); + $taskStatus = TaskUtils::getStatus($chunks, $task->getKeyspace(), $task->getKeyspaceProgress()); + // if one task of the wrapper is running, it is running + if ($taskStatus === 1) { + $status = 1; + break; } - - if (in_array("totalAssignedAgents", $aggregateFieldsets['taskwrapperdisplay'])) { - $qF = new QueryFilter(Task::TASK_WRAPPER_ID, $object->getId(), "=", Factory::getTaskFactory()); - $jF = new JoinFilter(Factory::getTaskFactory(), Assignment::TASK_ID, Task::TASK_ID); - - $assignedAgents = Factory::getAssignmentFactory()->countFilter([Factory::FILTER => $qF, Factory::JOIN => $jF]); - $aggregatedData["totalAssignedAgents"] = $assignedAgents; + if ($taskStatus === 3) { + $completed++; } - - $keyspace = $object->getKeyspace(); - $keyspaceProgress = $object->getKeyspaceProgress(); - - if ($object->getTaskType() === DTaskTypes::NORMAL) { - $tasks = TaskUtils::getTasksOfWrapper($object->getId()); - $task = $tasks[0]; - - if (in_array("dispatched", $aggregateFieldsets['taskwrapperdisplay'])) { - $aggregatedData["dispatched"] = Util::showperc($keyspaceProgress, $keyspace); - } - - if (in_array("searched", $aggregateFieldsets['taskwrapperdisplay'])) { - $aggregatedData["searched"] = Util::showperc(TaskUtils::getTaskProgress($task), $keyspace); - } - - if (in_array("currentSpeed", $aggregateFieldsets['taskwrapperdisplay'])) { - $qF1 = new QueryFilter(Chunk::TASK_ID, $task->getId(), "="); - $qF2 = new QueryFilter(Chunk::SOLVE_TIME, time() - SConfig::getInstance()->getVal(DConfig::CHUNK_TIMEOUT), ">"); - $qF3 = new QueryFilter(Chunk::PROGRESS, 10000, "<"); - $agg = new Aggregation(Chunk::SPEED, Aggregation::SUM); - $speed = Factory::getChunkFactory()->multicolAggregationFilter([Factory::FILTER => [$qF1, $qF2, $qF3]], [$agg])[$agg->getName()]; - if ($speed == null) { - $speed = 0; - } - $aggregatedData["currentSpeed"] = $speed; - } - - if (in_array("estimatedTime", $aggregateFieldsets['taskwrapperdisplay']) || - in_array("timeSpent", $aggregateFieldsets['taskwrapperdisplay']) || - in_array("cprogress", $aggregateFieldsets['taskwrapperdisplay'])) { - - $cProgress = TaskUtils::getTaskProgress($task); - if (in_array("cprogress", $aggregateFieldsets['taskwrapperdisplay'])) { - $aggregatedData["cprogress"] = $cProgress; - } - - $timeSpent = TaskUtils::getTimeSpentOnTask($task); - - if (in_array("timeSpent", $aggregateFieldsets['taskwrapperdisplay'])) { - $aggregatedData["timeSpent"] = $timeSpent; - } - - if (in_array("estimatedTime", $aggregateFieldsets['taskwrapperdisplay'])) { - $aggregatedData["estimatedTime"] = ($keyspace > 0 && $cProgress > 0) ? round($timeSpent / ($cProgress / $keyspace) - $timeSpent) : 0; - } - } + $total++; + } + if ($status !== 1) { + if ($total > 0 && $completed === $total) { + $status = 3; + } + else { + $status = 2; } } - return $aggregatedData; + return $status; } - + + /** + * @throws HttpError + * @throws Exception + */ + protected function getAggregateCurrentSpeed(object $object): int { + if ($object->getTaskType() !== DTaskTypes::NORMAL) { + throw new HttpError("Not possible to aggregate currentSpeed on other types than normal task!"); + } + + $task = TaskUtils::getTasksOfWrapper($object->getId())[0]; + + $qF1 = new QueryFilter(Chunk::TASK_ID, $task->getId(), "="); + $qF2 = new QueryFilter(Chunk::SOLVE_TIME, time() - SConfig::getInstance()->getVal(DConfig::CHUNK_TIMEOUT), ">"); + $qF3 = new QueryFilter(Chunk::PROGRESS, 10000, "<"); + $agg = new Aggregation(Chunk::SPEED, Aggregation::SUM); + $speed = Factory::getChunkFactory()->multicolAggregationFilter([Factory::FILTER => [$qF1, $qF2, $qF3]], [$agg])[$agg->getName()]; + if ($speed == null) { + $speed = 0; + } + return $speed; + } + + /** + * @throws HttpError + * @throws Exception + */ + protected function getAggregateEstimatedTime(object $object): int { + if ($object->getTaskType() !== DTaskTypes::NORMAL) { + throw new HttpError("Not possible to aggregate estimatedTime on other types than normal task!"); + } + + $keyspace = $object->getKeyspace(); + $cProgress = $this->getAggregateCProgress($object); + $timeSpent = $this->getAggregateTimeSpent($object); + return ($keyspace > 0 && $cProgress > 0) ? round($timeSpent / ($cProgress / $keyspace) - $timeSpent) : 0; + } + + /** + * @throws HttpError + * @throws Exception + */ + protected function getAggregateCProgress(object $object): int { + if ($object->getTaskType() !== DTaskTypes::NORMAL) { + throw new HttpError("Not possible to aggregate cprogress on other types than normal task!"); + } + + $task = TaskUtils::getTasksOfWrapper($object->getId())[0]; + return TaskUtils::getTaskProgress($task); + } + + /** + * @throws HttpError + * @throws Exception + */ + protected function getAggregateTimeSpent(object $object): int { + if ($object->getTaskType() !== DTaskTypes::NORMAL) { + throw new HttpError("Not possible to aggregate timeSpent on other types than normal task!"); + } + + $task = TaskUtils::getTasksOfWrapper($object->getId())[0]; + return TaskUtils::getTimeSpentOnTask($task); + } + /** * @throws HttpError */ protected function createObject(array $data): int { throw new HttpError("TaskWrapperDisplays cannot be created via API"); } - + /** * @throws HttpError */ public function updateObject(int $objectId, array $data): void { throw new HttpError("TaskWrapperDisplays cannot be updated via API"); } - + /** * @throws HttpError */ protected function deleteObject(object $object): void { throw new HttpError("TaskWrapperDisplays cannot be deleted via API"); } - + public static function getToManyRelationships(): array { return [ 'tasks' => [ From 9530220b9c0d4f12551daf31e905a3671002c9ff Mon Sep 17 00:00:00 2001 From: s3inlc Date: Wed, 17 Jun 2026 14:39:44 +0200 Subject: [PATCH 10/10] fixed phpdoc inconsistency --- src/inc/utils/TaskUtils.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/inc/utils/TaskUtils.php b/src/inc/utils/TaskUtils.php index 90b70224d..e3a7661d7 100644 --- a/src/inc/utils/TaskUtils.php +++ b/src/inc/utils/TaskUtils.php @@ -1395,7 +1395,7 @@ public static function isSaturatedByOtherAgents($task, $agent) { /** * @param $task Task - * @return mixed + * @return int * @throws Exception */ public static function getTaskProgress(Task $task): int {