diff --git a/composer.json b/composer.json index bc833324..ba3482bc 100644 --- a/composer.json +++ b/composer.json @@ -11,6 +11,8 @@ "cs:check": "php-cs-fixer fix --dry-run --diff", "cs:fix": "php-cs-fixer fix", "psalm": "psalm.phar --no-cache", + "psalm:update-baseline": "psalm.phar --threads=1 --update-baseline", + "psalm:update-baseline:force": "psalm.phar --threads=1 --update-baseline --set-baseline=tests/psalm-baseline.xml", "test:unit": "phpunit --config tests/phpunit.xml", "openapi": "generate-spec --verbose --allow-missing-docs" }, diff --git a/lib/Command/ExportProject.php b/lib/Command/ExportProject.php index a92ca0e2..7b87c921 100644 --- a/lib/Command/ExportProject.php +++ b/lib/Command/ExportProject.php @@ -14,6 +14,7 @@ namespace OCA\Cospend\Command; use OC\Core\Command\Base; use OCA\Cospend\Db\ProjectMapper; +use OCA\Cospend\Service\CospendService; use OCA\Cospend\Service\LocalProjectService; use Symfony\Component\Console\Input\InputArgument; use Symfony\Component\Console\Input\InputInterface; @@ -22,8 +23,9 @@ use Symfony\Component\Console\Output\OutputInterface; class ExportProject extends Base { public function __construct( - private LocalProjectService $projectService, - private ProjectMapper $projectMapper, + private LocalProjectService $localProjectService, + private CospendService $cospendService, + private ProjectMapper $projectMapper, ) { parent::__construct(); } @@ -48,7 +50,9 @@ class ExportProject extends Base { $name = $input->getArgument('filename'); $dbProject = $this->projectMapper->find($projectId); if ($dbProject !== null) { - $result = $this->projectService->exportCsvProject($projectId, $dbProject->getUserid(), $name); + $projectInfo = $this->localProjectService->getProjectInfoWithAccessLevel($projectId, $dbProject->getUserid()); + $bills = $this->localProjectService->getBills($projectId); + $result = $this->cospendService->exportCsvProject($projectId, $dbProject->getUserid(), $projectInfo, $bills, $name); if (array_key_exists('path', $result)) { $output->writeln( 'Project "'.$projectId.'" exported in "'.$result['path']. diff --git a/lib/Controller/PublicApiController.php b/lib/Controller/PublicApiController.php index 6cab54c0..00e60ac0 100644 --- a/lib/Controller/PublicApiController.php +++ b/lib/Controller/PublicApiController.php @@ -929,7 +929,7 @@ class PublicApiController extends OCSController { * * @param string $token Project share token * @param array $order Array describing the categories ordering - * @return DataResponse + * @return DataResponse|DataResponse, array{}> * * 200: Categories order is saved * 403: Not saved @@ -944,8 +944,6 @@ class PublicApiController extends OCSController { try { $this->localProjectService->saveCategoryOrder($this->projectId, $order); return new DataResponse(''); - } catch (CospendBasicException $e) { - return new DataResponse($e->data, $e->getCode()); } catch (\Throwable $e) { return new DataResponse(['error' => $e->getMessage()], Http::STATUS_BAD_REQUEST); } @@ -971,8 +969,6 @@ class PublicApiController extends OCSController { try { $this->localProjectService->deleteCategory($this->projectId, $categoryId); return new DataResponse($categoryId); - } catch (CospendBasicException $e) { - return new DataResponse($e->data, $e->getCode()); } catch (\Throwable $e) { return new DataResponse(['error' => $e->getMessage()], Http::STATUS_BAD_REQUEST); } @@ -984,7 +980,7 @@ class PublicApiController extends OCSController { * @param string $token * @param string $name * @param float $rate - * @return DataResponse + * @return DataResponse|DataResponse, array{}> */ #[NoAdminRequired] #[PublicPage] @@ -1008,8 +1004,9 @@ class PublicApiController extends OCSController { * @param int $currencyId * @param string $name * @param float $rate - * @return DataResponse|DataResponse, array{}> + * @return DataResponse|DataResponse, array{}> * @throws Exception + * @throws MultipleObjectsReturnedException */ #[NoAdminRequired] #[PublicPage] @@ -1024,7 +1021,12 @@ class PublicApiController extends OCSController { ); return new DataResponse($currency); } catch (CospendBasicException $e) { - return new DataResponse($e->data, $e->getCode()); + if ($e->getCode() == Http::STATUS_FORBIDDEN) { + return new DataResponse($e->data, Http::STATUS_FORBIDDEN); + } elseif ($e->getCode() == Http::STATUS_NOT_FOUND) { + return new DataResponse($e->data, Http::STATUS_NOT_FOUND); + } + return new DataResponse($e->data, Http::STATUS_BAD_REQUEST); } } @@ -1046,7 +1048,7 @@ class PublicApiController extends OCSController { $this->localProjectService->deleteCurrency($this->projectId, $currencyId); return new DataResponse(''); } catch (CospendBasicException $e) { - return new DataResponse($e->data, $e->getCode()); + return new DataResponse($e->data, Http::STATUS_BAD_REQUEST); } catch (\Throwable $e) { return new DataResponse(['error' => $e->getMessage()], Http::STATUS_BAD_REQUEST); } diff --git a/lib/Db/CategoryMapper.php b/lib/Db/CategoryMapper.php index b4c2c617..200c4e10 100644 --- a/lib/Db/CategoryMapper.php +++ b/lib/Db/CategoryMapper.php @@ -23,7 +23,7 @@ use OCP\IDBConnection; * @method Category mapRowToEntity(array $row) * @method Category findEntity(IQueryBuilder $query) * @method Category[] findEntities(IQueryBuilder $query) - * @template-extends QBMapper + * @template-extends QBMapper */ class CategoryMapper extends QBMapper { public function __construct(IDBConnection $db) { diff --git a/lib/Db/CurrencyMapper.php b/lib/Db/CurrencyMapper.php index 1a7986a2..52265132 100644 --- a/lib/Db/CurrencyMapper.php +++ b/lib/Db/CurrencyMapper.php @@ -23,7 +23,7 @@ use OCP\IDBConnection; * @method Currency mapRowToEntity(array $row) * @method Currency findEntity(IQueryBuilder $query) * @method Currency[] findEntities(IQueryBuilder $query) - * @template-extends QBMapper + * @template-extends QBMapper */ class CurrencyMapper extends QBMapper { public function __construct(IDBConnection $db) { diff --git a/lib/Db/PaymentModeMapper.php b/lib/Db/PaymentModeMapper.php index 88b777da..cfde5026 100644 --- a/lib/Db/PaymentModeMapper.php +++ b/lib/Db/PaymentModeMapper.php @@ -23,7 +23,7 @@ use OCP\IDBConnection; * @method PaymentMode mapRowToEntity(array $row) * @method PaymentMode findEntity(IQueryBuilder $query) * @method PaymentMode[] findEntities(IQueryBuilder $query) - * @template-extends QBMapper + * @template-extends QBMapper */ class PaymentModeMapper extends QBMapper { public function __construct(IDBConnection $db) { diff --git a/lib/Db/ProjectMapper.php b/lib/Db/ProjectMapper.php index b33cb1ab..13551a8b 100644 --- a/lib/Db/ProjectMapper.php +++ b/lib/Db/ProjectMapper.php @@ -56,6 +56,25 @@ class ProjectMapper extends QBMapper { return $this->findEntity($qb); } + /** + * @param string $id + * @return Project|null + */ + public function find(string $id): ?Project { + $qb = $this->db->getQueryBuilder(); + $qb->select('*') + ->from($this->getTableName()) + ->where( + $qb->expr()->eq('id', $qb->createNamedParameter($id, IQueryBuilder::PARAM_STR)) + ); + + try { + return $this->findEntity($qb); + } catch (DoesNotExistException | MultipleObjectsReturnedException |\OCP\DB\Exception $e) { + return null; + } + } + /** * @param string $name * @param string $id @@ -113,25 +132,6 @@ class ProjectMapper extends QBMapper { return $insertedProject; } - /** - * @param string $id - * @return Project|null - */ - public function find(string $id): ?Project { - $qb = $this->db->getQueryBuilder(); - $qb->select('*') - ->from($this->getTableName()) - ->where( - $qb->expr()->eq('id', $qb->createNamedParameter($id, IQueryBuilder::PARAM_STR)) - ); - - try { - return $this->findEntity($qb); - } catch (DoesNotExistException | MultipleObjectsReturnedException |\OCP\DB\Exception $e) { - return null; - } - } - /** * @param string $userId * @return Project[] diff --git a/lib/Service/CospendService.php b/lib/Service/CospendService.php index 0f282c64..631c11c5 100644 --- a/lib/Service/CospendService.php +++ b/lib/Service/CospendService.php @@ -746,7 +746,9 @@ class CospendService { $userFolder = $this->root->getUserFolder($uid); if (!$userFolder->nodeExists($outPath . '/' . $exportName)) { - $this->localProjectService->exportCsvProject($dbProjectId, $uid, $exportName); + $projectInfo = $this->localProjectService->getProjectInfoWithAccessLevel($dbProjectId, $uid); + $bills = $this->localProjectService->getBills($dbProjectId); + $this->exportCsvProject($dbProjectId, $uid, $projectInfo, $bills, $exportName); } } $req->closeCursor(); @@ -911,14 +913,15 @@ class CospendService { * * @param string $projectId * @param string $userId + * @param array $projectInfo + * @param array $bills * @param string|null $name * @return array + * @throws InvalidPathException + * @throws LockedException * @throws NoUserException * @throws NotFoundException * @throws NotPermittedException - * @throws \OCP\DB\Exception - * @throws InvalidPathException - * @throws LockedException */ public function exportCsvProject(string $projectId, string $userId, array $projectInfo, array $bills, ?string $name = null): array { // create export directory if needed diff --git a/lib/Service/FederatedProjectService.php b/lib/Service/FederatedProjectService.php index b6bb4694..5d397c78 100644 --- a/lib/Service/FederatedProjectService.php +++ b/lib/Service/FederatedProjectService.php @@ -27,6 +27,7 @@ class FederatedProjectService implements IProjectService { private IClient $client; public string $userId; + public string $USER_AGENT; public function __construct( IClientService $clientService, @@ -102,7 +103,7 @@ class FederatedProjectService implements IProjectService { $this->request($projectId, 'api/v1/public/projects/{token}/{password}', [], 'DELETE'); } - public function getProjectInfoWithAccessLevel(string $projectId, string $userId): ?array { + public function getProjectInfoWithAccessLevel(string $projectId, string $userId): array { $projectInfo = $this->request($projectId, 'api/v1/public/projects/{token}/{password}'); $projectInfo['id'] = $projectId; $projectInfo['federated'] = true; diff --git a/lib/Service/IProjectService.php b/lib/Service/IProjectService.php index 7766e74c..ef39b31f 100644 --- a/lib/Service/IProjectService.php +++ b/lib/Service/IProjectService.php @@ -31,9 +31,9 @@ interface IProjectService { /** * @param string $projectId * @param string $userId - * @return array|null + * @return array */ - public function getProjectInfoWithAccessLevel(string $projectId, string $userId): ?array; + public function getProjectInfoWithAccessLevel(string $projectId, string $userId): array; /** * Get project statistics diff --git a/lib/Service/LocalProjectService.php b/lib/Service/LocalProjectService.php index 536498f7..da6064a2 100644 --- a/lib/Service/LocalProjectService.php +++ b/lib/Service/LocalProjectService.php @@ -318,19 +318,16 @@ class LocalProjectService implements IProjectService { * Get all project data * * @param string $projectId - * @return CospendProjectInfoPlusExtra|null + * @return CospendProjectInfoPlusExtra * @throws CospendBasicException * @throws DoesNotExistException * @throws MultipleObjectsReturnedException * @throws \OCP\DB\Exception */ - public function getProjectInfo(string $projectId): ?array { + public function getProjectInfo(string $projectId): array { try { - $dbProject = $this->projectMapper->find($projectId); - } catch (Exception | Throwable $e) { - return null; - } - if ($dbProject === null) { + $dbProject = $this->projectMapper->getById($projectId); + } catch (DoesNotExistException) { throw new CospendBasicException('', Http::STATUS_NOT_FOUND, ['error' => 'project not found']); } $dbProjectId = $dbProject->getId(); @@ -374,10 +371,13 @@ class LocalProjectService implements IProjectService { /** * @param string $projectId * @param string $userId - * @return array|null + * @return array + * @throws CospendBasicException + * @throws DoesNotExistException + * @throws MultipleObjectsReturnedException * @throws \OCP\DB\Exception */ - public function getProjectInfoWithAccessLevel(string $projectId, string $userId): ?array { + public function getProjectInfoWithAccessLevel(string $projectId, string $userId): array { $projectInfo = $this->getProjectInfo($projectId); $projectInfo['myaccesslevel'] = $this->getUserMaxAccessLevel($userId, $projectId); return $projectInfo; @@ -998,11 +998,10 @@ class LocalProjectService implements IProjectService { $insertedBillId = $createdBill->getId(); // insert bill owers - $qb = $this->db->getQueryBuilder(); foreach ($owerIds as $owerId) { $billOwer = new BillOwer(); $billOwer->setBillid($insertedBillId); - $billOwer->setMemberid($owerId); + $billOwer->setMemberid((int)$owerId); $this->billOwerMapper->insert($billOwer); } @@ -1464,7 +1463,7 @@ class LocalProjectService implements IProjectService { * @param bool $active * @param string|null $color * @param string|null $userId - * @return array + * @return CospendMember * @throws CospendBasicException * @throws \OCP\DB\Exception */ @@ -2251,7 +2250,7 @@ class LocalProjectService implements IProjectService { foreach ($owerIds as $owerId) { $billOwer = new BillOwer(); $billOwer->setBillid($billId); - $billOwer->setMemberid($owerId); + $billOwer->setMemberid((int)$owerId); $this->billOwerMapper->insert($billOwer); } } @@ -2709,7 +2708,7 @@ class LocalProjectService implements IProjectService { * @throws \OCP\DB\Exception */ public function createPaymentMode(string $projectId, string $name, ?string $icon, string $color, ?int $order = 0): int { - $pm = new Category(); + $pm = new PaymentMode(); $pm->setProjectid($projectId); $pm->setName($name); $pm->setOrder(is_null($order) ? 0 : $order); @@ -2740,12 +2739,12 @@ class LocalProjectService implements IProjectService { * @param string $projectId * @param int $pmId * @return void - * @throws CospendBasicException + * @throws DoesNotExistException * @throws MultipleObjectsReturnedException * @throws \OCP\DB\Exception */ public function deletePaymentMode(string $projectId, int $pmId): void { - $pmToDelete = $this->getPaymentMode($projectId, $pmId); + $pmToDelete = $this->paymentModeMapper->getPaymentModeOfProject($projectId, $pmId); $this->paymentModeMapper->delete($pmToDelete); // then get rid of this pm in bills @@ -2841,12 +2840,12 @@ class LocalProjectService implements IProjectService { * @param string $projectId * @param int $categoryId * @return void - * @throws CospendBasicException + * @throws DoesNotExistException * @throws MultipleObjectsReturnedException * @throws \OCP\DB\Exception */ public function deleteCategory(string $projectId, int $categoryId): void { - $categoryToDelete = $this->getCategory($projectId, $categoryId); + $categoryToDelete = $this->categoryMapper->getCategoryOfProject($projectId, $categoryId); $this->categoryMapper->delete($categoryToDelete); // then get rid of this category in bills diff --git a/lib/UserMigration/UserMigrator.php b/lib/UserMigration/UserMigrator.php index 1731d3e1..a797c14b 100644 --- a/lib/UserMigration/UserMigrator.php +++ b/lib/UserMigration/UserMigrator.php @@ -64,13 +64,14 @@ class UserMigrator implements IMigrator, ISizeEstimationMigrator { public function export(IUser $user, IExportDestination $exportDestination, OutputInterface $output): void { $output->writeln('Exporting Cospend projects in ' . self::PROJECTS_PATH . '…'); $userId = $user->getUID(); - /** @var Project[] $projects */ $projects = $this->projectMapper->getProjects($userId); foreach ($projects as $project) { try { $exportFilePath = self::PROJECTS_PATH . '/' . $project->getId() . '.csv'; $content = ''; - foreach ($this->localProjectService->getJsonProject($project->getId()) as $chunk) { + $projectInfo = $this->localProjectService->getProjectInfoWithAccessLevel($project->getId(), $userId); + $bills = $this->localProjectService->getBills($project->getId()); + foreach ($this->cospendService->getJsonProject($projectInfo, $bills) as $chunk) { $content .= $chunk; } $exportDestination->addFileContents($exportFilePath, $content); diff --git a/psalm.xml b/psalm.xml index 3b348392..d473bbe9 100644 --- a/psalm.xml +++ b/psalm.xml @@ -32,6 +32,7 @@ + @@ -56,5 +57,9 @@ + + + + diff --git a/tests/stubs/GuzzleHttp_Exception_ClientException.php b/tests/stubs/GuzzleHttp_Exception_ClientException.php new file mode 100644 index 00000000..3edd3afc --- /dev/null +++ b/tests/stubs/GuzzleHttp_Exception_ClientException.php @@ -0,0 +1,12 @@ + + * @throws Exception + */ + public function getServers() { + } + /** + * Check if given server is a trusted Nextcloud server + */ + public function isTrustedServer(string $url) : bool { + } + /** + * Set server status + */ + public function setServerStatus(string $url, int $status) : void { + } + /** + * Get server status + */ + public function getServerStatus(string $url) : int { + } + /** + * Check if URL point to a ownCloud/Nextcloud server + */ + public function isNextcloudServer(string $url) : bool { + } + /** + * Check if ownCloud/Nextcloud version is >= 9.0 + * @throws HintException + */ + protected function checkNextcloudVersion(string $status) : bool { + } + /** + * Check if the URL contain a protocol, if not add https + */ + protected function updateProtocol(string $url) : string { + } +}