Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,10 @@
"phpstan/extension-installer": true
},
"process-timeout": 0,
"sort-packages": true
"sort-packages": true,
"audit": {
"block-insecure": false
}
},
"autoload": {
"psr-4": {
Expand Down
225 changes: 225 additions & 0 deletions src/inc/apiv2/helper/getTaskProgressImage.routes.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,225 @@
<?php

use DBA\Chunk;
use JetBrains\PhpStorm\NoReturn;
use Psr\Http\Message\ResponseInterface as Response;
use Psr\Http\Message\ServerRequestInterface as Request;
use DBA\Factory;
use DBA\OrderFilter;
use DBA\QueryFilter;
use DBA\Task;
use Middlewares\Utils\HttpErrorException;
use Slim\Exception\HttpNotFoundException;

require_once(dirname(__FILE__) . "/../common/AbstractHelperAPI.class.php");

class GetTaskProgressImageHelperAPI extends AbstractHelperAPI {
public static function getBaseUri(): string {
return "/api/v2/helper/getTaskProgressImage";
}

public static function getAvailableMethods(): array {
return ['GET'];
}

public function getRequiredPermissions(string $method): array {
return [Task::PERM_READ];
}

/**
* getfile is different because it returns actual binary data.
*/
public static function getResponse(): null {
return null;
}


#[NoReturn] public function actionPost(array $data): object|array|null {
assert(False, "GetTaskProgressImage has no POST");
Copy link

Copilot AI Nov 12, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Python-style boolean False used in PHP assertion. PHP uses lowercase false. This will cause a fatal error if the code path is ever reached and assertions are enabled, as False is an undefined constant in PHP (unless defined elsewhere).

Suggested change
assert(False, "GetTaskProgressImage has no POST");
assert(false, "GetTaskProgressImage has no POST");

Copilot uses AI. Check for mistakes.
}

/**
* Description of get params for swagger.
*/
public function getParamsSwagger(): array {
return [
[
"in" => "query",
"name" => "supertask",
"schema" => [
"type" => "integer",
"format" => "int32"
],
"required" => false,
"example" => 1,
"description" => "The ID of the supertask where you want to create the progress image of."
],
[
"in" => "query",
"name" => "task",
"schema" => [
"type" => "integer",
"format" => "int32"
],
"required" => false,
"example" => 1,
"description" => "The ID of the task where you want to create the progress image of."
]
];
}

/**
* Endpoint to download files
* @param Request $request
* @param Response $response
* @return Response
* @throws HTException
* @throws HttpErrorException
* @throws HttpForbidden
*/
public function handleGet(Request $request, Response $response): Response {
$this->preCommon($request);
$task_id = $request->getQueryParams()['task'];
$supertask_id = $request->getQueryParams()['supertask'];
Comment on lines +82 to +83
Copy link

Copilot AI Nov 12, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing validation for required query parameters. Unlike similar endpoints (e.g., getFile.routes.php), this code doesn't check if the required task or supertask parameters exist before accessing them. This will cause PHP notices/warnings if neither parameter is provided. Add validation: if (!isset($request->getQueryParams()['task']) && !isset($request->getQueryParams()['supertask'])) before lines 82-83.

Suggested change
$task_id = $request->getQueryParams()['task'];
$supertask_id = $request->getQueryParams()['supertask'];
$queryParams = $request->getQueryParams();
if (!isset($queryParams['task']) && !isset($queryParams['supertask'])) {
throw new HttpError("No task or super task has been provided");
}
$task_id = isset($queryParams['task']) ? $queryParams['task'] : null;
$supertask_id = isset($queryParams['supertask']) ? $queryParams['supertask'] : null;

Copilot uses AI. Check for mistakes.

//check if task exists and get information
if ($task_id) {
$task = Factory::getTaskFactory()->get($task_id);
if ($task == null) {
throw new HttpNotFoundException($request, "Invalid task");
}
$taskWrapper = Factory::getTaskWrapperFactory()->get($task->getTaskWrapperId());
if ($taskWrapper == null) {
throw new HttpError("Inconsistency on task!");
}
}
else if ($supertask_id) {
$taskWrapper = Factory::getTaskWrapperFactory()->get($supertask_id);
if ($taskWrapper == null) {
throw new HttpError("Invalid task wrapper!");
}
} else {
throw new HttpError("No task or super task has been provided");
}

$size = array(1500, 32);

//create image
$image = imagecreatetruecolor($size[0], $size[1]);
imagesavealpha($image, true);

//set colors
$transparency = imagecolorallocatealpha($image, 0, 0, 0, 127);
$yellow = imagecolorallocate($image, 255, 255, 0);
$red = imagecolorallocate($image, 255, 0, 0);
$grey = imagecolorallocate($image, 192, 192, 192);
$green = imagecolorallocate($image, 0, 255, 0);
$blue = imagecolorallocate($image, 60, 60, 245);

//prepare image
imagefill($image, 0, 0, $transparency);

if ($taskWrapper->getTaskType() == DTaskTypes::SUPERTASK && isset($supertask_id)) {
Copy link

Copilot AI Nov 12, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Inconsistent variable usage in conditional check. Line 122 checks isset($supertask_id) but line 96 uses else if ($supertask_id) which will be truthy even if $supertask_id is 0 or empty string. The logic should be consistent - either use isset() in both places or check for truthiness in both. Additionally, the variable may not be set if neither parameter is provided (which is now only caught by the final else on line 101).

Copilot uses AI. Check for mistakes.
// handle supertask progress drawing here
$qF = new QueryFilter(Task::TASK_WRAPPER_ID, $taskWrapper->getId(), "=");
$oF = new OrderFilter(Task::PRIORITY, "DESC");
$tasks = Factory::getTaskFactory()->filter([Factory::FILTER => $qF, Factory::ORDER => $oF]);
$numTasks = sizeof($tasks);
for ($i = 0; $i < sizeof($tasks); $i++) {
$qF = new QueryFilter(Chunk::TASK_ID, $tasks[$i]->getId(), "=");
$chunks = Factory::getChunkFactory()->filter([Factory::FILTER => $qF]);
$progress = 0;
foreach ($chunks as $chunk) {
$progress += $chunk->getCheckpoint();
}
$qF = new QueryFilter(Chunk::TASK_ID, $tasks[$i]->getId(), "=");
$chunks = Factory::getChunkFactory()->filter([Factory::FILTER => $qF]);
Comment on lines +135 to +136
Copy link

Copilot AI Nov 12, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Duplicate code: Lines 129-131 and 135-137 perform identical database queries to fetch chunks. The query on lines 135-137 is redundant and should be removed. The $chunks variable from lines 129-131 should be reused.

Suggested change
$qF = new QueryFilter(Chunk::TASK_ID, $tasks[$i]->getId(), "=");
$chunks = Factory::getChunkFactory()->filter([Factory::FILTER => $qF]);

Copilot uses AI. Check for mistakes.
$cracked = 0;
foreach ($chunks as $chunk) {
$cracked += $chunk->getCracked();
}
if ($cracked > 0) {
imagefilledrectangle($image, $i * $size[0] / $numTasks, 0, ($i + 1) * $size[0] / $numTasks, $size[1] - 1, $green);
}
else if ($tasks[$i]->getKeyspace() > 0 && $progress >= $tasks[$i]->getKeyspace()) {
imagefilledrectangle($image, $i * $size[0] / $numTasks, 0, ($i + 1) * $size[0] / $numTasks, $size[1] - 1, $blue);
}
else if ($tasks[$i]->getKeyspace() > 0 && $progress > 0) {
imagefilledrectangle($image, $i * $size[0] / $numTasks, 0, ($i + 1) * $size[0] / $numTasks, $size[1] - 1, $yellow);
}
else {
imagefilledrectangle($image, $i * $size[0] / $numTasks, 0, ($i + 1) * $size[0] / $numTasks, $size[1] - 1, $grey);
}
}
}
else {
Copy link

Copilot AI Nov 12, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Potential undefined variable usage. If the $supertask_id path is taken (lines 96-100), the variable $task is never defined, but it's used in line 156 within the else block. This will cause an error when accessing a supertask but falling into the non-supertask rendering path. The conditional logic on line 122 should prevent this, but the code structure is fragile and could lead to bugs.

Suggested change
else {
else {
if (!isset($task)) {
throw new HttpError("Task is not defined for non-supertask rendering path.");
}

Copilot uses AI. Check for mistakes.
$progress = $task->getKeyspaceProgress();
$keyspace = max($task->getKeyspace(), 1);

//load chunks
$qF = new QueryFilter(Task::TASK_ID, $task->getId(), "=");
Copy link

Copilot AI Nov 12, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wrong QueryFilter field. This should use Chunk::TASK_ID instead of Task::TASK_ID since we're filtering chunks by task ID, not tasks. The Chunk model has a TASK_ID field that should be used here.

Suggested change
$qF = new QueryFilter(Task::TASK_ID, $task->getId(), "=");
$qF = new QueryFilter(Chunk::TASK_ID, $task->getId(), "=");

Copilot uses AI. Check for mistakes.
$chunks = Factory::getChunkFactory()->filter([Factory::FILTER => $qF]);
foreach ($chunks as $chunk) {
if ($task->getUsePreprocessor() == 1 && $task->getKeyspace() <= 0) {
continue;
}
$start = floor(($size[0] - 1) * $chunk->getSkip() / $keyspace);
$end = floor(($size[0] - 1) * ($chunk->getSkip() + $chunk->getLength()) / $keyspace) - 1;
//division by 10000 is required because rprogress is saved in percents with two decimals
$current = floor(($size[0] - 1) * ($chunk->getSkip() + $chunk->getLength() * $chunk->getProgress() / 10000) / $keyspace) - 1;

if ($current > $end) {
$current = $end;
}

if ($end - $start < 3) {
if ($chunk->getState() >= 6) {
imagefilledrectangle($image, $start, 0, $end, $size[1] - 1, $red);
}
else if ($chunk->getCracked() > 0) {
imagefilledrectangle($image, $start, 0, $end, $size[1] - 1, $green);
}
else {
imagefilledrectangle($image, $start, 0, $end, $size[1] - 1, $yellow);
}
}
else {
if ($chunk->getState() >= 6) {
imagerectangle($image, $start, 0, $end, ($size[1] - 1), $red);
}
else {
imagerectangle($image, $start, 0, $end, ($size[1] - 1), $grey);
}
if ($chunk->getCracked() > 0) {
imagefilledrectangle($image, $start + 1, 1, $current - 1, $size[1] - 2, $green);
}
else {
imagefilledrectangle($image, $start + 1, 1, $current - 1, $size[1] - 2, $yellow);
}
}
}
}

//send image data to output
ob_start();
imagepng($image);
$imageData = ob_get_clean();
imagedestroy($image);
$response->getBody()->write($imageData);
return $response->withStatus(200)
->withHeader("Content-Type", "image/png")
->withHeader("Cache-Control", "no-cache");
}
Copy link

Copilot AI Nov 12, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing resource cleanup. The image resource created by imagecreatetruecolor() on line 108 is never explicitly destroyed with imagedestroy($image). While PHP will clean this up eventually, it's best practice to explicitly free image resources after use, especially for endpoints that may be called frequently.

Copilot uses AI. Check for mistakes.

static public function register($app): void {
$baseUri = GetTaskProgressImageHelperAPI::getBaseUri();

/* Allow CORS preflight requests */
$app->options($baseUri, function (Request $request, Response $response): Response {
return $response;
});
$app->get($baseUri, "GetTaskProgressImageHelperAPI:handleGet");
}
}

GetTaskProgressImageHelperAPI::register($app);