diff --git a/app/controllers/contents/courseware.php b/app/controllers/contents/courseware.php index e509a6bd1ec53ce049fe923bcf5afee49d2a8c3d..4df0b49996b024011390797da03e965cae9be88b 100755 --- a/app/controllers/contents/courseware.php +++ b/app/controllers/contents/courseware.php @@ -139,7 +139,7 @@ class Contents_CoursewareController extends AuthenticatedController { Navigation::activateItem('/contents/courseware/bookmarks'); $this->bookmarks = array(); - $cw_bookmarks = Courseware\Bookmark::findUsersBookmarks($this->user->id); + $cw_bookmarks = Courseware\Bookmark::findUsersBookmarks($this->user); foreach($cw_bookmarks as $bookmark) { $bm = array(); $bm['bookmark'] = $bookmark; diff --git a/lib/classes/JsonApi/RouteMap.php b/lib/classes/JsonApi/RouteMap.php index a258b10457d3377a891dbbbdad3f77179ab555af..c8ba716509a509f804b5917e03b1b3836526f8d0 100644 --- a/lib/classes/JsonApi/RouteMap.php +++ b/lib/classes/JsonApi/RouteMap.php @@ -307,6 +307,14 @@ class RouteMap ); $group->get('/courseware-instances/{id}/bookmarks', Routes\Courseware\BookmarkedStructuralElementsIndex::class); + $group->get('/users/{id}/courseware-bookmarks', Routes\Courseware\UsersBookmarkedStructuralElementsIndex::class); + $this->addRelationship( + $group, + '/users/{id}/relationships/courseware-bookmarks', + Routes\Courseware\Rel\UsersBookmarkedStructuralElements::class + ); + + $group->get('/courseware-blocks/{id}', Routes\Courseware\BlocksShow::class); $group->post('/courseware-blocks', Routes\Courseware\BlocksCreate::class); $group->patch('/courseware-blocks/{id}', Routes\Courseware\BlocksUpdate::class); diff --git a/lib/classes/JsonApi/Routes/Courseware/Authority.php b/lib/classes/JsonApi/Routes/Courseware/Authority.php index 63b32b496a9f6401b12e3607241985c42c8554e5..e83d9aaab9daaee5ab2c56789db255a0c19a7c96 100755 --- a/lib/classes/JsonApi/Routes/Courseware/Authority.php +++ b/lib/classes/JsonApi/Routes/Courseware/Authority.php @@ -189,6 +189,21 @@ class Authority return self::canShowCoursewareInstance($user, $resource); } + public static function canAddBookmarkToAUser(User $actor, User $user) + { + return $actor->id === $user->id; + } + + public static function canModifyBookmarksOfAUser(User $actor, User $user) + { + return $actor->id === $user->id; + } + + public static function canIndexBookmarksOfAUser(User $actor, User $user) + { + return $actor->id === $user->id; + } + /** * @SuppressWarnings(PHPMD.Superglobals) */ diff --git a/lib/classes/JsonApi/Routes/Courseware/BookmarkedStructuralElementsIndex.php b/lib/classes/JsonApi/Routes/Courseware/BookmarkedStructuralElementsIndex.php index 3ea220ae5dc0014b5f55eb7195d395582f151bb6..8ab546d387e8a7f49368213f9b4873424d4b21cb 100755 --- a/lib/classes/JsonApi/Routes/Courseware/BookmarkedStructuralElementsIndex.php +++ b/lib/classes/JsonApi/Routes/Courseware/BookmarkedStructuralElementsIndex.php @@ -48,6 +48,6 @@ class BookmarkedStructuralElementsIndex extends JsonApiController $total = count($resources); list($offset, $limit) = $this->getOffsetAndLimit(); - return $this->getPaginatedResponse(array_slice($resources, $offset, $limit), $total); + return $this->getPaginatedContentResponse(array_slice($resources, $offset, $limit), $total); } } diff --git a/lib/classes/JsonApi/Routes/Courseware/CoursewareInstancesHelper.php b/lib/classes/JsonApi/Routes/Courseware/CoursewareInstancesHelper.php index d22d9e3f17b1492fca3d6571595dec16b23688e5..843f7c2a902378b777be88918c1c3489c080b1c8 100755 --- a/lib/classes/JsonApi/Routes/Courseware/CoursewareInstancesHelper.php +++ b/lib/classes/JsonApi/Routes/Courseware/CoursewareInstancesHelper.php @@ -11,7 +11,10 @@ trait CoursewareInstancesHelper { private function findInstance(string $instanceId): Instance { - list($rangeType, $rangeId) = explode('_', $instanceId); + [$rangeType, $rangeId] = explode('_', $instanceId); + if (!is_string($rangeType) || !is_string($rangeId)) { + throw new BadRequestException('Invalid instance id: "' . $instanceId . '".'); + } return $this->findInstanceWithRange($rangeType, $rangeId); } diff --git a/lib/classes/JsonApi/Routes/Courseware/Rel/BookmarkedStructuralElements.php b/lib/classes/JsonApi/Routes/Courseware/Rel/BookmarkedStructuralElements.php index 7d191f53ccf407812d7429faf4175a476678d1f4..df3ede6f5a1219e50aaea4b6af576037c544d94c 100755 --- a/lib/classes/JsonApi/Routes/Courseware/Rel/BookmarkedStructuralElements.php +++ b/lib/classes/JsonApi/Routes/Courseware/Rel/BookmarkedStructuralElements.php @@ -169,6 +169,9 @@ class BookmarkedStructuralElements extends RelationshipsController private function addBookmarks(\User $user, array $newIds): void { foreach ($newIds as $structuralElementId) { + if (Bookmark::countBySQL('user_id = ? AND element_id = ?', [$user->id, $structuralElementId])) { + continue; + } Bookmark::create(['user_id' => $user->id, 'element_id' => $structuralElementId]); } } diff --git a/lib/classes/JsonApi/Routes/Courseware/Rel/UsersBookmarkedStructuralElements.php b/lib/classes/JsonApi/Routes/Courseware/Rel/UsersBookmarkedStructuralElements.php new file mode 100644 index 0000000000000000000000000000000000000000..7f05c66a64880882404dd07ed938cdea95d40412 --- /dev/null +++ b/lib/classes/JsonApi/Routes/Courseware/Rel/UsersBookmarkedStructuralElements.php @@ -0,0 +1,180 @@ +<?php + +namespace JsonApi\Routes\Courseware\Rel; + +use Courseware\Bookmark; +use Courseware\Instance; +use Courseware\StructuralElement; +use JsonApi\Errors\AuthorizationFailedException; +use JsonApi\Errors\BadRequestException; +use JsonApi\Errors\ConflictException; +use JsonApi\Errors\RecordNotFoundException; +use JsonApi\Routes\Courseware\Authority; +use JsonApi\Routes\Courseware\CoursewareInstancesHelper; +use JsonApi\Routes\RelationshipsController; +use Psr\Http\Message\ServerRequestInterface as Request; + +class UsersBookmarkedStructuralElements extends RelationshipsController +{ + use CoursewareInstancesHelper; + + protected $allowedPagingParameters = ['offset', 'limit']; + + /** + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ + protected function fetchRelationship(Request $request, $related) + { + $bookmarks = array_column(Bookmark::findUsersBookmarks($related), 'element'); + $total = count($bookmarks); + list($offset, $limit) = $this->getOffsetAndLimit(); + $page = array_slice($bookmarks, $offset, $limit); + + return $this->getPaginatedIdentifiersResponse($page, $total); + } + + protected function replaceRelationship(Request $request, $related) + { + $json = $this->validate($request); + $structuralElements = $this->validateStructuralElements($user = $this->getUser($request), $json, $related); + $this->replaceBookmarks($related, $structuralElements); + + return $this->getCodeResponse(204); + } + + protected function addToRelationship(Request $request, $related) + { + $json = $this->validate($request); + $structuralElements = $this->validateStructuralElements($user = $this->getUser($request), $json, $related); + $this->addBookmarks($related, $structuralElements); + + return $this->getCodeResponse(204); + } + + protected function removeFromRelationship(Request $request, $related) + { + $json = $this->validate($request); + $structuralElements = $this->validateStructuralElements($user = $this->getUser($request), $json, $related); + $this->removeBookmarks($user, $structuralElements); + + return $this->getCodeResponse(204); + } + + protected function findRelated(array $args) + { + if (!($related = \User::find($args['id']))) { + throw new RecordNotFoundException(); + } + + return $related; + } + + protected function authorize(Request $request, $resource) + { + $observer = $this->getUser($request); + $observed = $resource; + switch ($request->getMethod()) { + case 'GET': + return Authority::canIndexBookmarksOfAUser($observer, $observed); + + case 'DELETE': + case 'PATCH': + case 'POST': + return Authority::canModifyBookmarksOfAUser($observer, $observed); + + default: + return false; + } + } + + /** + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ + protected function getRelationshipSelfLink($resource, $schema, $userData) + { + return $schema->getRelationshipSelfLink($resource, \JsonApi\Schemas\User::REL_COURSEWARE_BOOKMARKS); + } + + /** + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ + protected function getRelationshipRelatedLink($resource, $schema, $userData) + { + return $schema->getRelationshipRelatedLink($resource, \JsonApi\Schemas\User::REL_COURSEWARE_BOOKMARKS); + } + + protected function validateResourceDocument($json, $data) + { + if (!self::arrayHas($json, 'data')) { + return 'Missing `data` member at document´s top level.'; + } + + $data = self::arrayGet($json, 'data'); + + if (!is_array($data)) { + return 'Document´s ´data´ must be an array.'; + } + + foreach ($data as $item) { + if (\JsonApi\Schemas\Courseware\StructuralElement::TYPE !== self::arrayGet($item, 'type')) { + return 'Wrong `type` in document´s `data`.'; + } + + if (!self::arrayGet($item, 'id')) { + return 'Missing `id` of document´s `data`.'; + } + } + + if (self::arrayHas($json, 'data.attributes')) { + return 'Document must not have `attributes`.'; + } + } + + private function validateStructuralElements(\User $actor, $json, \User $user) + { + $structuralElements = []; + + foreach (self::arrayGet($json, 'data') as $structuralElementResource) { + if (!($structuralElement = StructuralElement::find($structuralElementResource['id']))) { + throw new RecordNotFoundException(); + } + + if (!Authority::canModifyBookmarksOfAUser($actor, $user)) { + throw new AuthorizationFailedException(); + } + + if (!Authority::canShowStructuralElement($user, $structuralElement)) { + throw new RecordNotFoundException(); + } + + $structuralElements[] = $structuralElement->id; + } + + return $structuralElements; + } + + private function replaceBookmarks(\User $user, array $newIds) + { + $oldIds = array_column(Bookmark::findUsersBookmarks($user), 'element_id'); + $onlyInOld = array_diff($oldIds, $newIds); + $onlyInNew = array_diff($newIds, $oldIds); + + $this->removeBookmarks($user, $onlyInOld); + $this->addBookmarks($user, $onlyInNew); + } + + private function addBookmarks(\User $user, array $newIds): void + { + foreach ($newIds as $structuralElementId) { + if (Bookmark::countBySQL('user_id = ? AND element_id = ?', [$user->id, $structuralElementId])) { + continue; + } + Bookmark::create(['user_id' => $user->id, 'element_id' => $structuralElementId]); + } + } + + private function removeBookmarks(\User $user, array $oldIds): void + { + Bookmark::deleteBySQL('user_id = ? AND element_id IN (?)', [$user->id, $oldIds]); + } +} diff --git a/lib/classes/JsonApi/Routes/Courseware/UsersBookmarkedStructuralElementsIndex.php b/lib/classes/JsonApi/Routes/Courseware/UsersBookmarkedStructuralElementsIndex.php new file mode 100644 index 0000000000000000000000000000000000000000..618dc2e38e29d5a05909f3d1e61203222c192ead --- /dev/null +++ b/lib/classes/JsonApi/Routes/Courseware/UsersBookmarkedStructuralElementsIndex.php @@ -0,0 +1,55 @@ +<?php + +namespace JsonApi\Routes\Courseware; + +use Courseware\Bookmark; +use JsonApi\Errors\AuthorizationFailedException; +use JsonApi\Errors\RecordNotFoundException; +use JsonApi\JsonApiController; +use Psr\Http\Message\ResponseInterface as Response; +use Psr\Http\Message\ServerRequestInterface as Request; + +/** + * Displays the user's bookmarked structural elements. + */ +class UsersBookmarkedStructuralElementsIndex extends JsonApiController +{ + use CoursewareInstancesHelper; + + protected $allowedIncludePaths = [ + 'ancestors', + 'containers', + 'containers.blocks', + 'containers.blocks.edit-blocker', + 'containers.blocks.editor', + 'containers.blocks.owner', + 'containers.blocks.user-data-field', + 'containers.blocks.user-progress', + 'course', + 'editor', + 'owner', + 'parent', + ]; + + protected $allowedPagingParameters = ['offset', 'limit']; + + /** + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ + public function __invoke(Request $request, Response $response, $args) + { + if (!($user = \User::find($args['id']))) { + throw new RecordNotFoundException(); + } + $actor = $this->getUser($request); + if (!Authority::canIndexBookmarksOfAUser($actor, $user)) { + throw new AuthorizationFailedException(); + } + + $resources = array_column(Bookmark::findUsersBookmarks($user), 'element'); + $total = count($resources); + [$offset, $limit] = $this->getOffsetAndLimit(); + + return $this->getPaginatedContentResponse(array_slice($resources, $offset, $limit), $total); + } +} diff --git a/lib/classes/JsonApi/Schemas/User.php b/lib/classes/JsonApi/Schemas/User.php index 657f8f9c10594753135943d08cadacaf9221af44..069e09f6c7a5d0f985d15eb672dd05175af4546a 100644 --- a/lib/classes/JsonApi/Schemas/User.php +++ b/lib/classes/JsonApi/Schemas/User.php @@ -16,6 +16,7 @@ class User extends SchemaProvider const REL_CONTACTS = 'contacts'; const REL_COURSES = 'courses'; const REL_COURSE_MEMBERSHIPS = 'course-memberships'; + const REL_COURSEWARE_BOOKMARKS = 'courseware-bookmarks'; const REL_EVENTS = 'events'; const REL_FILES = 'file-refs'; const REL_FOLDERS = 'folders'; @@ -165,6 +166,8 @@ class User extends SchemaProvider $relationships = $this->getNewsRelationship($relationships, $user, $this->shouldInclude($context, self::REL_NEWS)); $relationships = $this->getOutboxRelationship($relationships, $user, $this->shouldInclude($context, self::REL_OUTBOX)); $relationships = $this->getScheduleRelationship($relationships, $user, $this->shouldInclude($context, self::REL_SCHEDULE)); + + $relationships = $this->getCoursewareBookmarksRelationship($relationships, $user, $this->shouldInclude($context, self::REL_COURSEWARE_BOOKMARKS)); } return $relationships; @@ -256,6 +259,21 @@ class User extends SchemaProvider return $relationships; } + /** + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ + private function getCoursewareBookmarksRelationship(array $relationships, \User $user, $includeData) + { + $relationships[self::REL_COURSEWARE_BOOKMARKS] = [ + self::RELATIONSHIP_LINKS_SELF => true, + self::RELATIONSHIP_LINKS => [ + Link::RELATED => $this->getRelationshipRelatedLink($user, self::REL_COURSEWARE_BOOKMARKS), + ], + ]; + + return $relationships; + } + /** * @SuppressWarnings(PHPMD.UnusedFormalParameter) */ diff --git a/lib/models/Courseware/Bookmark.php b/lib/models/Courseware/Bookmark.php index d6e5910521d80dd0ecc7bc8e9adfbfdaa3f8eea0..e11954825cc6d656d78ad9de00cf987b7acd9ca4 100755 --- a/lib/models/Courseware/Bookmark.php +++ b/lib/models/Courseware/Bookmark.php @@ -54,12 +54,12 @@ class Bookmark extends \SimpleORMap /** * Returns all bookmarks of a user. * - * @param string $userId the user's ID for whom to search for bookmarks + * @param \User $user the user for whom to search for bookmarks * * @return Bookmark[] the list of bookmarks */ - public function findUsersBookmarks(string $userId): array + public static function findUsersBookmarks($user): array { - return self::findBySQL('user_id = ?', [$userId]); + return self::findBySQL('user_id = ? ORDER BY chdate', [$user->id]); } }