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
1 change: 1 addition & 0 deletions appinfo/routes.php
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
['name' => 'board#transferOwner', 'url' => '/boards/{boardId}/transferOwner', 'verb' => 'PUT'],
['name' => 'board#export', 'url' => '/boards/{boardId}/export', 'verb' => 'GET'],
['name' => 'board#import', 'url' => '/boards/import', 'verb' => 'POST'],
['name' => 'board#importCsv', 'url' => '/boards/{boardId}/importCsv', 'verb' => 'POST'],

// stacks
['name' => 'stack#index', 'url' => '/stacks/{boardId}', 'verb' => 'GET'],
Expand Down
12 changes: 9 additions & 3 deletions cypress/e2e/boardFeatures.js
Original file line number Diff line number Diff line change
Expand Up @@ -266,19 +266,25 @@ describe('Board import', function() {
})

it('Imports a board from JSON', function() {
cy.get('#app-navigation-vue .app-navigation__list .app-navigation-entry:contains("Import board")')
.should('be.visible')
// Open settings dialog
cy.get('#app-navigation-vue').contains('Deck settings').click()

// Click "Import from device" in the Import section
cy.get('.app-settings-section').contains('button', 'Import from device')
.click()

// Upload a JSON file
cy.get('input[type="file"]')
cy.get('.app-settings-section input[type="file"]')
.selectFile([
{
contents: 'cypress/fixtures/import-board.json',
fileName: 'import-board.json',
},
], { force: true })

// Close settings dialog by pressing Escape
cy.get('body').type('{esc}')

cy.get('.app-navigation__list .app-navigation-entry:contains("Imported board")')
.should('be.visible')
})
Expand Down
12 changes: 12 additions & 0 deletions lib/Controller/AttachmentOcsController.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,9 @@
use OCA\Deck\NotImplementedException;
use OCA\Deck\Service\AttachmentService;
use OCA\Deck\Service\BoardService;
use OCP\AppFramework\Http\Attribute\CORS;
use OCP\AppFramework\Http\Attribute\NoAdminRequired;
use OCP\AppFramework\Http\Attribute\NoCSRFRequired;
use OCP\AppFramework\Http\DataResponse;
use OCP\AppFramework\OCSController;
use OCP\IRequest;
Expand All @@ -36,34 +38,44 @@ private function ensureLocalBoard(?int $boardId): void {
}

#[NoAdminRequired]
#[CORS]
#[NoCSRFRequired]
public function getAll(int $cardId, ?int $boardId = null): DataResponse {
$this->ensureLocalBoard($boardId);
$attachment = $this->attachmentService->findAll($cardId, true);
return new DataResponse($attachment);
}

#[NoAdminRequired]
#[CORS]
#[NoCSRFRequired]
public function create(int $cardId, string $type, string $data = '', ?int $boardId = null): DataResponse {
$this->ensureLocalBoard($boardId);
$attachment = $this->attachmentService->create($cardId, $type, $data);
return new DataResponse($attachment);
}

#[NoAdminRequired]
#[CORS]
#[NoCSRFRequired]
public function update(int $cardId, int $attachmentId, string $data, string $type = 'file', ?int $boardId = null): DataResponse {
$this->ensureLocalBoard($boardId);
$attachment = $this->attachmentService->update($cardId, $attachmentId, $data, $type);
return new DataResponse($attachment);
}

#[NoAdminRequired]
#[CORS]
#[NoCSRFRequired]
public function delete(int $cardId, int $attachmentId, string $type = 'file', ?int $boardId = null): DataResponse {
$this->ensureLocalBoard($boardId);
$attachment = $this->attachmentService->delete($cardId, $attachmentId, $type);
return new DataResponse($attachment);
}

