Skip to content

Commit

Permalink
Add support for file question
Browse files Browse the repository at this point in the history
Signed-off-by: Konstantin Myakshin <[email protected]>
  • Loading branch information
Koc committed Apr 1, 2024
1 parent 949d2c7 commit 2410888
Show file tree
Hide file tree
Showing 10 changed files with 436 additions and 9 deletions.
8 changes: 8 additions & 0 deletions appinfo/routes.php
Original file line number Diff line number Diff line change
Expand Up @@ -331,6 +331,14 @@
'apiVersion' => 'v2(\.[1-4])?'
]
],
[
'name' => 'api#uploadFile',
'url' => '/api/{apiVersion}/uploadFile/{formId}',
'verb' => 'POST',
'requirements' => [
'apiVersion' => 'v2.5'
]
],
[
'name' => 'api#insertSubmission',
'url' => '/api/{apiVersion}/submission/insert',
Expand Down
4 changes: 3 additions & 1 deletion lib/Constants.php
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@ class Constants {
public const ANSWER_TYPE_DATE = 'date';
public const ANSWER_TYPE_DATETIME = 'datetime';
public const ANSWER_TYPE_TIME = 'time';
public const ANSWER_TYPE_FILE = 'file';

// All AnswerTypes
public const ANSWER_TYPES = [
Expand All @@ -83,7 +84,8 @@ class Constants {
self::ANSWER_TYPE_LONG,
self::ANSWER_TYPE_DATE,
self::ANSWER_TYPE_DATETIME,
self::ANSWER_TYPE_TIME
self::ANSWER_TYPE_TIME,
self::ANSWER_TYPE_FILE,
];

// AnswerTypes, that need/have predefined Options
Expand Down
107 changes: 104 additions & 3 deletions lib/Controller/ApiController.php
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@

namespace OCA\Forms\Controller;

use OC\Files\Filesystem;
use OCA\Forms\Constants;
use OCA\Forms\Db\Answer;
use OCA\Forms\Db\AnswerMapper;
Expand All @@ -40,18 +41,24 @@
use OCA\Forms\Db\ShareMapper;
use OCA\Forms\Db\Submission;
use OCA\Forms\Db\SubmissionMapper;
use OCA\Forms\Db\UploadedFile;
use OCA\Forms\Db\UploadedFileMapper;
use OCA\Forms\Service\ConfigService;
use OCA\Forms\Service\FormsService;
use OCA\Forms\Service\SubmissionService;

use OCP\AppFramework\Db\DoesNotExistException;
use OCP\AppFramework\Db\IMapperException;
use OCP\AppFramework\Http;
use OCP\AppFramework\Http\DataDownloadResponse;
use OCP\AppFramework\Http\DataResponse;
use OCP\AppFramework\Http\JSONResponse;
use OCP\AppFramework\Http\Response;
use OCP\AppFramework\OCS\OCSBadRequestException;
use OCP\AppFramework\OCS\OCSForbiddenException;
use OCP\AppFramework\OCS\OCSNotFoundException;
use OCP\AppFramework\OCSController;
use OCP\Files\IRootFolder;
use OCP\Files\NotFoundException;
use OCP\IL10N;
use OCP\IRequest;
Expand All @@ -60,6 +67,7 @@
use OCP\IUserSession;

use Psr\Log\LoggerInterface;
use Ramsey\Uuid\Uuid;

class ApiController extends OCSController {
/** @var IUser */
Expand All @@ -81,6 +89,8 @@ public function __construct(
private IL10N $l10n,
private LoggerInterface $logger,
private IUserManager $userManager,
private IRootFolder $storage,
private UploadedFileMapper $uploadedFileMapper,
) {
parent::__construct($appName, $request);
$this->currentUser = $userSession->getUser();
Expand Down Expand Up @@ -1002,9 +1012,24 @@ public function getSubmissions(string $hash): DataResponse {
*
* @param int $submissionId
* @param array $question
* @param array $answerArray [arrayOfString]
* @param string[]|array{uploadedFileId: string, uploadedFileName: string} $answerArray
*/
private function storeAnswersForQuestion($submissionId, array $question, array $answerArray) {
if ($question['type'] === Constants::ANSWER_TYPE_FILE) {
$answerEntity = new Answer();
$answerEntity->setSubmissionId($submissionId);
$answerEntity->setQuestionId($question['id']);
$answerEntity->setText($answerArray['uploadedFileName']);

$uploadedFile = $this->uploadedFileMapper->findByUploadedFileId($answerArray['uploadedFileId']);
$answerEntity->setFileId($uploadedFile->getFileId());

$this->answerMapper->insert($answerEntity);
$this->uploadedFileMapper->delete($uploadedFile);

return;
}

foreach ($answerArray as $answer) {
$answerText = '';

Expand Down Expand Up @@ -1033,6 +1058,83 @@ private function storeAnswersForQuestion($submissionId, array $question, array $
}
}


/**
* @CORS
* @PublicCORSFix
* @NoAdminRequired
* @PublicPage
*
* Uploads a temporary file to the server
*
* @return Response
*/
public function uploadFile(int $formId): Response {
// plan:
// +create migration to store temporary files in the table: id (hashed or uuid), form_id, original_file_name, file_id, created_at
// +create a new folder in form owner dir (formId), store file inside it with random file name
//
// +return back file_name and id
// during form submission store the file_id in the answer as separate column, and original file name in the text column

// during view check info about file id, and original file name, and display the file name with link
// during export do the same

$this->logger->debug('Uploading file for formId: {formId}', ['formId' => $formId]);

$uploadedFile = $this->request->getUploadedFile('file');

if (is_null($uploadedFile)) {
return new JSONResponse(
['data' => ['message' => $this->l10n->t('Invalid file provided')]],
Http::STATUS_BAD_REQUEST
);
}

$error = $uploadedFile['error'] ?? 0;
if ($error !== UPLOAD_ERR_OK) {
$this->logger->error('Failed to get the uploaded file. PHP file upload error code: ' . $error);

return new JSONResponse(
['data' => ['message' => $this->l10n->t('Failed to upload the file')]],
Http::STATUS_BAD_REQUEST
);
}

//fixme: add support of the multiple files?
//fixme: do we need add validation per file size and supported mime types?
if (is_uploaded_file($uploadedFile['tmp_name']) && !Filesystem::isFileBlacklisted($uploadedFile['name'])) {

Check failure on line 1106 in lib/Controller/ApiController.php

View workflow job for this annotation

GitHub Actions / Static analysis

UndefinedClass

lib/Controller/ApiController.php:1106:55: UndefinedClass: Class, interface or enum named OC\Files\Filesystem does not exist (see https://psalm.dev/019)
$form = $this->formMapper->findById($formId);
$userFolder = $this->storage->getUserFolder($form->getOwnerId());
$path = 'forms/form-' . $formId;

if ($userFolder->nodeExists($path)) {
$formFolder = $userFolder->get($path);
} else {
$formFolder = $userFolder->newFolder($path);
}

$uploadedFileId = Uuid::uuid4()->toString();
$extension = pathinfo($uploadedFile['name'], PATHINFO_EXTENSION);
$file = $formFolder->newFile($uploadedFileId.'.'.$extension, file_get_contents($uploadedFile['tmp_name']));

$uploadedFileEntity = new UploadedFile();
$uploadedFileEntity->setId($uploadedFileId);
$uploadedFileEntity->setFormId($formId);
$uploadedFileEntity->setOriginalFileName($uploadedFile['name']);
$uploadedFileEntity->setFileId($file->getId());
$uploadedFileEntity->setCreated(time());
$this->uploadedFileMapper->insert($uploadedFileEntity);

return new DataResponse(['uploadedFileId' => $uploadedFileId, 'uploadedFileName' => $uploadedFile['name']]);
}

return new JSONResponse(
['data' => ['message' => $this->l10n->t('Invalid file provided')]],
Http::STATUS_BAD_REQUEST
);
}

/**
* @CORS
* @NoAdminRequired
Expand Down Expand Up @@ -1063,7 +1165,7 @@ public function insertSubmission(int $formId, array $answers, string $shareHash
throw new OCSBadRequestException();
}

// Does the user have access to the form (Either by logged in user, or by providing public share-hash.)
// Does the user have access to the form (Either by logged-in user, or by providing public share-hash.)
try {
$isPublicShare = false;

Expand Down Expand Up @@ -1120,7 +1222,6 @@ public function insertSubmission(int $formId, array $answers, string $shareHash

// Insert new submission
$this->submissionMapper->insert($submission);
$submissionId = $submission->getId();

// Ensure the form is unique if needed.
// If we can not submit anymore then the submission must be unique
Expand Down
5 changes: 5 additions & 0 deletions lib/Db/Answer.php
Original file line number Diff line number Diff line change
Expand Up @@ -35,12 +35,15 @@
* @method void setSubmissionId(integer $value)
* @method integer getQuestionId()
* @method void setQuestionId(integer $value)
* @method integer|null getFileId()
* @method void setFileId(?integer $value)
* @method string getText()
* @method void setText(string $value)
*/
class Answer extends Entity {
protected $submissionId;
protected $questionId;
protected $fileId;
protected $text;

/**
Expand All @@ -49,12 +52,14 @@ class Answer extends Entity {
public function __construct() {
$this->addType('submissionId', 'integer');
$this->addType('questionId', 'integer');
$this->addType('fileId', 'integer');
}

public function read(): array {
return [
'id' => $this->getId(),
'submissionId' => $this->getSubmissionId(),
'fileId' => $this->getFileId(),
'questionId' => $this->getQuestionId(),
'text' => (string)$this->getText(),
];
Expand Down
69 changes: 69 additions & 0 deletions lib/Db/UploadedFile.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
<?php

declare(strict_types=1);

/**
* @copyright Copyright (c) 2024 Kostiantyn Miakshyn <[email protected]>
*
* @author Kostiantyn Miakshyn <[email protected]>
*
* @license AGPL-3.0-or-later
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/

namespace OCA\Forms\Db;

use OCP\AppFramework\Db\Entity;

/**
* @method string getId()
* @method void setId(string $value)
* @method integer getFormId()
* @method void setFormId(integer $value)
* @method string getOriginalFileName()
* @method void setOriginalFileName(string $value)
* @method integer getFileId()
* @method void setFileId(integer $value)
* @method integer getCreated()
* @method void setCreated(integer $value)
*/
class UploadedFile extends Entity {
protected $formId;
protected $originalFileName;
protected $fileId;
protected $created;

/**
* Answer constructor.
*/
public function __construct() {
$this->addType('id', 'string');
$this->addType('formId', 'integer');
$this->addType('originalFileName', 'string');
$this->addType('fileId', 'integer');
$this->addType('created', 'integer');
}

public function read(): array {
return [
'id' => $this->getId(),
'formId' => $this->getFormId(),
'originalFileName' => $this->getOriginalFileName(),
'fileId' => $this->getFileId(),
'created' => $this->getCreated(),
];
}
}
59 changes: 59 additions & 0 deletions lib/Db/UploadedFileMapper.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
<?php
/**
* @copyright Copyright (c) 2024 Kostiantyn Miakshyn <[email protected]>
*
* @author Kostiantyn Miakshyn <[email protected]>
*
* @license AGPL-3.0-or-later
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/

namespace OCA\Forms\Db;

use OCP\AppFramework\Db\QBMapper;
use OCP\DB\QueryBuilder\IQueryBuilder;
use OCP\IDBConnection;

/**
* @extends QBMapper<UploadedFile>
*/
class UploadedFileMapper extends QBMapper {

/**
* AnswerMapper constructor.
* @param IDBConnection $db
*/
public function __construct(IDBConnection $db) {
parent::__construct($db, 'forms_v2_uploaded_files', UploadedFile::class);
}

/**
* @param string $uploadedFileId
* @throws \OCP\AppFramework\Db\DoesNotExistException if not found
* @return UploadedFile
*/
public function findByUploadedFileId(string $uploadedFileId): UploadedFile {
$qb = $this->db->getQueryBuilder();

$qb->select('*')
->from($this->getTableName())
->where(
$qb->expr()->eq('id', $qb->createNamedParameter($uploadedFileId))
);

return $this->findEntities($qb)[0];
}
}
Loading

0 comments on commit 2410888

Please sign in to comment.