aboutsummaryrefslogtreecommitdiff
path: root/lib/classes/JsonApi
diff options
context:
space:
mode:
Diffstat (limited to 'lib/classes/JsonApi')
-rw-r--r--lib/classes/JsonApi/JsonApiController.php6
-rw-r--r--lib/classes/JsonApi/Middlewares/Auth/OAuth1Strategy.php114
-rw-r--r--lib/classes/JsonApi/Middlewares/Authentication.php44
-rw-r--r--lib/classes/JsonApi/Middlewares/StudipMockNavigation.php27
-rw-r--r--lib/classes/JsonApi/NonJsonApiController.php4
-rw-r--r--lib/classes/JsonApi/RouteMap.php27
-rw-r--r--lib/classes/JsonApi/Routes/Clipboards/Authority.php28
-rw-r--r--lib/classes/JsonApi/Routes/Clipboards/ClipboardItemsCreate.php106
-rw-r--r--lib/classes/JsonApi/Routes/Clipboards/ClipboardItemsDelete.php54
-rw-r--r--lib/classes/JsonApi/Routes/Clipboards/ClipboardItemsShow.php28
-rw-r--r--lib/classes/JsonApi/Routes/Clipboards/ClipboardsCreate.php46
-rw-r--r--lib/classes/JsonApi/Routes/Clipboards/ClipboardsDelete.php31
-rw-r--r--lib/classes/JsonApi/Routes/Clipboards/ClipboardsUpdate.php50
-rw-r--r--lib/classes/JsonApi/Routes/Consultations/SlotCreationCount.php105
-rw-r--r--lib/classes/JsonApi/Routes/Courseware/UnitsCreate.php14
-rw-r--r--lib/classes/JsonApi/Routes/Files/RangeFileRefsIndex.php2
-rw-r--r--lib/classes/JsonApi/Routes/Files/SubfilerefsIndex.php11
-rw-r--r--lib/classes/JsonApi/Routes/Files/SubfoldersIndex.php23
-rw-r--r--lib/classes/JsonApi/SchemaMap.php4
-rw-r--r--lib/classes/JsonApi/Schemas/Clipboard.php81
-rw-r--r--lib/classes/JsonApi/Schemas/ClipboardItem.php61
-rw-r--r--lib/classes/JsonApi/Schemas/File.php2
-rw-r--r--lib/classes/JsonApi/Schemas/Folder.php12
-rw-r--r--lib/classes/JsonApi/Schemas/WikiPage.php2
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),