Skip to content
Open
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
9 changes: 6 additions & 3 deletions core/Controller/TaskProcessingApiController.php
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@
use OCP\TaskProcessing\Exception\PreConditionNotMetException;
use OCP\TaskProcessing\Exception\UnauthorizedException;
use OCP\TaskProcessing\Exception\ValidationException;
use OCP\TaskProcessing\FileShaped;
use OCP\TaskProcessing\IManager;
use OCP\TaskProcessing\ShapeEnumValue;
use OCP\TaskProcessing\Task;
Expand Down Expand Up @@ -534,7 +535,8 @@ public function setFileContentsExApp(int $taskId): DataResponse {
if (!$handle) {
return new DataResponse(['message' => $this->l->t('Internal error')], Http::STATUS_INTERNAL_SERVER_ERROR);
}
$fileId = $this->setFileContentsInternal($handle);
$ext = pathinfo($file['name'], PATHINFO_EXTENSION);
Comment thread
marcelklehr marked this conversation as resolved.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Suggested change
$ext = pathinfo($file['name'], PATHINFO_EXTENSION);
$ext = pathinfo(($file['name'] ?? ''), PATHINFO_EXTENSION);

$fileId = $this->setFileContentsInternal($handle, $ext);
return new DataResponse(['fileId' => $fileId], Http::STATUS_CREATED);
} catch (NotFoundException) {
return new DataResponse(['message' => $this->l->t('Not found')], Http::STATUS_NOT_FOUND);
Expand Down Expand Up @@ -854,14 +856,15 @@ public function getNextScheduledTaskBatch(array $providerIds, array $taskTypeIds
* @return int
* @throws NotPermittedException
*/
private function setFileContentsInternal($data): int {
private function setFileContentsInternal($data, string $ext = ''): int {
try {
$folder = $this->appData->getFolder('TaskProcessing');
} catch (\OCP\Files\NotFoundException) {
$folder = $this->appData->newFolder('TaskProcessing');
}
$ext = FileShaped::sanitizeExtension($ext);
/** @var SimpleFile $file */
$file = $folder->newFile(time() . '-' . rand(1, 100000), $data);
$file = $folder->newFile(time() . '-' . rand(1, 100000) . ($ext ? '.' . $ext : ''), $data);
return $file->getId();
Comment thread
marcelklehr marked this conversation as resolved.
}

Expand Down
1 change: 1 addition & 0 deletions lib/composer/composer/autoload_classmap.php
Original file line number Diff line number Diff line change
Expand Up @@ -928,6 +928,7 @@
'OCP\\TaskProcessing\\Exception\\UnauthorizedException' => $baseDir . '/lib/public/TaskProcessing/Exception/UnauthorizedException.php',
'OCP\\TaskProcessing\\Exception\\UserFacingProcessingException' => $baseDir . '/lib/public/TaskProcessing/Exception/UserFacingProcessingException.php',
'OCP\\TaskProcessing\\Exception\\ValidationException' => $baseDir . '/lib/public/TaskProcessing/Exception/ValidationException.php',
'OCP\\TaskProcessing\\FileShaped' => $baseDir . '/lib/public/TaskProcessing/FileShaped.php',
'OCP\\TaskProcessing\\IInternalTaskType' => $baseDir . '/lib/public/TaskProcessing/IInternalTaskType.php',
'OCP\\TaskProcessing\\IManager' => $baseDir . '/lib/public/TaskProcessing/IManager.php',
'OCP\\TaskProcessing\\IProvider' => $baseDir . '/lib/public/TaskProcessing/IProvider.php',
Expand Down
1 change: 1 addition & 0 deletions lib/composer/composer/autoload_static.php
Original file line number Diff line number Diff line change
Expand Up @@ -969,6 +969,7 @@ class ComposerStaticInit749170dad3f5e7f9ca158f5a9f04f6a2
'OCP\\TaskProcessing\\Exception\\UnauthorizedException' => __DIR__ . '/../../..' . '/lib/public/TaskProcessing/Exception/UnauthorizedException.php',
'OCP\\TaskProcessing\\Exception\\UserFacingProcessingException' => __DIR__ . '/../../..' . '/lib/public/TaskProcessing/Exception/UserFacingProcessingException.php',
'OCP\\TaskProcessing\\Exception\\ValidationException' => __DIR__ . '/../../..' . '/lib/public/TaskProcessing/Exception/ValidationException.php',
'OCP\\TaskProcessing\\FileShaped' => __DIR__ . '/../../..' . '/lib/public/TaskProcessing/FileShaped.php',
'OCP\\TaskProcessing\\IInternalTaskType' => __DIR__ . '/../../..' . '/lib/public/TaskProcessing/IInternalTaskType.php',
'OCP\\TaskProcessing\\IManager' => __DIR__ . '/../../..' . '/lib/public/TaskProcessing/IManager.php',
'OCP\\TaskProcessing\\IProvider' => __DIR__ . '/../../..' . '/lib/public/TaskProcessing/IProvider.php',
Expand Down
23 changes: 19 additions & 4 deletions lib/private/TaskProcessing/Manager.php
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@
use OCP\TaskProcessing\Exception\UnauthorizedException;
use OCP\TaskProcessing\Exception\UserFacingProcessingException;
use OCP\TaskProcessing\Exception\ValidationException;
use OCP\TaskProcessing\FileShaped;
use OCP\TaskProcessing\IInternalTaskType;
use OCP\TaskProcessing\IManager;
use OCP\TaskProcessing\IProvider;
Expand Down Expand Up @@ -1603,14 +1604,28 @@ public function encapsulateOutputFileData(array $output, ...$specs): array {
continue;
}
if (EShapeType::getScalarType($type) === $type) {
if ($output[$key] instanceof FileShaped) {
$data = $output[$key]->getData();
$ext = FileShaped::sanitizeExtension($output[$key]->getExtension());
} else {
$data = $output[$key];
$ext = '';
}
/** @var SimpleFile $file */
$file = $folder->newFile(time() . '-' . rand(1, 100000), $output[$key]);
$file = $folder->newFile(time() . '-' . rand(1, 100000) . ($ext ? '.' . $ext : ''), $data);
Comment thread
marcelklehr marked this conversation as resolved.
$newOutput[$key] = $file->getId(); // polymorphic call to SimpleFile
Comment thread
marcelklehr marked this conversation as resolved.
} else {
$newOutput = [];
$newOutput[$key] = [];
foreach ($output[$key] as $item) {
if ($item instanceof FileShaped) {
$data = $item->getData();
$ext = FileShaped::sanitizeExtension($item->getExtension());
} else {
$data = $item;
$ext = '';
}
/** @var SimpleFile $file */
$file = $folder->newFile(time() . '-' . rand(1, 100000), $item);
$file = $folder->newFile(time() . '-' . rand(1, 100000) . ($ext ? '.' . $ext : ''), $data);
$newOutput[$key][] = $file->getId();
}
}
Expand Down Expand Up @@ -1757,7 +1772,7 @@ private function validateOutputFileIds(array $output, ...$specs): array {
$newOutput[$key] = $this->validateFileId($output[$key]);
} else {
// Is list of file IDs
$newOutput = [];
$newOutput[$key] = [];
foreach ($output[$key] as $item) {
$newOutput[$key][] = $this->validateFileId($item);
}
Expand Down
20 changes: 11 additions & 9 deletions lib/public/TaskProcessing/EShapeType.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,15 @@

namespace OCP\TaskProcessing;

use OCP\AppFramework\Attribute\Consumable;
use OCP\TaskProcessing\Exception\ValidationException;

/**
* The input and output Shape types
*
* @since 30.0.0
*/
#[Consumable(since: '30.0.0')]
enum EShapeType: int {
/**
* @since 30.0.0
Expand Down Expand Up @@ -156,28 +158,28 @@ public function validateInput(mixed $value): void {
*/
public function validateOutputWithFileData(mixed $value): void {
$this->validateNonFileType($value);
if ($this === EShapeType::Image && !is_string($value)) {
if ($this === EShapeType::Image && !is_string($value) && !($value instanceof FileShaped && $value->getShapeType() === EShapeType::Image)) {
throw new ValidationException('Non-image item provided for Image slot');
}
if ($this === EShapeType::ListOfImages && (!is_array($value) || count(array_filter($value, fn ($item) => !is_string($item))) > 0)) {
if ($this === EShapeType::ListOfImages && (!is_array($value) || count(array_filter($value, fn ($item) => !is_string($item) && !($item instanceof FileShaped && $item->getShapeType() === EShapeType::Image))) > 0)) {
Comment on lines -159 to +164

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

maybe it would be nice to exact these checks in functions with separate ifs for different checks so they are easier to parse.
they could even be reused for the array checks then.

something along these lines but feel free to do it your way:

	/**
	 * Check whether a single value is valid file output data for the given shape type.
	 * Accepts raw string data or a FileShaped wrapper whose shape type matches.
	 */
	private static function isValidFileData(mixed $value, EShapeType $expected): bool {
		if (is_string($value)) {
			return true;
		}
		return $value instanceof FileShaped && $value->getShapeType() === $expected;
	}

	/**
	 * Check whether a value is a valid list of file output data items.
	 */
	private static function isValidFileDataList(mixed $value, EShapeType $scalarType): bool {
		if (!is_array($value)) {
			return false;
		}
		foreach ($value as $item) {
			if (!self::isValidFileData($item, $scalarType)) {
				return false;
			}
		}
		return true;
	}

and can be used like:

		if ($this === EShapeType::Image && !self::isValidFileData($value, EShapeType::Image)) {
			throw new ValidationException('Non-image item provided for Image slot');
		}

throw new ValidationException('Non-image list item provided for ListOfImages slot');
}
if ($this === EShapeType::Audio && !is_string($value)) {
if ($this === EShapeType::Audio && !is_string($value) && !($value instanceof FileShaped && $value->getShapeType() === EShapeType::Audio)) {
throw new ValidationException('Non-audio item provided for Audio slot');
}
if ($this === EShapeType::ListOfAudios && (!is_array($value) || count(array_filter($value, fn ($item) => !is_string($item))) > 0)) {
if ($this === EShapeType::ListOfAudios && (!is_array($value) || count(array_filter($value, fn ($item) => !is_string($item) && !($item instanceof FileShaped && $item->getShapeType() === EShapeType::Audio))) > 0)) {
throw new ValidationException('Non-audio list item provided for ListOfAudio slot');
}
if ($this === EShapeType::Video && !is_string($value)) {
if ($this === EShapeType::Video && !is_string($value) && !($value instanceof FileShaped && $value->getShapeType() === EShapeType::Video)) {
throw new ValidationException('Non-video item provided for Video slot');
}
if ($this === EShapeType::ListOfVideos && (!is_array($value) || count(array_filter($value, fn ($item) => !is_string($item))) > 0)) {
throw new ValidationException('Non-video list item provided for ListOfTexts slot');
if ($this === EShapeType::ListOfVideos && (!is_array($value) || count(array_filter($value, fn ($item) => !is_string($item) && !($item instanceof FileShaped && $item->getShapeType() === EShapeType::Video))) > 0)) {
throw new ValidationException('Non-video list item provided for ListOfVideos slot');
}
if ($this === EShapeType::File && !is_string($value)) {
if ($this === EShapeType::File && !is_string($value) && !($value instanceof FileShaped && $value->getShapeType() === EShapeType::File)) {
throw new ValidationException('Non-file item provided for File slot');
}
if ($this === EShapeType::ListOfFiles && (!is_array($value) || count(array_filter($value, fn ($item) => !is_string($item))) > 0)) {
if ($this === EShapeType::ListOfFiles && (!is_array($value) || count(array_filter($value, fn ($item) => !is_string($item) && !($item instanceof FileShaped && $item->getShapeType() === EShapeType::File))) > 0)) {
throw new ValidationException('Non-audio list item provided for ListOfFiles slot');

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Suggested change
throw new ValidationException('Non-audio list item provided for ListOfFiles slot');
throw new ValidationException('Non-file list item provided for ListOfFiles slot');

}
}
Expand Down
69 changes: 69 additions & 0 deletions lib/public/TaskProcessing/FileShaped.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
<?php

/**
* SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/

namespace OCP\TaskProcessing;

use OCP\AppFramework\Attribute\Consumable;

/**
* Data object for file-shaped output entries
*
* @since 35.0.0
*/
#[Consumable(since: '35.0.0')]
Comment thread
marcelklehr marked this conversation as resolved.
final class FileShaped {
/**
* @param EShapeType $shapeType
* @param string $data
* @param string $extension (optional)
*
* @since 35.0.0
*/
public function __construct(
private EShapeType $shapeType,
private string $data,
private string $extension = '',
) {
$this->extension = self::sanitizeExtension($this->extension);
}

/**
* @return string
* @since 35.0.0
*/
public function getData(): string {
return $this->data;
}

/**
* @since 35.0.0
*/
public function getShapeType(): EShapeType {
return $this->shapeType;
}

/**
* @since 35.0.0
*/
public function getExtension(): string {
return $this->extension;
}

/**
* @since 35.0.0
*/
public static function sanitizeExtension(string $ext): string {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

the extension provider is within the trust boundary of ex-apps and php apps but it may not hurt to reject .php, .phar and .htaccess extensions

if ($ext === '') {
return '';
}
$ext = str_replace(['.', '/'], '', $ext);
Comment thread
marcelklehr marked this conversation as resolved.
$ext = preg_replace('/[^A-Za-z0-9]/', '', $ext) ?? $ext;
$ext = strtolower($ext);
$ext = substr($ext, 0, 16);
return $ext;
}
}
4 changes: 4 additions & 0 deletions lib/public/TaskProcessing/IInternalTaskType.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,15 @@

namespace OCP\TaskProcessing;

use OCP\AppFramework\Attribute\Implementable;

/**
* This is a task type interface that is implemented by task processing
* task types that should not show up in the assistant UI
*
* @since 33.0.0
*/
#[Implementable(since: '33.0.0')]
interface IInternalTaskType extends ITaskType {

}
4 changes: 4 additions & 0 deletions lib/public/TaskProcessing/IProvider.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,15 @@

namespace OCP\TaskProcessing;

use OCP\AppFramework\Attribute\Implementable;

/**
* This is the interface that is implemented by apps that
* implement a task processing provider
*
* @since 30.0.0
*/
#[Implementable(since: '30.0.0')]
interface IProvider {
/**
* The unique id of this provider
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@

namespace OCP\TaskProcessing;

use OCP\AppFramework\Attribute\Implementable;
use OCP\Files\File;
use OCP\TaskProcessing\Exception\ProcessingException;

Expand All @@ -17,6 +18,7 @@
* implement a task processing provider
* @since 35.0.0
*/
#[Implementable(since: '35.0.0')]
interface ISynchronousOptionsAwareProvider extends ISynchronousProvider {

/**
Expand All @@ -26,7 +28,7 @@ interface ISynchronousOptionsAwareProvider extends ISynchronousProvider {
* @param array<string, list<numeric|string|File>|numeric|string|File> $input The task input
* @param callable(float):bool $reportProgress Report the task progress. If this returns false, that means the task was cancelled and processing should be stopped.
* @param SynchronousProviderOptions $options The task options
* @psalm-return array<string, list<numeric|string>|numeric|string>
* @psalm-return array<string, list<numeric|string|FileShaped>|numeric|string|FileShaped>
* @throws ProcessingException
* @since 35.0.0
*/
Expand Down
4 changes: 3 additions & 1 deletion lib/public/TaskProcessing/ISynchronousProvider.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@

namespace OCP\TaskProcessing;

use OCP\AppFramework\Attribute\Implementable;
use OCP\Files\File;
use OCP\TaskProcessing\Exception\ProcessingException;

Expand All @@ -17,6 +18,7 @@
* implement a task processing provider
* @since 30.0.0
*/
#[Implementable(since: '30.0.0')]
interface ISynchronousProvider extends IProvider {

/**
Expand All @@ -25,7 +27,7 @@ interface ISynchronousProvider extends IProvider {
* @param null|string $userId The user that created the current task
* @param array<string, list<numeric|string|File>|numeric|string|File> $input The task input
* @param callable(float):bool $reportProgress Report the task progress. If this returns false, that means the task was cancelled and processing should be stopped.
* @psalm-return array<string, list<numeric|string>|numeric|string>
* @psalm-return array<string, list<numeric|string|FileShaped>|numeric|string|FileShaped>
Comment on lines -28 to +30

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

maybe it can be noted that FileShaped addition in the return type was made in 35.0.0
like in

* @since 31.0.0 Added the `showDisabled` argument.

* @throws ProcessingException
* @since 30.0.0
*/
Expand Down
4 changes: 4 additions & 0 deletions lib/public/TaskProcessing/ITaskType.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,15 @@

namespace OCP\TaskProcessing;

use OCP\AppFramework\Attribute\Implementable;

/**
* This is a task type interface that is implemented by task processing
* task types
*
* @since 30.0.0
*/
#[Implementable(since: '30.0.0')]
interface ITaskType {
/**
* Returns the unique id of this task type
Expand Down
4 changes: 4 additions & 0 deletions lib/public/TaskProcessing/ITriggerableProvider.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,15 @@

namespace OCP\TaskProcessing;

use OCP\AppFramework\Attribute\Implementable;

/**
* This is the interface that is implemented by apps that
* implement a task processing provider with support for being triggered
*
* @since 33.0.0
*/
#[Implementable(since: '33.0.0')]
interface ITriggerableProvider extends IProvider {

/**
Expand Down
4 changes: 4 additions & 0 deletions lib/public/TaskProcessing/ShapeDescriptor.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,14 @@

namespace OCP\TaskProcessing;

use OCP\AppFramework\Attribute\Consumable;

/**
* Data object for input output shape entries
*
* @since 30.0.0
*/
#[Consumable(since: '30.0.0')]
class ShapeDescriptor implements \JsonSerializable {
/**
* @param string $name
Expand Down
4 changes: 4 additions & 0 deletions lib/public/TaskProcessing/ShapeEnumValue.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,14 @@

namespace OCP\TaskProcessing;

use OCP\AppFramework\Attribute\Consumable;

/**
* Data object for input output shape enum slot value
*
* @since 30.0.0
*/
#[Consumable(since: '30.0.0')]
class ShapeEnumValue implements \JsonSerializable {
/**
* @param string $name
Expand Down
3 changes: 3 additions & 0 deletions lib/public/TaskProcessing/SynchronousProviderOptions.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,12 @@

namespace OCP\TaskProcessing;

use OCP\AppFramework\Attribute\Consumable;

/**
* @since 35.0.0
*/
#[Consumable(since: '35.0.0')]
class SynchronousProviderOptions {
private \Closure $reportIntermediateOutput;

Expand Down
Loading
Loading