#[NoAdminRequired]
#[CORS]
#[NoCSRFRequired]
public function restore(int $cardId, int $attachmentId, string $type = 'file', ?int $boardId = null): DataResponse {
$this->ensureLocalBoard($boardId);
$attachment = $this->attachmentService->restore($cardId, $attachmentId, $type);
Expand Down
107 changes: 98 additions & 9 deletions lib/Controller/BoardController.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
use OCA\Deck\Db\Board;
use OCA\Deck\NoPermissionException;
use OCA\Deck\Service\BoardService;
use OCA\Deck\Service\CsvImportService;
use OCA\Deck\Service\ExternalBoardService;
use OCA\Deck\Service\Importer\BoardImportService;
use OCA\Deck\Service\PermissionService;
Expand All @@ -29,6 +30,7 @@ public function __construct(
private ExternalBoardService $externalBoardService,
private PermissionService $permissionService,
private BoardImportService $boardImportService,
private CsvImportService $csvImportService,
private IL10N $l10n,
private $userId,
) {
Expand Down Expand Up @@ -168,8 +170,9 @@ public function import(): DataResponse {
if (!empty($file) && array_key_exists('error', $file) && $file['error'] !== UPLOAD_ERR_OK) {
$error = $phpFileUploadErrors[$file['error']];
}
if (!empty($file) && $file['error'] === UPLOAD_ERR_OK && !in_array($file['type'], ['application/json', 'text/plain'], true)) {
$error = $this->l10n->t('Invalid file type. Only JSON files are allowed.');
$isCsv = $this->isCsvFile($file);
if (!empty($file) && $file['error'] === UPLOAD_ERR_OK && !$isCsv && !in_array($file['type'], ['application/json', 'text/plain'], true)) {
$error = $this->l10n->t('Invalid file type. Only JSON and CSV files are allowed.');
}
if ($error !== null) {
return new DataResponse([
Expand All @@ -180,20 +183,45 @@ public function import(): DataResponse {

try {
$fileContent = file_get_contents($file['tmp_name']);
$this->boardImportService->setSystem('DeckJson');
$config = new \stdClass();
$config->owner = $this->userId;
$this->boardImportService->setConfigInstance($config);
$this->boardImportService->setData(json_decode($fileContent));

if ($isCsv) {
$boardTitle = pathinfo($file['name'] ?? 'Imported Board', PATHINFO_FILENAME) ?: 'Imported Board';
$this->boardImportService->setSystem('DeckCsv');
$config = new \stdClass();
$config->owner = $this->userId;
$config->boardTitle = $boardTitle;
$this->boardImportService->setConfigInstance($config);
$data = new \stdClass();
$data->rawCsvContent = $fileContent;
$data->title = $boardTitle;
$this->boardImportService->setData($data);
} else {
$this->boardImportService->setSystem('DeckJson');
$config = new \stdClass();
$config->owner = $this->userId;
$this->boardImportService->setConfigInstance($config);
$this->boardImportService->setData(json_decode($fileContent));
}

$importErrors = [];
$this->boardImportService->registerErrorCollector(function (string $message) use (&$importErrors) {
$importErrors[] = $message;
});

$this->boardImportService->import();
$importedBoard = $this->boardImportService->getBoard();
$board = $this->boardService->find($importedBoard->getId());

return new DataResponse($board, Http::STATUS_OK);
return new DataResponse([
'board' => $board,
'import' => [
'errors' => $importErrors,
],
], Http::STATUS_OK);
} catch (\TypeError $e) {
return new DataResponse([
'status' => 'error',
'message' => $this->l10n->t('Invalid JSON data'),
'message' => $this->l10n->t('Invalid import data'),
], Http::STATUS_BAD_REQUEST);
} catch (\Exception $e) {
return new DataResponse([
Expand All @@ -202,4 +230,65 @@ public function import(): DataResponse {
], Http::STATUS_BAD_REQUEST);
}
}

/**
* @NoAdminRequired
*/
public function importCsv(int $boardId): DataResponse {
$file = $this->request->getUploadedFile('file');
$error = null;
$phpFileUploadErrors = [
UPLOAD_ERR_OK => $this->l10n->t('The file was uploaded'),
UPLOAD_ERR_INI_SIZE => $this->l10n->t('The uploaded file exceeds the upload_max_filesize directive in php.ini'),
UPLOAD_ERR_FORM_SIZE => $this->l10n->t('The uploaded file exceeds the MAX_FILE_SIZE directive that was specified in the HTML form'),
UPLOAD_ERR_PARTIAL => $this->l10n->t('The file was only partially uploaded'),
UPLOAD_ERR_NO_FILE => $this->l10n->t('No file was uploaded'),
UPLOAD_ERR_NO_TMP_DIR => $this->l10n->t('Missing a temporary folder'),
UPLOAD_ERR_CANT_WRITE => $this->l10n->t('Could not write file to disk'),
UPLOAD_ERR_EXTENSION => $this->l10n->t('A PHP extension stopped the file upload'),
];

if (empty($file)) {
$error = $this->l10n->t('No file uploaded or file size exceeds maximum of %s', [\OCP\Util::humanFileSize(\OCP\Util::uploadLimit())]);
}
if (!empty($file) && array_key_exists('error', $file) && $file['error'] !== UPLOAD_ERR_OK) {
$error = $phpFileUploadErrors[$file['error']];
}
if (!empty($file) && $file['error'] === UPLOAD_ERR_OK && !$this->isCsvFile($file)) {
$error = $this->l10n->t('Invalid file type. Only CSV files are allowed.');
}
if ($error !== null) {
return new DataResponse([
'status' => 'error',
'message' => $error,
], Http::STATUS_BAD_REQUEST);
}

try {
$fileContent = file_get_contents($file['tmp_name']);
$importResult = $this->csvImportService->importToBoard($boardId, $fileContent, $this->userId);
$board = $this->boardService->find($boardId);

return new DataResponse([
'board' => $board,
'import' => $importResult,
], Http::STATUS_OK);
} catch (\Exception $e) {
return new DataResponse([
'status' => 'error',
'message' => $this->l10n->t('Failed to import cards from CSV'),
], Http::STATUS_BAD_REQUEST);
}
}

private function isCsvFile(?array $file): bool {
if (empty($file)) {
return false;
}
if (in_array($file['type'] ?? '', ['text/csv', 'application/csv'], true)) {
return true;
}
$name = $file['name'] ?? '';
return str_ends_with(strtolower($name), '.csv');
}
}
7 changes: 7 additions & 0 deletions lib/Controller/BoardOcsController.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,9 @@
use OCA\Deck\Service\BoardService;
use OCA\Deck\Service\ExternalBoardService;
use OCP\AppFramework\Http\Attribute\NoAdminRequired;
use OCP\AppFramework\Http\Attribute\NoCSRFRequired;
use OCP\AppFramework\Http\Attribute\PublicPage;
use OCP\AppFramework\Http\Attribute\RequestHeader;
use OCP\AppFramework\Http\DataResponse;
use OCP\AppFramework\OCSController;
use OCP\IRequest;
Expand All @@ -36,6 +38,8 @@ public function index(): DataResponse {

#[NoAdminRequired]
#[PublicPage]
#[NoCSRFRequired]
#[RequestHeader(name: 'x-nextcloud-federation', description: 'Set to 1 when the request is performed by another Nextcloud Server to indicate a federation request', indirect: true)]
public function read(int $boardId): DataResponse {
$localBoard = $this->boardService->find($boardId, true, true);
if ($localBoard->getExternalId() !== null) {
Expand All @@ -45,16 +49,19 @@ public function read(int $boardId): DataResponse {
}

#[NoAdminRequired]
#[NoCSRFRequired]
public function create(string $title, string $color): DataResponse {
return new DataResponse($this->boardService->create($title, $this->userId, $color));
}

#[NoAdminRequired]
#[NoCSRFRequired]
public function addAcl(int $boardId, int $type, string $participant, bool $permissionEdit, bool $permissionShare, bool $permissionManage, ?string $remote = null): DataResponse {
return new DataResponse($this->boardService->addAcl($boardId, $type, $participant, $permissionEdit, $permissionShare, $permissionManage));
}

#[NoAdminRequired]
#[NoCSRFRequired]
public function updateAcl(int $id, bool $permissionEdit, bool $permissionShare, bool $permissionManage): DataResponse {
return new DataResponse($this->boardService->updateAcl($id, $permissionEdit, $permissionShare, $permissionManage));
}
Expand Down
11 changes: 11 additions & 0 deletions lib/Controller/CardOcsController.php
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,9 @@
use OCA\Deck\Service\ExternalBoardService;
use OCA\Deck\Service\StackService;
use OCP\AppFramework\Http\Attribute\NoAdminRequired;
use OCP\AppFramework\Http\Attribute\NoCSRFRequired;
use OCP\AppFramework\Http\Attribute\PublicPage;
use OCP\AppFramework\Http\Attribute\RequestHeader;
use OCP\AppFramework\Http\DataResponse;
use OCP\AppFramework\OCSController;
use OCP\IRequest;
Expand All @@ -35,6 +37,8 @@ public function __construct(

#[NoAdminRequired]
#[PublicPage]
#[NoCSRFRequired]
#[RequestHeader(name: 'x-nextcloud-federation', description: 'Set to 1 when the request is performed by another Nextcloud Server to indicate a federation request', indirect: true)]
public function create(string $title, int $stackId, ?int $boardId = null, ?string $type = 'plain', ?string $owner = null, ?int $order = 999, ?string $description = '', $duedate = null, $startdate = null, ?array $labels = [], ?array $users = []) {
if ($boardId) {
$board = $this->boardService->find($boardId, false);
Expand Down Expand Up @@ -63,6 +67,7 @@ public function create(string $title, int $stackId, ?int $boardId = null, ?strin

#[NoAdminRequired]
#[PublicPage]
#[NoCSRFRequired]
public function assignLabel(?int $boardId, int $cardId, int $labelId): DataResponse {
if ($boardId) {
$board = $this->boardService->find($boardId, false);
Expand All @@ -76,6 +81,7 @@ public function assignLabel(?int $boardId, int $cardId, int $labelId): DataRespo

#[NoAdminRequired]
#[PublicPage]
#[NoCSRFRequired]
public function assignUser(?int $boardId, int $cardId, string $userId, int $type = 0): DataResponse {
if ($boardId) {
$localBoard = $this->boardService->find($boardId, false);
Expand All @@ -88,6 +94,7 @@ public function assignUser(?int $boardId, int $cardId, string $userId, int $type

#[NoAdminRequired]
#[PublicPage]
#[NoCSRFRequired]
public function unAssignUser(?int $boardId, int $cardId, string $userId, int $type = 0): DataResponse {
if ($boardId) {
$localBoard = $this->boardService->find($boardId, false);
Expand All @@ -100,6 +107,7 @@ public function unAssignUser(?int $boardId, int $cardId, string $userId, int $ty

#[NoAdminRequired]
#[PublicPage]
#[NoCSRFRequired]
public function removeLabel(?int $boardId, int $cardId, int $labelId): DataResponse {
if ($boardId) {
$board = $this->boardService->find($boardId, false);
Expand All @@ -113,6 +121,8 @@ public function removeLabel(?int $boardId, int $cardId, int $labelId): DataRespo

#[NoAdminRequired]
#[PublicPage]
#[NoCSRFRequired]
#[RequestHeader(name: 'x-nextcloud-federation', description: 'Set to 1 when the request is performed by another Nextcloud Server to indicate a federation request', indirect: true)]
public function update(int $id, string $title, int $stackId, string $type, int $order, string $description, $duedate, $deletedAt, int $boardId, array|string|null $owner = null, $archived = null, $startdate = null): DataResponse {
$done = array_key_exists('done', $this->request->getParams())
? new OptionalNullableValue($this->request->getParam('done', null))
Expand Down Expand Up @@ -160,6 +170,7 @@ public function update(int $id, string $title, int $stackId, string $type, int $

#[NoAdminRequired]
#[PublicPage]
#[NoCSRFRequired]
public function reorder(int $cardId, int $stackId, int $order, ?int $boardId): DataResponse {
if ($boardId) {
$board = $this->boardService->find($boardId, false);
Expand Down
Loading
Loading