aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--core/Controller/TaskProcessingApiController.php10
-rw-r--r--core/Migrations/Version30000Date20240717111406.php51
-rw-r--r--core/openapi-full.json10
-rw-r--r--core/openapi.json10
-rw-r--r--lib/composer/composer/autoload_classmap.php1
-rw-r--r--lib/composer/composer/autoload_static.php1
-rw-r--r--lib/private/TaskProcessing/Db/Task.php16
-rw-r--r--lib/private/TaskProcessing/Manager.php79
-rw-r--r--lib/public/TaskProcessing/Task.php37
-rw-r--r--tests/lib/TaskProcessing/TaskProcessingTest.php4
-rw-r--r--version.php2
11 files changed, 215 insertions, 6 deletions
diff --git a/core/Controller/TaskProcessingApiController.php b/core/Controller/TaskProcessingApiController.php
index d9bcbd5da45..6e2a039606f 100644
--- a/core/Controller/TaskProcessingApiController.php
+++ b/core/Controller/TaskProcessingApiController.php
@@ -97,7 +97,8 @@ class TaskProcessingApiController extends \OCP\AppFramework\OCSController {
* @param string $type Type of the task
* @param string $appId ID of the app that will execute the task
* @param string $customId An arbitrary identifier for the task
- *
+ * @param string|null $webhookUri URI to be requested when the task finishes
+ * @param string|null $webhookMethod Method used for the webhook request (HTTP:GET, HTTP:POST, HTTP:PUT, HTTP:DELETE or AppAPI:APP_ID:GET, AppAPI:APP_ID:POST...)
* @return DataResponse<Http::STATUS_OK, array{task: CoreTaskProcessingTask}, array{}>|DataResponse<Http::STATUS_INTERNAL_SERVER_ERROR|Http::STATUS_BAD_REQUEST|Http::STATUS_PRECONDITION_FAILED|Http::STATUS_UNAUTHORIZED, array{message: string}, array{}>
*
* 200: Task scheduled successfully
@@ -109,8 +110,13 @@ class TaskProcessingApiController extends \OCP\AppFramework\OCSController {
#[UserRateLimit(limit: 20, period: 120)]
#[AnonRateLimit(limit: 5, period: 120)]
#[ApiRoute(verb: 'POST', url: '/schedule', root: '/taskprocessing')]
- public function schedule(array $input, string $type, string $appId, string $customId = ''): DataResponse {
+ public function schedule(
+ array $input, string $type, string $appId, string $customId = '',
+ ?string $webhookUri = null, ?string $webhookMethod = null
+ ): DataResponse {
$task = new Task($type, $input, $appId, $this->userId, $customId);
+ $task->setWebhookUri($webhookUri);
+ $task->setWebhookMethod($webhookMethod);
try {
$this->taskProcessingManager->scheduleTask($task);
diff --git a/core/Migrations/Version30000Date20240717111406.php b/core/Migrations/Version30000Date20240717111406.php
new file mode 100644
index 00000000000..67293d8c1e7
--- /dev/null
+++ b/core/Migrations/Version30000Date20240717111406.php
@@ -0,0 +1,51 @@
+<?php
+
+declare(strict_types=1);
+
+/**
+ * SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+namespace OC\Core\Migrations;
+
+use Closure;
+use OCP\DB\ISchemaWrapper;
+use OCP\DB\Types;
+use OCP\Migration\IOutput;
+use OCP\Migration\SimpleMigrationStep;
+
+/**
+ *
+ */
+class Version30000Date20240717111406 extends SimpleMigrationStep {
+
+ /**
+ * @param IOutput $output
+ * @param Closure $schemaClosure The `\Closure` returns a `ISchemaWrapper`
+ * @param array $options
+ * @return null|ISchemaWrapper
+ */
+ public function changeSchema(IOutput $output, Closure $schemaClosure, array $options): ?ISchemaWrapper {
+ /** @var ISchemaWrapper $schema */
+ $schema = $schemaClosure();
+
+ if ($schema->hasTable('taskprocessing_tasks')) {
+ $table = $schema->getTable('taskprocessing_tasks');
+
+ $table->addColumn('webhook_uri', Types::STRING, [
+ 'notnull' => false,
+ 'default' => null,
+ 'length' => 4000,
+ ]);
+ $table->addColumn('webhook_method', Types::STRING, [
+ 'notnull' => false,
+ 'default' => null,
+ 'length' => 64,
+ ]);
+
+ return $schema;
+ }
+
+ return null;
+ }
+}
diff --git a/core/openapi-full.json b/core/openapi-full.json
index 290dc462cdc..a90cc1efaaf 100644
--- a/core/openapi-full.json
+++ b/core/openapi-full.json
@@ -3842,6 +3842,16 @@
"type": "string",
"default": "",
"description": "An arbitrary identifier for the task"
+ },
+ "webhookUri": {
+ "type": "string",
+ "nullable": true,
+ "description": "URI to be requested when the task finishes"
+ },
+ "webhookMethod": {
+ "type": "string",
+ "nullable": true,
+ "description": "Method used for the webhook request (HTTP:GET, HTTP:POST, HTTP:PUT, HTTP:DELETE or AppAPI:APP_ID:GET, AppAPI:APP_ID:POST...)"
}
}
}
diff --git a/core/openapi.json b/core/openapi.json
index 51698f6d430..869bc4ef2c5 100644
--- a/core/openapi.json
+++ b/core/openapi.json
@@ -3842,6 +3842,16 @@
"type": "string",
"default": "",
"description": "An arbitrary identifier for the task"
+ },
+ "webhookUri": {
+ "type": "string",
+ "nullable": true,
+ "description": "URI to be requested when the task finishes"
+ },
+ "webhookMethod": {
+ "type": "string",
+ "nullable": true,
+ "description": "Method used for the webhook request (HTTP:GET, HTTP:POST, HTTP:PUT, HTTP:DELETE or AppAPI:APP_ID:GET, AppAPI:APP_ID:POST...)"
}
}
}
diff --git a/lib/composer/composer/autoload_classmap.php b/lib/composer/composer/autoload_classmap.php
index aa6e50eab20..c074a50f26e 100644
--- a/lib/composer/composer/autoload_classmap.php
+++ b/lib/composer/composer/autoload_classmap.php
@@ -1326,6 +1326,7 @@ return array(
'OC\\Core\\Migrations\\Version29000Date20240124132202' => $baseDir . '/core/Migrations/Version29000Date20240124132202.php',
'OC\\Core\\Migrations\\Version29000Date20240131122720' => $baseDir . '/core/Migrations/Version29000Date20240131122720.php',
'OC\\Core\\Migrations\\Version30000Date20240429122720' => $baseDir . '/core/Migrations/Version30000Date20240429122720.php',
+ 'OC\\Core\\Migrations\\Version30000Date20240717111406' => $baseDir . '/core/Migrations/Version30000Date20240717111406.php',
'OC\\Core\\Notification\\CoreNotifier' => $baseDir . '/core/Notification/CoreNotifier.php',
'OC\\Core\\ResponseDefinitions' => $baseDir . '/core/ResponseDefinitions.php',
'OC\\Core\\Service\\LoginFlowV2Service' => $baseDir . '/core/Service/LoginFlowV2Service.php',
diff --git a/lib/composer/composer/autoload_static.php b/lib/composer/composer/autoload_static.php
index 5c0535b4362..61df306bda3 100644
--- a/lib/composer/composer/autoload_static.php
+++ b/lib/composer/composer/autoload_static.php
@@ -1359,6 +1359,7 @@ class ComposerStaticInit749170dad3f5e7f9ca158f5a9f04f6a2
'OC\\Core\\Migrations\\Version29000Date20240124132202' => __DIR__ . '/../../..' . '/core/Migrations/Version29000Date20240124132202.php',
'OC\\Core\\Migrations\\Version29000Date20240131122720' => __DIR__ . '/../../..' . '/core/Migrations/Version29000Date20240131122720.php',
'OC\\Core\\Migrations\\Version30000Date20240429122720' => __DIR__ . '/../../..' . '/core/Migrations/Version30000Date20240429122720.php',
+ 'OC\\Core\\Migrations\\Version30000Date20240717111406' => __DIR__ . '/../../..' . '/core/Migrations/Version30000Date20240717111406.php',
'OC\\Core\\Notification\\CoreNotifier' => __DIR__ . '/../../..' . '/core/Notification/CoreNotifier.php',
'OC\\Core\\ResponseDefinitions' => __DIR__ . '/../../..' . '/core/ResponseDefinitions.php',
'OC\\Core\\Service\\LoginFlowV2Service' => __DIR__ . '/../../..' . '/core/Service/LoginFlowV2Service.php',
diff --git a/lib/private/TaskProcessing/Db/Task.php b/lib/private/TaskProcessing/Db/Task.php
index 1ac40327899..9fc999faf1a 100644
--- a/lib/private/TaskProcessing/Db/Task.php
+++ b/lib/private/TaskProcessing/Db/Task.php
@@ -35,6 +35,10 @@ use OCP\TaskProcessing\Task as OCPTask;
* @method null|string getErrorMessage()
* @method setProgress(null|float $progress)
* @method null|float getProgress()
+ * @method setWebhookUri(string $webhookUri)
+ * @method string getWebhookUri()
+ * @method setWebhookMethod(string $webhookMethod)
+ * @method string getWebhookMethod()
*/
class Task extends Entity {
protected $lastUpdated;
@@ -48,16 +52,18 @@ class Task extends Entity {
protected $completionExpectedAt;
protected $errorMessage;
protected $progress;
+ protected $webhookUri;
+ protected $webhookMethod;
/**
* @var string[]
*/
- public static array $columns = ['id', 'last_updated', 'type', 'input', 'output', 'status', 'user_id', 'app_id', 'custom_id', 'completion_expected_at', 'error_message', 'progress'];
+ public static array $columns = ['id', 'last_updated', 'type', 'input', 'output', 'status', 'user_id', 'app_id', 'custom_id', 'completion_expected_at', 'error_message', 'progress', 'webhook_uri', 'webhook_method'];
/**
* @var string[]
*/
- public static array $fields = ['id', 'lastUpdated', 'type', 'input', 'output', 'status', 'userId', 'appId', 'customId', 'completionExpectedAt', 'errorMessage', 'progress'];
+ public static array $fields = ['id', 'lastUpdated', 'type', 'input', 'output', 'status', 'userId', 'appId', 'customId', 'completionExpectedAt', 'errorMessage', 'progress', 'webhookUri', 'webhookMethod'];
public function __construct() {
@@ -74,6 +80,8 @@ class Task extends Entity {
$this->addType('completionExpectedAt', 'datetime');
$this->addType('errorMessage', 'string');
$this->addType('progress', 'float');
+ $this->addType('webhookUri', 'string');
+ $this->addType('webhookMethod', 'string');
}
public function toRow(): array {
@@ -97,6 +105,8 @@ class Task extends Entity {
'customId' => $task->getCustomId(),
'completionExpectedAt' => $task->getCompletionExpectedAt(),
'progress' => $task->getProgress(),
+ 'webhookUri' => $task->getWebhookUri(),
+ 'webhookMethod' => $task->getWebhookMethod(),
]);
return $taskEntity;
}
@@ -114,6 +124,8 @@ class Task extends Entity {
$task->setCompletionExpectedAt($this->getCompletionExpectedAt());
$task->setErrorMessage($this->getErrorMessage());
$task->setProgress($this->getProgress());
+ $task->setWebhookUri($this->getWebhookUri());
+ $task->setWebhookMethod($this->getWebhookMethod());
return $task;
}
}
diff --git a/lib/private/TaskProcessing/Manager.php b/lib/private/TaskProcessing/Manager.php
index d5a09ff2472..714a23ce5e2 100644
--- a/lib/private/TaskProcessing/Manager.php
+++ b/lib/private/TaskProcessing/Manager.php
@@ -9,9 +9,12 @@ declare(strict_types=1);
namespace OC\TaskProcessing;
+use GuzzleHttp\Exception\ClientException;
+use GuzzleHttp\Exception\ServerException;
use OC\AppFramework\Bootstrap\Coordinator;
use OC\Files\SimpleFS\SimpleFile;
use OC\TaskProcessing\Db\TaskMapper;
+use OCP\App\IAppManager;
use OCP\AppFramework\Db\DoesNotExistException;
use OCP\AppFramework\Db\MultipleObjectsReturnedException;
use OCP\BackgroundJob\IJobList;
@@ -27,6 +30,7 @@ use OCP\Files\IRootFolder;
use OCP\Files\Node;
use OCP\Files\NotPermittedException;
use OCP\Files\SimpleFS\ISimpleFile;
+use OCP\Http\Client\IClientService;
use OCP\IConfig;
use OCP\IL10N;
use OCP\IServerContainer;
@@ -53,6 +57,8 @@ use OCP\TaskProcessing\TaskTypes\TextToText;
use OCP\TaskProcessing\TaskTypes\TextToTextHeadline;
use OCP\TaskProcessing\TaskTypes\TextToTextSummary;
use OCP\TaskProcessing\TaskTypes\TextToTextTopics;
+use Psr\Container\ContainerExceptionInterface;
+use Psr\Container\NotFoundExceptionInterface;
use Psr\Log\LoggerInterface;
class Manager implements IManager {
@@ -83,6 +89,8 @@ class Manager implements IManager {
private \OCP\TextToImage\IManager $textToImageManager,
private \OCP\SpeechToText\ISpeechToTextManager $speechToTextManager,
private IUserMountCache $userMountCache,
+ private IClientService $clientService,
+ private IAppManager $appManager,
) {
$this->appData = $appDataFactory->get('core');
}
@@ -651,6 +659,7 @@ class Manager implements IManager {
$taskEntity = \OC\TaskProcessing\Db\Task::fromPublicTask($task);
try {
$this->taskMapper->update($taskEntity);
+ $this->runWebhook($task);
} catch (\OCP\DB\Exception $e) {
throw new \OCP\TaskProcessing\Exception\Exception('There was a problem finding the task', 0, $e);
}
@@ -739,6 +748,7 @@ class Manager implements IManager {
$taskEntity = \OC\TaskProcessing\Db\Task::fromPublicTask($task);
try {
$this->taskMapper->update($taskEntity);
+ $this->runWebhook($task);
} catch (\OCP\DB\Exception $e) {
throw new \OCP\TaskProcessing\Exception\Exception('There was a problem finding the task', 0, $e);
}
@@ -975,7 +985,7 @@ class Manager implements IManager {
/**
* @param mixed $fileId
- * @param string $userId
+ * @param string|null $userId
* @return void
* @throws UnauthorizedException
*/
@@ -989,4 +999,71 @@ class Manager implements IManager {
throw new UnauthorizedException('User ' . $userId . ' does not have access to file ' . $fileId);
}
}
+
+ /**
+ * Make a request to the task's webhookUri if necessary
+ *
+ * @param Task $task
+ */
+ private function runWebhook(Task $task): void {
+ $uri = $task->getWebhookUri();
+ $method = $task->getWebhookMethod();
+
+ if (!$uri || !$method) {
+ return;
+ }
+
+ if (in_array($method, ['HTTP:GET', 'HTTP:POST', 'HTTP:PUT', 'HTTP:DELETE'], true)) {
+ $client = $this->clientService->newClient();
+ $httpMethod = preg_replace('/^HTTP:/', '', $method);
+ $options = [
+ 'timeout' => 30,
+ 'body' => json_encode([
+ 'task' => $task->jsonSerialize(),
+ ]),
+ 'headers' => ['Content-Type' => 'application/json'],
+ ];
+ try {
+ $client->request($httpMethod, $uri, $options);
+ } catch (ClientException | ServerException $e) {
+ $this->logger->warning('Task processing HTTP webhook failed for task ' . $task->getId() . '. Request failed', ['exception' => $e]);
+ } catch (\Exception | \Throwable $e) {
+ $this->logger->warning('Task processing HTTP webhook failed for task ' . $task->getId() . '. Unknown error', ['exception' => $e]);
+ }
+ } elseif (str_starts_with($method, 'AppAPI:') && str_starts_with($uri, '/')) {
+ $parsedMethod = explode(':', $method, 4);
+ if (count($parsedMethod) < 3) {
+ $this->logger->warning('Task processing AppAPI webhook failed for task ' . $task->getId() . '. Invalid method: ' . $method);
+ }
+ [, $exAppId, $httpMethod] = $parsedMethod;
+ if (!$this->appManager->isInstalled('app_api')) {
+ $this->logger->warning('Task processing AppAPI webhook failed for task ' . $task->getId() . '. AppAPI is disabled or not installed.');
+ return;
+ }
+ try {
+ $appApiFunctions = \OCP\Server::get(\OCA\AppAPI\PublicFunctions::class);
+ } catch (ContainerExceptionInterface|NotFoundExceptionInterface) {
+ $this->logger->warning('Task processing AppAPI webhook failed for task ' . $task->getId() . '. Could not get AppAPI public functions.');
+ return;
+ }
+ $exApp = $appApiFunctions->getExApp($exAppId);
+ if ($exApp === null) {
+ $this->logger->warning('Task processing AppAPI webhook failed for task ' . $task->getId() . '. ExApp ' . $exAppId . ' is missing.');
+ return;
+ } elseif (!$exApp['enabled']) {
+ $this->logger->warning('Task processing AppAPI webhook failed for task ' . $task->getId() . '. ExApp ' . $exAppId . ' is disabled.');
+ return;
+ }
+ $requestParams = [
+ 'task' => $task->jsonSerialize(),
+ ];
+ $requestOptions = [
+ 'timeout' => 30,
+ ];
+ $response = $appApiFunctions->exAppRequest($exAppId, $uri, $task->getUserId(), $httpMethod, $requestParams, $requestOptions);
+ if (is_array($response) && isset($response['error'])) {
+ $this->logger->warning('Task processing AppAPI webhook failed for task ' . $task->getId() . '. Error during request to ExApp(' . $exAppId . '): ', $response['error']);
+ }
+ }
+ }
}
diff --git a/lib/public/TaskProcessing/Task.php b/lib/public/TaskProcessing/Task.php
index 2ec367e4a0a..44834c3b846 100644
--- a/lib/public/TaskProcessing/Task.php
+++ b/lib/public/TaskProcessing/Task.php
@@ -30,6 +30,9 @@ final class Task implements \JsonSerializable {
protected int $lastUpdated;
+ protected ?string $webhookUri = null;
+ protected ?string $webhookMethod = null;
+
/**
* @since 30.0.0
*/
@@ -265,6 +268,40 @@ final class Task implements \JsonSerializable {
}
/**
+ * @return null|string
+ * @since 30.0.0
+ */
+ final public function getWebhookUri(): ?string {
+ return $this->webhookUri;
+ }
+
+ /**
+ * @param string|null $webhookUri
+ * @return void
+ * @since 30.0.0
+ */
+ final public function setWebhookUri(?string $webhookUri): void {
+ $this->webhookUri = $webhookUri;
+ }
+
+ /**
+ * @return null|string
+ * @since 30.0.0
+ */
+ final public function getWebhookMethod(): ?string {
+ return $this->webhookMethod;
+ }
+
+ /**
+ * @param string|null $webhookMethod
+ * @return void
+ * @since 30.0.0
+ */
+ final public function setWebhookMethod(?string $webhookMethod): void {
+ $this->webhookMethod = $webhookMethod;
+ }
+
+ /**
* @param int $status
* @return 'STATUS_CANCELLED'|'STATUS_FAILED'|'STATUS_SUCCESSFUL'|'STATUS_RUNNING'|'STATUS_SCHEDULED'|'STATUS_UNKNOWN'
* @since 30.0.0
diff --git a/tests/lib/TaskProcessing/TaskProcessingTest.php b/tests/lib/TaskProcessing/TaskProcessingTest.php
index 27f46bed17c..c88f73a861c 100644
--- a/tests/lib/TaskProcessing/TaskProcessingTest.php
+++ b/tests/lib/TaskProcessing/TaskProcessingTest.php
@@ -12,6 +12,7 @@ use OC\EventDispatcher\EventDispatcher;
use OC\TaskProcessing\Db\TaskMapper;
use OC\TaskProcessing\Manager;
use OC\TaskProcessing\RemoveOldTasksBackgroundJob;
+use OCP\App\IAppManager;
use OCP\AppFramework\Utility\ITimeFactory;
use OCP\BackgroundJob\IJobList;
use OCP\EventDispatcher\IEventDispatcher;
@@ -19,6 +20,7 @@ use OCP\Files\AppData\IAppDataFactory;
use OCP\Files\Config\ICachedMountInfo;
use OCP\Files\Config\IUserMountCache;
use OCP\Files\IRootFolder;
+use OCP\Http\Client\IClientService;
use OCP\IConfig;
use OCP\IDBConnection;
use OCP\IServerContainer;
@@ -387,6 +389,8 @@ class TaskProcessingTest extends \Test\TestCase {
$text2imageManager,
\OC::$server->get(ISpeechToTextManager::class),
$this->userMountCache,
+ \OC::$server->get(IClientService::class),
+ \OC::$server->get(IAppManager::class),
);
}
diff --git a/version.php b/version.php
index 28e91a4702c..44fbf088d31 100644
--- a/version.php
+++ b/version.php
@@ -9,7 +9,7 @@
// between betas, final and RCs. This is _not_ the public version number. Reset minor/patch level
// when updating major/minor version number.
-$OC_Version = [30, 0, 0, 1];
+$OC_Version = [30, 0, 0, 2];
// The human-readable string
$OC_VersionString = '30.0.0 dev';