diff options
| author | Philipp Schüttlöffel <schuettloeffel@zqs.uni-hannover.de> | 2024-09-24 10:53:31 +0200 |
|---|---|---|
| committer | Philipp Schüttlöffel <schuettloeffel@zqs.uni-hannover.de> | 2024-09-24 10:53:31 +0200 |
| commit | 4459dd7917f4d1c34f40bb68f0e991e9c3d53e4c (patch) | |
| tree | 5c07151ae61276d334e88f6309c30d439a85c12e /lib/classes/JsonApi | |
| parent | da0022e5c1abbf9825ae76debaabdff7e8623bb4 (diff) | |
| parent | 97a188592c679890a25c37ab78463add76a52ff7 (diff) | |
Merge branch 'main' into issue-3911issue-3911
Diffstat (limited to 'lib/classes/JsonApi')
24 files changed, 710 insertions, 172 deletions
diff --git a/lib/classes/JsonApi/JsonApiController.php b/lib/classes/JsonApi/JsonApiController.php index 1718c52..614650d 100644 --- a/lib/classes/JsonApi/JsonApiController.php +++ b/lib/classes/JsonApi/JsonApiController.php @@ -375,12 +375,12 @@ class JsonApiController * * @param Request $request Request der eingehende Request * - * @return mixed entweder null oder das User-Objekt des - * "eingeloggten" Nutzers + * @return null|\User entweder null oder das User-Objekt des "eingeloggten" + * Nutzers */ public function getUser(Request $request) { - return $request->getAttribute(Authentication::USER_KEY, null); + return $request->getAttribute(Authentication::USER_KEY); } /** diff --git a/lib/classes/JsonApi/Middlewares/Auth/OAuth1Strategy.php b/lib/classes/JsonApi/Middlewares/Auth/OAuth1Strategy.php deleted file mode 100644 index 113ee09..0000000 --- a/lib/classes/JsonApi/Middlewares/Auth/OAuth1Strategy.php +++ /dev/null @@ -1,114 +0,0 @@ -<?php - -namespace JsonApi\Middlewares\Auth; - -use Psr\Http\Message\ResponseInterface as Response; -use Psr\Http\Message\ServerRequestInterface as Request; - -class OAuth1Strategy implements Strategy -{ - /** @var callable */ - protected $authenticator; - - /** @var Request */ - protected $request; - - /** @var ?\User */ - protected $user; - - /** - * @param callable $authenticator - */ - public function __construct(Request $request, $authenticator) - { - $this->request = $request; - $this->authenticator = $authenticator; - - \OAuthStore::instance('PDO', ['conn' => \DBManager::get()]); - } - - public function check() - { - return !is_null($this->user()); - } - - public function user() - { - if (!is_null($this->user)) { - return $this->user; - } - - $this->user = $this->detect(); - - return $this->user; - } - - public function addChallenge(Response $response) - { - return $response; //->withHeader('WWW-Authenticate', sprintf('Basic realm="%s"', 'Stud.IP JSON-API')); - } - - private function detect(): ?\User - { - if (!\OAuthRequestVerifier::requestIsSigned()) { - return null; - } - - $uri = (string) $this->request->getUri(); - $method = $this->request->getMethod(); - - if ('GET' === strtoupper(($method))) { - $parameters = (array) $this->request->getQueryParams(); - } elseif ('POST' === strtoupper(($method))) { - $parameters = (array) $this->request->getParsedBody(); - } else { - $parameters = []; - } - $parameters = $this->getParamsFromAuthorizationHeader($this->request, $parameters); - - $req = new \OAuthRequestVerifier($uri, $method, $parameters); - - // Check oauth timestamp and deny access if timestamp is outdated - if ($req->getParam('oauth_timestamp') < strtotime('-6 hours')) { - return null; - } - - $result = $req->verifyExtended('access'); - - $query = 'SELECT user_id FROM api_oauth_user_mapping WHERE oauth_id = ?'; - $statement = \DBManager::get()->prepare($query); - $statement->execute([$result['user_id']]); - - if (!$userId = $statement->fetchColumn()) { - return null; - } - - /** @var \User */ - return \User::find($userId); - } - - private function getParamsFromAuthorizationHeader(Request $request, array $params): array - { - if ($request->hasHeader('Authorization')) { - $auth = $request->getHeaderLine('Authorization'); - if (0 == strncasecmp($auth, 'OAuth', 4)) { - foreach (explode(',', substr($auth, 6)) as $v) { - if (!strpos($v, '=')) { - continue; - } - $v = trim($v); - list($name, $value) = explode('=', $v, 2); - if (!empty($value) && '"' == $value[0] && '"' == substr($value, -1)) { - $value = substr(substr($value, 1), 0, -1); - } - - if (0 != strcasecmp($name, 'realm')) { - $params[$name] = $value; - } - } - } - } - - return $params; - } -} diff --git a/lib/classes/JsonApi/Middlewares/Authentication.php b/lib/classes/JsonApi/Middlewares/Authentication.php index de92e15..bbcfef1 100644 --- a/lib/classes/JsonApi/Middlewares/Authentication.php +++ b/lib/classes/JsonApi/Middlewares/Authentication.php @@ -15,22 +15,21 @@ class Authentication // $user = $request->getAttribute(Authentication::USER_KEY); const USER_KEY = 'studip-user'; - // a callable accepting two arguments username and password and - // returning either null or a Stud.IP user object - /** @var callable */ - private $authenticator; - /** * Der Konstruktor. * - * @param callable $authenticator ein Callable, das den Nutzernamen und + * @param \Closure $authenticator eine Closure, die den Nutzernamen und * das Passwort als Argumente erhält und * damit entweder einen Stud.IP-User-Objekt * oder null zurückgibt + * @param array $excluded_strategies */ - public function __construct($authenticator) - { - $this->authenticator = $authenticator; + public function __construct( + // a callable accepting two arguments username and password and + // returning either null or a Stud.IP user object + private readonly \Closure $authenticator, + private readonly array $excluded_strategies = [] + ) { } /** @@ -45,12 +44,7 @@ class Authentication */ public function __invoke(Request $request, RequestHandler $handler) { - $guards = [ - new Auth\SessionStrategy(), - new Auth\HttpBasicAuthStrategy($request, $this->authenticator), - new Auth\OAuth2Strategy($request, $this->authenticator), - new Auth\OAuth1Strategy($request, $this->authenticator), - ]; + $guards = $this->getGuards($request); foreach ($guards as $guard) { if ($guard->check()) { @@ -101,4 +95,24 @@ class Authentication return $request->withAttribute(self::USER_KEY, $user); } + + /** + * @param Request $request + * + * @return array + */ + protected function getGuards(Request $request): array + { + $guards = [ + 'session' => new Auth\SessionStrategy(), + 'basic' => new Auth\HttpBasicAuthStrategy($request, $this->authenticator), + 'oauth2' => new Auth\OAuth2Strategy($request, $this->authenticator), + ]; + + foreach ($this->excluded_strategies as $strategy) { + unset($guards[$strategy]); + } + + return $guards; + } } diff --git a/lib/classes/JsonApi/Middlewares/StudipMockNavigation.php b/lib/classes/JsonApi/Middlewares/StudipMockNavigation.php index 34225d4..0883809 100644 --- a/lib/classes/JsonApi/Middlewares/StudipMockNavigation.php +++ b/lib/classes/JsonApi/Middlewares/StudipMockNavigation.php @@ -18,53 +18,38 @@ class DummyNavigation extends \Navigation implements \ArrayAccess /** * ArrayAccess: Check whether the given offset exists. - * - * @todo Add bool return type when Stud.IP requires PHP8 minimal */ - #[ReturnTypeWillChange] - public function offsetExists($offset) + public function offsetExists($offset): bool { return true; } /** * ArrayAccess: Get the value at the given offset. - * - * @todo Add mixed return type when Stud.IP requires PHP8 minimal */ - #[\ReturnTypeWillChange] - public function offsetGet($offset) + public function offsetGet($offset): mixed { return $this; } /** * ArrayAccess: Set the value at the given offset. - * - * @todo Add void return type when Stud.IP requires PHP8 minimal */ - #[\ReturnTypeWillChange] - public function offsetSet($offset, $value) + public function offsetSet($offset, $value): void { } /** * ArrayAccess: Delete the value at the given offset. - * - * @todo Add void return type when Stud.IP requires PHP8 minimal */ - #[\ReturnTypeWillChange] - public function offsetUnset($offset) + public function offsetUnset($offset): void { } /** - * IteratorAggregate: Create interator for request parameters. - * - * @todo Add \Traversable return type when Stud.IP requires PHP8 minimal + * IteratorAggregate: Create iterator for request parameters. */ - #[\ReturnTypeWillChange] - public function getIterator() + public function getIterator(): \Traversable { return new \ArrayIterator(); } diff --git a/lib/classes/JsonApi/NonJsonApiController.php b/lib/classes/JsonApi/NonJsonApiController.php index 8384b54..bfc51af 100644 --- a/lib/classes/JsonApi/NonJsonApiController.php +++ b/lib/classes/JsonApi/NonJsonApiController.php @@ -49,11 +49,11 @@ class NonJsonApiController } /** - * @return mixed + * @return null|\User */ protected function getUser(Request $request) { - return $request->getAttribute(Authentication::USER_KEY, null); + return $request->getAttribute(Authentication::USER_KEY); } /** diff --git a/lib/classes/JsonApi/RouteMap.php b/lib/classes/JsonApi/RouteMap.php index 4f44165..63c69e6 100644 --- a/lib/classes/JsonApi/RouteMap.php +++ b/lib/classes/JsonApi/RouteMap.php @@ -5,8 +5,7 @@ namespace JsonApi; use JsonApi\Contracts\JsonApiPlugin; use JsonApi\Middlewares\Authentication; use JsonApi\Middlewares\DangerousRouteHandler; -use JsonApi\Middlewares\JsonApi as JsonApiMiddleware; -use JsonApi\Middlewares\StudipMockNavigation; +use JsonApi\Routes\Consultations\SlotCreationCount; use JsonApi\Routes\Holidays\HolidaysShow; use Slim\Routing\RouteCollectorProxy; @@ -49,7 +48,6 @@ use Slim\Routing\RouteCollectorProxy; * * $this->app->post('/article/{id}/comments', MeineRoute::class); * - * @see \JsonApi\Middlewares\JsonApi * @see \JsonApi\Middlewares\Authentication * @see \JsonApi\Contracts\JsonApiPlugin * @see http://www.slimframework.com/docs/objects/router.html#how-to-create-routes @@ -118,11 +116,12 @@ class RouteMap $group->get('/status-groups/{id}', Routes\StatusgroupShow::class); $this->addAuthenticatedBlubberRoutes($group); + $this->addAuthenticatedClipboardRoutes($group); $this->addAuthenticatedConsultationRoutes($group); $this->addAuthenticatedContactsRoutes($group); $this->addAuthenticatedCoursesRoutes($group); - if (\PluginManager::getInstance()->getPlugin('CoursewareModule')) { + if (\PluginManager::getInstance()->getPlugin(\CoursewareModule::class)) { $this->addAuthenticatedCoursewareRoutes($group); } @@ -155,7 +154,7 @@ class RouteMap $group->get('/studip/properties', Routes\Studip\PropertiesIndex::class); - if (\PluginManager::getInstance()->getPlugin('CoursewareModule')) { + if (\PluginManager::getInstance()->getPlugin(\CoursewareModule::class)) { $group->get('/public/courseware/{link_id}/courseware-structural-elements/{id}', Routes\Courseware\PublicStructuralElementsShow::class); $group->get('/public/courseware/{link_id}/courseware-structural-elements', Routes\Courseware\PublicStructuralElementsIndex::class); } @@ -205,8 +204,26 @@ class RouteMap ); } + private function addAuthenticatedClipboardRoutes(RouteCollectorProxy $group): void + { + $group->post('/clipboards', Routes\Clipboards\ClipboardsCreate::class); + $group->patch('/clipboards/{id}', Routes\Clipboards\ClipboardsUpdate::class); + $group->delete('/clipboards/{id}', Routes\Clipboards\ClipboardsDelete::class); + + $group->get('/clipboard-items/{id}', Routes\Clipboards\ClipboardItemsShow::class); + $group->post('/clipboards/{id}/items', Routes\Clipboards\ClipboardItemsCreate::class); + $group->delete('/clipboards/{id}/items', Routes\Clipboards\ClipboardItemsDelete::class); + $group->delete('/clipboards/{id}/items/{itemId}', Routes\Clipboards\ClipboardItemsDelete::class); + + $group->post('/clipboard-items', Routes\Clipboards\ClipboardItemsCreate::class); + $group->delete('/clipboard-items/{id}', Routes\Clipboards\ClipboardItemsDelete::class); + } + private function addAuthenticatedConsultationRoutes(RouteCollectorProxy $group): void { + // TODO: I know, not very JSONAPI-like but it's a NonJsonApiController ¯\_(ツ)_/¯ + $group->get('/consultation-slots/count', SlotCreationCount::class); + $group->get('/{type:courses|institutes|users}/{id}/consultations', Routes\Consultations\BlocksByRangeIndex::class); $group->get('/consultation-blocks/{id}', Routes\Consultations\BlockShow::class); diff --git a/lib/classes/JsonApi/Routes/Clipboards/Authority.php b/lib/classes/JsonApi/Routes/Clipboards/Authority.php new file mode 100644 index 0000000..5cc053a --- /dev/null +++ b/lib/classes/JsonApi/Routes/Clipboards/Authority.php @@ -0,0 +1,28 @@ +<?php +namespace JsonApi\Routes\Clipboards; + +use User; + +final class Authority +{ + public static function canCreateClipboard(User $user): bool + { + return true; + } + + public static function canAccessClipboard(User $user, \Clipboard $clipboard): bool + { + return $user->id === $clipboard->user_id + || $user->perms === 'root'; + } + + public static function canUpdateClipboard(User $user, \Clipboard $clipboard): bool + { + return self::canAccessClipboard($user, $clipboard); + } + + public static function canDeleteClipboard(User $user, \Clipboard $clipboard): bool + { + return self::canUpdateClipboard($user, $clipboard); + } +} diff --git a/lib/classes/JsonApi/Routes/Clipboards/ClipboardItemsCreate.php b/lib/classes/JsonApi/Routes/Clipboards/ClipboardItemsCreate.php new file mode 100644 index 0000000..d57d0c5 --- /dev/null +++ b/lib/classes/JsonApi/Routes/Clipboards/ClipboardItemsCreate.php @@ -0,0 +1,106 @@ +<?php +namespace JsonApi\Routes\Clipboards; + +use JsonApi\Errors\AuthorizationFailedException; +use JsonApi\JsonApiController; +use JsonApi\Routes\ValidationTrait; +use JsonApi\Schemas\Clipboard; +use Psr\Http\Message\{ + ResponseInterface as Response, + ServerRequestInterface as Request +}; + +final class ClipboardItemsCreate extends JsonApiController +{ + use ValidationTrait; + + public function __invoke(Request $request, Response $response, $args): Response + { + $json = $this->validate($request, $args); + + $clipboard_id = $args['id'] ?? $json['data']['relationships']['clipboard']['data']['id']; + $clipboard = \Clipboard::find($clipboard_id); + + $user = $this->getUser($request); + if (!Authority::canUpdateClipboard($user, $clipboard)) { + throw new AuthorizationFailedException(); + } + + $range_id = $json['data']['attributes']['range_id']; + $range_type = $json['data']['attributes']['range_type']; + + $item = \ClipboardItem::findOneBySql( + 'clipboard_id = ? AND range_id = ? AND range_type = ?', + [$clipboard_id, $range_id, $range_type] + ); + + if ($item) { + return $this->getCodeResponse(302, [ + 'Location' => $this->getLinkToItem($item), + ]); + } + + $item = \ClipboardItem::create([ + 'clipboard_id' => $clipboard_id, + 'range_id' => $range_id, + 'range_type' => $range_type, + ]); + + return $this->getContentResponse($item); + } + + protected function validateResourceDocument($json, $data) + { + $clipboardValidationError = $this->validateRequestContainsValidClipboard($json, $data); + if ($clipboardValidationError !== null) { + return $clipboardValidationError; + } + + if (!self::arrayHas($json, 'data.attributes.range_id')) { + return 'No range_id defined'; + } + + if (!self::arrayHas($json, 'data.attributes.range_type')) { + return 'No range_type defined'; + } + + $range_type = self::arrayGet($json, 'data.attributes.range_type'); + if (!is_a($range_type, \StudipItem::class, true)) { + return 'Range type must implement interface StudipItem'; + } + + return null; + } + + private function validateRequestContainsValidClipboard($json, $data): ?string + { + if (isset($data['id'])) { + if (!\Clipboard::exists($data['id'])) { + return 'Provided clipboard id is invalid'; + } + } else { + if (!self::arrayHas($json, 'data.relationships.clipboard')) { + return 'No clipboard relationship defined'; + } + + $clipboard = self::arrayGet($json, 'data.relationships.clipboard'); + if ( + !isset($clipboard['data']['type'], $clipboard['data']['id']) + || $clipboard['data']['type'] !== Clipboard::TYPE + ) { + return 'Defined clipboard relationship has invalid format.'; + } + if (!\Clipboard::exists($clipboard['data']['id'])) { + return 'Related clipboard does not exist.'; + } + } + + return null; + } + + private function getLinkToItem(\ClipboardItem $item): string + { + $json = $this->encoder->encodeData($item); + return json_decode($json, true)['data']['links']['self']; + } +} diff --git a/lib/classes/JsonApi/Routes/Clipboards/ClipboardItemsDelete.php b/lib/classes/JsonApi/Routes/Clipboards/ClipboardItemsDelete.php new file mode 100644 index 0000000..a9c7cd4 --- /dev/null +++ b/lib/classes/JsonApi/Routes/Clipboards/ClipboardItemsDelete.php @@ -0,0 +1,54 @@ +<?php +namespace JsonApi\Routes\Clipboards; + +use JsonApi\Errors\BadRequestException; +use JsonApi\Errors\RecordNotFoundException; +use JsonApi\JsonApiController; +use Psr\Http\Message\{ + ResponseInterface as Response, + ServerRequestInterface as Request +}; + +final class ClipboardItemsDelete extends JsonApiController +{ + protected $allowedFilteringParameters = ['range_id']; + + public function __invoke(Request $request, Response $response, $args): Response + { + $clipboard = \Clipboard::find($args['id']); + if (!$clipboard) { + throw new RecordNotFoundException('Clipboard not found'); + } + + $user = $this->getUser($request); + if (!Authority::canUpdateClipboard($user, $clipboard)) { + throw new \AccessDeniedException(); + } + + $item = null; + if (isset($args['itemId'])) { + $item = \ClipboardItem::find($args['itemId']); + } else { + $filtering = iterator_to_array($this->getQueryParameters()->getFilters()); + if (!isset($filtering['range_id'])) { + throw new BadRequestException('No range_id filter given'); + } + $item = \ClipboardItem::findOneBySQL( + 'clipboard_id = ? AND range_id = ?', + [$clipboard->id, $filtering['range_id']] + ); + } + + if (!$item) { + throw new RecordNotFoundException('Item not found'); + } + + if ($item->clipboard_id !== $clipboard->id) { + throw new BadRequestException('Item does not belong to clipboard'); + } + + $item->delete(); + + return $this->getCodeResponse(204); + } +} diff --git a/lib/classes/JsonApi/Routes/Clipboards/ClipboardItemsShow.php b/lib/classes/JsonApi/Routes/Clipboards/ClipboardItemsShow.php new file mode 100644 index 0000000..3c91708 --- /dev/null +++ b/lib/classes/JsonApi/Routes/Clipboards/ClipboardItemsShow.php @@ -0,0 +1,28 @@ +<?php +namespace JsonApi\Routes\Clipboards; + +use JsonApi\Errors\AuthorizationFailedException; +use JsonApi\Errors\RecordNotFoundException; +use JsonApi\JsonApiController; +use Psr\Http\Message\{ + ResponseInterface as Response, + ServerRequestInterface as Request +}; + +final class ClipboardItemsShow extends JsonApiController +{ + public function __invoke(Request $request, Response $response, $args): Response + { + $item = \ClipboardItem::find($args['id']); + if (!$item) { + throw new RecordNotFoundException(); + } + + $user = $this->getUser($request); + if (!Authority::canAccessClipboard($user, $item->clipboard)) { + throw new AuthorizationFailedException(); + } + + return $this->getContentResponse($item); + } +} diff --git a/lib/classes/JsonApi/Routes/Clipboards/ClipboardsCreate.php b/lib/classes/JsonApi/Routes/Clipboards/ClipboardsCreate.php new file mode 100644 index 0000000..57fd9b9 --- /dev/null +++ b/lib/classes/JsonApi/Routes/Clipboards/ClipboardsCreate.php @@ -0,0 +1,46 @@ +<?php +namespace JsonApi\Routes\Clipboards; + +use JsonApi\Errors\AuthorizationFailedException; +use JsonApi\JsonApiController; +use JsonApi\Routes\ValidationTrait; +use Psr\Http\Message\{ + ResponseInterface as Response, + ServerRequestInterface as Request +}; + +final class ClipboardsCreate extends JsonApiController +{ + use ValidationTrait; + + public function __invoke(Request $request, Response $response, $args): Response + { + $user = $this->getUser($request); + + if (!Authority::canCreateClipboard($user)) { + throw new AuthorizationFailedException(); + } + + $json = $this->validate($request, $args); + + $clipboard = \Clipboard::create([ + 'name' => $json['data']['attributes']['name'], + 'user_id' => $user->id, + ]); + + return $this->getContentResponse($clipboard); + } + + protected function validateResourceDocument($json, $data) + { + if (!self::arrayHas($json, 'data.attributes.name')) { + return 'No name for the clipboard defined'; + } + + if (!trim(self::arrayGet($json, 'data.attributes.name'))) { + return 'Name of the clipboard may not be empty'; + } + + return null; + } +} diff --git a/lib/classes/JsonApi/Routes/Clipboards/ClipboardsDelete.php b/lib/classes/JsonApi/Routes/Clipboards/ClipboardsDelete.php new file mode 100644 index 0000000..0897843 --- /dev/null +++ b/lib/classes/JsonApi/Routes/Clipboards/ClipboardsDelete.php @@ -0,0 +1,31 @@ +<?php +namespace JsonApi\Routes\Clipboards; + +use JsonApi\Errors\AuthorizationFailedException; +use JsonApi\Errors\RecordNotFoundException; +use JsonApi\JsonApiController; +use Psr\Http\Message\{ + ResponseInterface as Response, + ServerRequestInterface as Request +}; + +final class ClipboardsDelete extends JsonApiController +{ + public function __invoke(Request $request, Response $response, $args): Response + { + $clipboard = \Clipboard::find($args['id']); + if (!$clipboard) { + throw new RecordNotFoundException(); + } + + $user = $this->getUser($request); + + if (!Authority::canDeleteClipboard($user, $clipboard)) { + throw new AuthorizationFailedException(); + } + + $clipboard->delete(); + + return $this->getCodeResponse(204); + } +} diff --git a/lib/classes/JsonApi/Routes/Clipboards/ClipboardsUpdate.php b/lib/classes/JsonApi/Routes/Clipboards/ClipboardsUpdate.php new file mode 100644 index 0000000..83d9539 --- /dev/null +++ b/lib/classes/JsonApi/Routes/Clipboards/ClipboardsUpdate.php @@ -0,0 +1,50 @@ +<?php +namespace JsonApi\Routes\Clipboards; + +use JsonApi\Errors\AuthorizationFailedException; +use JsonApi\Errors\RecordNotFoundException; +use JsonApi\JsonApiController; +use JsonApi\Routes\ValidationTrait; +use Psr\Http\Message\{ + ResponseInterface as Response, + ServerRequestInterface as Request +}; + +final class ClipboardsUpdate extends JsonApiController +{ + use ValidationTrait; + + public function __invoke(Request $request, Response $response, $args): Response + { + $clipboard = \Clipboard::find($args['id']); + if (!$clipboard) { + throw new RecordNotFoundException(); + } + + $user = $this->getUser($request); + + if (!Authority::canUpdateClipboard($user, $clipboard)) { + throw new AuthorizationFailedException(); + } + + $json = $this->validate($request, $args); + + $clipboard->name = $json['data']['attributes']['name']; + $clipboard->store(); + + return $this->getContentResponse($clipboard); + } + + protected function validateResourceDocument($json, $data) + { + if (!self::arrayHas($json, 'data.attributes.name')) { + return 'No name for the clipboard defined'; + } + + if (!trim(self::arrayGet($json, 'data.attributes.name'))) { + return 'Name of the clipboard may not be empty'; + } + + return null; + } +} diff --git a/lib/classes/JsonApi/Routes/Consultations/SlotCreationCount.php b/lib/classes/JsonApi/Routes/Consultations/SlotCreationCount.php new file mode 100644 index 0000000..c378771 --- /dev/null +++ b/lib/classes/JsonApi/Routes/Consultations/SlotCreationCount.php @@ -0,0 +1,105 @@ +<?php +namespace JsonApi\Routes\Consultations; + +use ConsultationBlock; +use JsonApi\Errors\BadRequestException; +use JsonApi\NonJsonApiController; +use Neomerx\JsonApi\Exceptions\JsonApiException; +use Neomerx\JsonApi\Schema\ErrorCollection; +use Psr\Http\Message\ResponseInterface as Response; +use Psr\Http\Message\ServerRequestInterface as Request; + +final class SlotCreationCount extends NonJsonApiController +{ + public function __invoke(Request $request, Response $response, array $args) + { + $parameters = $request->getQueryParams(); + + $this->validateParameters($parameters); + + // Determine duration of a slot and pause times + $slot_count = ConsultationBlock::countSlots( + strtotime($parameters['start']), + strtotime($parameters['end']), + $parameters['dow'], + $parameters['interval'], + $parameters['duration'], + $parameters['pause_time'] ?? null, + $parameters['pause_duration'] ?? null + ); + + $response->getBody()->write((string) $slot_count); + return $response->withAddedHeader('Content-Type', 'application/json'); + } + + private function validateParameters(array $parameters): void + { + $collection = new ErrorCollection(); + + foreach (['start', 'end', 'dow', 'interval', 'duration'] as $key) { + if (!isset($parameters[$key])) { + $collection->addQueryParameterError($key, 'Parameter is missing'); + } + } + + if (isset($parameters['start'], $parameters['end'])) { + $start = strtotime($parameters['start']); + $end = strtotime($parameters['end']); + + if (!$start) { + $collection->addQueryParameterError('start', 'Parameter has invalid datetime format'); + } + + if (!$end) { + $collection->addQueryParameterError('end', 'Parameter has invalid datetime format'); + } + + if ($start && $end && $start > $end) { + $collection->addQueryParameterError('start', 'Datetime value of start must be before end'); + } + } + + if ( + isset($parameters['dow']) + && ( + !ctype_digit($parameters['dow']) + || $parameters['dow'] < 0 + || $parameters['dow'] > 6 + ) + ) { + $collection->addQueryParameterError('dow', 'Parameter must be a number between 0 and 6'); + } + + if ( + isset($parameters['interval']) + && ( + !ctype_digit($parameters['interval']) + || $parameters['interval'] < 0 + || $parameters['interval'] > 4 + ) + ) { + $collection->addQueryParameterError('interval', 'Parameter must be a number between 0 and 4'); + } + + if ( + isset($parameters['duration']) + && ( + !ctype_digit($parameters['duration']) + || $parameters['duration'] <= 0 + ) + ) { + $collection->addQueryParameterError('duration', 'Parameter must be a positive number'); + } + + if ( + isset($parameters['pause_time'], $parameters['duration']) + && $parameters['pause_time'] < $parameters['duration'] + ) { + $collection->addQueryParameterError('pause_time', 'The defined time to a pause is shorter than the duration of a slot.'); + } + + if (count($collection) > 0) { + throw new JsonApiException($collection); + } + } +} diff --git a/lib/classes/JsonApi/Routes/Courseware/UnitsCreate.php b/lib/classes/JsonApi/Routes/Courseware/UnitsCreate.php index e676507..d913966 100644 --- a/lib/classes/JsonApi/Routes/Courseware/UnitsCreate.php +++ b/lib/classes/JsonApi/Routes/Courseware/UnitsCreate.php @@ -100,6 +100,20 @@ class UnitsCreate extends JsonApiController 'commentable' => 0 ]); + \Courseware\Container::create([ + 'structural_element_id' => $struct->id, + 'owner_id' => $user->id, + 'editor_id' => $user->id, + 'edit_blocker_id' => '', + 'position' => 0, + 'container_type' => 'list', + + 'payload' => json_encode([ + 'colspan' => 'full', + 'sections' => [['name' => _('erstes Element'), 'icon' => '','blocks' => []]] + ]), + ]); + $unit = \Courseware\Unit::create([ 'range_id' => $range->getRangeId(), 'range_type' => $range->getRangeType(), diff --git a/lib/classes/JsonApi/Routes/Files/RangeFileRefsIndex.php b/lib/classes/JsonApi/Routes/Files/RangeFileRefsIndex.php index 8f69d6a..773071e 100644 --- a/lib/classes/JsonApi/Routes/Files/RangeFileRefsIndex.php +++ b/lib/classes/JsonApi/Routes/Files/RangeFileRefsIndex.php @@ -13,7 +13,7 @@ class RangeFileRefsIndex extends AbstractRangeIndex $filerefs = []; foreach ($filesAndFolders['files'] as $file_object) { - if (method_exists($file_object, "getFileRef")) { + if (method_exists($file_object, 'getFileRef')) { $filerefs[] = $file_object->getFileRef(); } } diff --git a/lib/classes/JsonApi/Routes/Files/SubfilerefsIndex.php b/lib/classes/JsonApi/Routes/Files/SubfilerefsIndex.php index 0ff0603..2ed1a23 100644 --- a/lib/classes/JsonApi/Routes/Files/SubfilerefsIndex.php +++ b/lib/classes/JsonApi/Routes/Files/SubfilerefsIndex.php @@ -2,6 +2,7 @@ namespace JsonApi\Routes\Files; +use FileRef; use Psr\Http\Message\ServerRequestInterface as Request; use Psr\Http\Message\ResponseInterface as Response; use JsonApi\Errors\AuthorizationFailedException; @@ -28,8 +29,14 @@ class SubfilerefsIndex extends JsonApiController throw new AuthorizationFailedException(); } - $fileRefs = $folder->file_refs->getArrayCopy(); - list($offset, $limit) = $this->getOffsetAndLimit(); + $fileRefs = array_map( + function (\FileType $file): FileRef { + return $file->getFileRef(); + }, + $folder->getFiles() + ); + + [$offset, $limit] = $this->getOffsetAndLimit(); return $this->getPaginatedContentResponse( array_slice($fileRefs, $offset, $limit), diff --git a/lib/classes/JsonApi/Routes/Files/SubfoldersIndex.php b/lib/classes/JsonApi/Routes/Files/SubfoldersIndex.php index e8f4d13..f0ad18c 100644 --- a/lib/classes/JsonApi/Routes/Files/SubfoldersIndex.php +++ b/lib/classes/JsonApi/Routes/Files/SubfoldersIndex.php @@ -19,20 +19,31 @@ class SubfoldersIndex extends JsonApiController */ public function __invoke(Request $request, Response $response, $args) { - if (!$folder = \FileManager::getTypedFolder($args['id'])) { + $folder = \FileManager::getTypedFolder($args['id']); + if (!$folder) { throw new RecordNotFoundException(); } - if (!Authority::canShowFolder($this->getUser($request), $folder)) { + $user = $this->getUser($request); + + if (!Authority::canShowFolder($user, $folder)) { throw new AuthorizationFailedException(); } - $subfolders = array_map( - function ($subfolder) { - return $subfolder->getTypedFolder(); + $subfolders = array_reduce( + $folder->subfolders->getArrayCopy(), + function ($result, $subfolder) use ($user) { + $folder = $subfolder->getTypedFolder(); + + if (Authority::canShowFolder($user, $folder)) { + $result[] = $folder; + } + + return $result; }, - $folder->subfolders->getArrayCopy() + [] ); + list($offset, $limit) = $this->getOffsetAndLimit(); return $this->getPaginatedContentResponse( diff --git a/lib/classes/JsonApi/SchemaMap.php b/lib/classes/JsonApi/SchemaMap.php index 97212bc..1498daf 100644 --- a/lib/classes/JsonApi/SchemaMap.php +++ b/lib/classes/JsonApi/SchemaMap.php @@ -2,6 +2,8 @@ namespace JsonApi; +use JsonApi\Schemas\Clipboard; + /** * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ @@ -19,6 +21,8 @@ class SchemaMap \BlubberThread::class => Schemas\BlubberThread::class, \CalendarDateAssignment::class => Schemas\CalendarDateAssignment::class, + \Clipboard::class => Schemas\Clipboard::class, + \ClipboardItem::class => Schemas\ClipboardItem::class, \ConsultationBlock::class => Schemas\ConsultationBlock::class, \ConsultationBooking::class => Schemas\ConsultationBooking::class, \ConsultationSlot::class => Schemas\ConsultationSlot::class, diff --git a/lib/classes/JsonApi/Schemas/Clipboard.php b/lib/classes/JsonApi/Schemas/Clipboard.php new file mode 100644 index 0000000..af90d73 --- /dev/null +++ b/lib/classes/JsonApi/Schemas/Clipboard.php @@ -0,0 +1,81 @@ +<?php +namespace JsonApi\Schemas; + +use Neomerx\JsonApi\Contracts\Schema\ContextInterface; +use Neomerx\JsonApi\Schema\Link; + +final class Clipboard extends SchemaProvider +{ + public const TYPE = 'clipboards'; + public const REL_USER = 'user'; + public const REL_ITEMS = 'clipboard-items'; + + /** + * @param \Clipboard $resource + */ + public function getId($resource): ?string + { + return (string) $resource->id; + } + + /** + * @param \Clipboard $resource + */ + public function getAttributes($resource, ContextInterface $context): iterable + { + return [ + 'name' => $resource->name, + 'handler' => $resource->handler, + 'allows_item_class' => $resource->allowed_item_class, + 'mkdate' => date('c', $resource->mkdate), + 'chdate' => date('c', $resource->chdate), + ]; + } + + /** + * @param \Clipboard $resource + */ + public function getRelationships($resource, ContextInterface $context): iterable + { + $relationships = []; + + $isPrimary = $context->getPosition()->getLevel() === 0; + if ($isPrimary) { + $relationships = $this->getUserRelationship($relationships, $resource, $this->shouldInclude($context, self::REL_USER)); + $relationships = $this->getItemsRelationship($relationships, $resource, $this->shouldInclude($context, self::REL_ITEMS)); + } + + + return $relationships; + } + + private function getUserRelationship(array $relationships, \Clipboard $clipboard, bool $includeData): array + { + $relationships[self::REL_USER] = [ + self::RELATIONSHIP_LINKS => [ + Link::RELATED => $this->createLinkToResource($clipboard->user), + ], + self::RELATIONSHIP_DATA => $includeData ? $clipboard->user : \User::build(['id' => $clipboard->user_id], false), + ]; + + return $relationships; + } + + private function getItemsRelationship(array $relationships, \Clipboard $clipboard, bool $includeData): array + { + if ($includeData) { + $relatedItems = $clipboard->items; + } else { + $relatedItems = $clipboard->items->map(fn($item) => \ClipboardItem::build(['id' => $item->id], false)); + } + + $relationships[self::REL_ITEMS] = [ + self::RELATIONSHIP_LINKS => [ + Link::RELATED => $this->getRelationshipRelatedLink($clipboard, self::REL_ITEMS), + ], + self::RELATIONSHIP_DATA => $relatedItems, + ]; + + return $relationships; + } +} diff --git a/lib/classes/JsonApi/Schemas/ClipboardItem.php b/lib/classes/JsonApi/Schemas/ClipboardItem.php new file mode 100644 index 0000000..9c84823 --- /dev/null +++ b/lib/classes/JsonApi/Schemas/ClipboardItem.php @@ -0,0 +1,61 @@ +<?php +namespace JsonApi\Schemas; + +use Neomerx\JsonApi\Contracts\Schema\ContextInterface; +use Neomerx\JsonApi\Schema\Link; + +final class ClipboardItem extends SchemaProvider +{ + public const TYPE = 'clipboard-items'; + public const REL_CLIPBOARD = 'clipboard'; + + /** + * @param \ClipboardItem $resource + */ + public function getId($resource): ?string + { + return (string) $resource->id; + } + + /** + * @param \ClipboardItem $resource + */ + public function getAttributes($resource, ContextInterface $context): iterable + { + return [ + 'range_id' => $resource->range_id, + 'range_type' => $resource->range_type, + 'name' => $resource->name, + 'mkdate' => date('c', $resource->mkdate), + 'chdate' => date('c', $resource->chdate), + ]; + } + + /** + * @param \ClipboardItem $resource + */ + public function getRelationships($resource, ContextInterface $context): iterable + { + $relationships = []; + + $isPrimary = $context->getPosition()->getLevel() === 0; + if ($isPrimary) { + $relationships = $this->getClipboardRelationship($relationships, $resource, $this->shouldInclude($context, self::REL_CLIPBOARD)); + } + + + return $relationships; + } + + private function getClipboardRelationship(array $relationships, \ClipboardItem $clipboardItem, bool $includeData): array + { + $relationships[self::REL_CLIPBOARD] = [ + self::RELATIONSHIP_LINKS => [ + Link::RELATED => $this->createLinkToResource($clipboardItem->clipboard), + ], + self::RELATIONSHIP_DATA => $includeData ? $clipboardItem->clipboard : \User::build(['id' => $clipboardItem->clipboard_id], false), + ]; + + return $relationships; + } +} diff --git a/lib/classes/JsonApi/Schemas/File.php b/lib/classes/JsonApi/Schemas/File.php index 8eb8046..df0263a 100644 --- a/lib/classes/JsonApi/Schemas/File.php +++ b/lib/classes/JsonApi/Schemas/File.php @@ -29,7 +29,7 @@ class File extends SchemaProvider 'chdate' => date('c', $resource['chdate']), ]; - if ($resource['metadata']['url']) { + if (!empty($resource['metadata']['url'])) { if (FilesAuthority::canUpdateFile($this->currentUser, $resource)) { $attributes['url'] = $resource['metadata']['url']; } diff --git a/lib/classes/JsonApi/Schemas/Folder.php b/lib/classes/JsonApi/Schemas/Folder.php index 2c61cae..4cb277e 100644 --- a/lib/classes/JsonApi/Schemas/Folder.php +++ b/lib/classes/JsonApi/Schemas/Folder.php @@ -169,14 +169,24 @@ class Folder extends SchemaProvider return $relationships; } + /** + * @param \FolderType $resource + */ private function getFilesRelationship(array $relationships, $resource) { + $fileRefs = array_map( + function (\FileType $file): \FileRef { + return $file->getFileRef(); + }, + $resource->getFiles() + ); + $relationships[self::REL_FILE_REFS] = [ self::RELATIONSHIP_LINKS => [ Link::RELATED => $this->getRelationshipRelatedLink($resource, self::REL_FILE_REFS), ], self::RELATIONSHIP_META => [ - 'count' => count($resource->file_refs) + 'count' => count($fileRefs), ], ]; diff --git a/lib/classes/JsonApi/Schemas/WikiPage.php b/lib/classes/JsonApi/Schemas/WikiPage.php index 857666e..f061ecc 100644 --- a/lib/classes/JsonApi/Schemas/WikiPage.php +++ b/lib/classes/JsonApi/Schemas/WikiPage.php @@ -152,7 +152,7 @@ class WikiPage extends SchemaProvider */ private function addAuthorRelationship($relationships, $wiki, $includeList) { - if ($wiki->user_id) { + if ($wiki->user_id && $wiki->user_id !== 'nobody') { $relationships[self::REL_AUTHOR] = [ self::RELATIONSHIP_LINKS => [ Link::RELATED => $this->createLinkToResource($wiki->user), |
