Skip to content
Snippets Groups Projects
Commit b11152c9 authored by Ron Lucke's avatar Ron Lucke
Browse files

StEP00363: Externer Ansicht als Link für Courseware-Seiten

Closes #918

Merge request studip/studip!638
parent 573bbd21
Branches
No related tags found
No related merge requests found
Showing
with 645 additions and 15 deletions
......@@ -185,6 +185,20 @@ class Contents_CoursewareController extends AuthenticatedController
$this->setBookmarkSidebar();
}
/**
* Show users releases
*
* @SuppressWarnings(PHPMD.CamelCaseMethodName)
* @SuppressWarnings(PHPMD.Superglobals)
* @SuppressWarnings(PHPMD.UnusedFormalParameter)
*/
public function releases_action()
{
Navigation::activateItem('/contents/courseware/releases');
$this->user_id = $GLOBALS['user']->id;
}
private function setBookmarkSidebar()
{
$sidebar = Sidebar::Get();
......
<?php
use Courseware\PublicLink;
class Courseware_PublicController extends StudipController
{
public function before_filter(&$action, &$args)
{
parent::before_filter($action, $args);
PageLayout::setTitle(_('Courseware'));
PageLayout::setHelpKeyword('Basis.Courseware');
}
public function index_action()
{
$this->invalid = true;
$this->link_id = Request::option('link');
if ($this->link_id) {
$publicLink = PublicLink::find($this->link_id);
$this->invalid = $publicLink === null;
if (!$this->invalid) {
$this->expired = $publicLink->isExpired();
$this->link_pass = $publicLink->password;
$this->entry_element_id = $publicLink->structural_element_id;
}
}
}
}
<div
id="courseware-content-releases-app"
entry-type="users"
entry-id="<?= htmlReady($user_id) ?>"
>
</div>
<? if (!$expired && !$invalid): ?>
<div
id="courseware-public-app"
link-id="<?= htmlReady($link_id) ?>"
link-pass="<?= htmlReady($link_pass) ?>"
entry-type="public"
entry-element-id="<?= htmlReady($entry_element_id) ?>"
>
</div>
<? endif; ?>
<? if ($expired): ?>
<?= MessageBox::warning(_('Der Link zu dieser Seite ist abgelaufen.'))->hideClose() ?>
<? endif; ?>
<? if ($invalid): ?>
<?= MessageBox::error(_('Es wurde kein gültiger Link aufgerufen.'))->hideClose() ?>
<? endif; ?>
<?php
final class AddCoursewarePublicLinks extends Migration
{
public function description()
{
return 'Create Courseware public links database table';
}
public function up()
{
\DBManager::get()->exec("CREATE TABLE `cw_public_links` (
`id` char(32) CHARACTER SET latin1 COLLATE latin1_bin NOT NULL,
`user_id` char(32) CHARACTER SET latin1 COLLATE latin1_bin NOT NULL,
`structural_element_id` int(11) NOT NULL,
`password` varbinary(64) NOT NULL,
`expire_date` int(11) NOT NULL,
`mkdate` int(11) NOT NULL,
`chdate` int(11) NOT NULL,
PRIMARY KEY (`id`),
INDEX index_user_id (`user_id`),
INDEX index_structural_element_id (`structural_element_id`)
)
");
}
public function down()
{
\DBManager::get()->exec("DROP TABLE IF EXISTS `cw_public_links`");
}
}
......@@ -143,6 +143,9 @@ class RouteMap
$group->get('/semesters/{id}', Routes\SemestersShow::class)->setName('get-semester');
$group->get('/studip/properties', Routes\Studip\PropertiesIndex::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);
}
private function getAuthenticator(): callable
......@@ -449,6 +452,12 @@ class RouteMap
$group->post('/courseware-templates', Routes\Courseware\TemplatesCreate::class);
$group->patch('/courseware-templates/{id}', Routes\Courseware\TemplatesUpdate::class);
$group->delete('/courseware-templates/{id}', Routes\Courseware\TemplatesDelete::class);
$group->get('/courseware-public-links/{id}', Routes\Courseware\PublicLinksShow::class);
$group->get('/courseware-public-links', Routes\Courseware\PublicLinksIndex::class);
$group->post('/courseware-public-links', Routes\Courseware\PublicLinksCreate::class);
$group->patch('/courseware-public-links/{id}', Routes\Courseware\PublicLinksUpdate::class);
$group->delete('/courseware-public-links/{id}', Routes\Courseware\PublicLinksDelete::class);
}
private function addAuthenticatedFilesRoutes(RouteCollectorProxy $group): void
......
......@@ -14,6 +14,7 @@ use Courseware\TaskGroup;
use Courseware\Template;
use Courseware\UserDataField;
use Courseware\UserProgress;
use Courseware\PublicLink;
use User;
/**
......@@ -427,4 +428,36 @@ class Authority
return self::canCreateTemplate($user);
}
public static function canIndexPublicLinks(User $user): bool
{
return self::canCreatePublicLink($user);
}
public static function canShowPublicLink(User $user, PublicLink $resource): bool
{
return self::canUpdatePublicLink($user, $resource);
}
public static function canCreatePublicLink(User $user): bool
{
return true;
}
public static function canUpdatePublicLink(User $user, PublicLink $resource): bool
{
return $resource->user_id === $user->id;
}
public static function canDeletePublicLink(User $user, PublicLink $resource): bool
{
return self::canUpdatePublicLink($user, $resource);
}
public static function canShowPublicStructuralElement(StructuralElement $resource): bool
{
$publicLink = PublicLink::findOneBySQL('structural_element_id = ?', [$resource->id]);
return (bool) $publicLink;
}
}
<?php
namespace JsonApi\Routes\Courseware;
use Courseware\PublicLink;
use JsonApi\Errors\AuthorizationFailedException;
use JsonApi\JsonApiController;
use JsonApi\Routes\TimestampTrait;
use JsonApi\Routes\ValidationTrait;
use Psr\Http\Message\ResponseInterface as Response;
use Psr\Http\Message\ServerRequestInterface as Request;
/**
* Create a Template.
*/
class PublicLinksCreate extends JsonApiController
{
use TimestampTrait;
use ValidationTrait;
/**
* @SuppressWarnings(PHPMD.UnusedFormalParameter)
*/
public function __invoke(Request $request, Response $response, $args)
{
$json = $this->validate($request);
if (!Authority::canCreatePublicLink($user = $this->getUser($request))) {
throw new AuthorizationFailedException();
}
$publicLink = $this->createPublicLink($json, $user);
return $this->getCreatedResponse($publicLink);
}
/**
* @SuppressWarnings(PHPMD.UnusedFormalParameters)
*/
protected function validateResourceDocument($json, $data)
{
if (!self::arrayHas($json, 'data')) {
return 'Missing `data` member at document´s top level.';
}
if (!self::arrayHas($json, 'data.relationships.structural-element.data.id')) {
return 'Missing `structural-element-id` value.';
}
}
private function createPublicLink(array $json, $user): PublicLink
{
$get = function ($key, $default = '') use ($json) {
return self::arrayGet($json, $key, $default);
};
$publicLink = new PublicLink();
$publicLink->setId($publicLink->getNewId());
$publicLink->user_id = $user->id;
$publicLink->structural_element_id = $get('data.relationships.structural-element.data.id');
$publicLink->password = str_replace(' ', '', $get('data.attributes.password'));
$expire_date = $get('data.attributes.expire-date');
$expireDate = self::fromISO8601($expire_date);
$publicLink->expire_date = $expireDate->getTimestamp();
$publicLink->store();
return $publicLink;
}
}
<?php
namespace JsonApi\Routes\Courseware;
use Courseware\PublicLink;
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;
/**
* Delete one PublicLink.
*/
class PublicLinksDelete extends JsonApiController
{
/**
* @SuppressWarnings(PHPMD.UnusedFormalParameter)
*/
public function __invoke(Request $request, Response $response, $args)
{
$resource = PublicLink::find($args['id']);
if (!$resource) {
throw new RecordNotFoundException();
}
if (!Authority::canDeletePublicLink($user = $this->getUser($request), $resource)) {
throw new AuthorizationFailedException();
}
$resource->delete();
return $this->getCodeResponse(204);
}
}
<?php
namespace JsonApi\Routes\Courseware;
use Courseware\PublicLink;
use JsonApi\Errors\AuthorizationFailedException;
use JsonApi\JsonApiController;
use Psr\Http\Message\ResponseInterface as Response;
use Psr\Http\Message\ServerRequestInterface as Request;
/**
* Displays all PublicLinks
*/
class PublicLinksIndex extends JsonApiController
{
protected $allowedIncludePaths = ['structural-element'];
/**
* @SuppressWarnings(PHPMD.UnusedFormalParameter)
*/
public function __invoke(Request $request, Response $response, $args)
{
$user = $this->getUser($request);
if (!Authority::canIndexPublicLinks($user)) {
throw new AuthorizationFailedException();
}
$resources = PublicLink::findBySQL('user_id = ? ORDER BY mkdate', [$user->id]);
return $this->getContentResponse($resources);
}
}
\ No newline at end of file
<?php
namespace JsonApi\Routes\Courseware;
use Courseware\PublicLink;
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 one PublicLink.
*/
class PublicLinksShow extends JsonApiController
{
protected $allowedIncludePaths = ['structural-element'];
/**
* @SuppressWarnings(PHPMD.UnusedFormalParameter)
*/
public function __invoke(Request $request, Response $response, $args)
{
$resource = PublicLink::find($args['id']);
if (!$resource) {
throw new RecordNotFoundException();
}
if (!Authority::canShowPublicLink($this->getUser($request), $resource)) {
throw new AuthorizationFailedException();
}
return $this->getContentResponse($resource);
}
}
<?php
namespace JsonApi\Routes\Courseware;
use Courseware\PublicLink;
use JsonApi\Errors\AuthorizationFailedException;
use JsonApi\Errors\RecordNotFoundException;
use JsonApi\Errors\UnprocessableEntityException;
use JsonApi\JsonApiController;
use JsonApi\Routes\TimestampTrait;
use JsonApi\Routes\ValidationTrait;
use Psr\Http\Message\ResponseInterface as Response;
use Psr\Http\Message\ServerRequestInterface as Request;
/**
* Update one PublicLink.
*/
class PublicLinksUpdate extends JsonApiController
{
use TimestampTrait;
use ValidationTrait;
/**
* @SuppressWarnings(PHPMD.UnusedFormalParameter)
*/
public function __invoke(Request $request, Response $response, $args)
{
$resource = PublicLink::find($args['id']);
if (!$resource) {
throw new RecordNotFoundException();
}
$json = $this->validate($request, $resource);
if (!Authority::canUpdatePublicLink($user = $this->getUser($request), $resource)) {
throw new AuthorizationFailedException();
}
$resource = $this->updatePublicLink($resource, $json);
return $this->getContentResponse($resource);
}
/**
* @SuppressWarnings(PHPMD.UnusedFormalParameters)
*/
protected function validateResourceDocument($json, $data)
{
if (!self::arrayHas($json, 'data')) {
return 'Missing `data` member at document´s top level.';
}
if (!self::arrayHas($json, 'data.id')) {
return 'Document must have an `id`.';
}
if (self::arrayHas($json, 'data.attributes.expire-date')) {
$expire_date = self::arrayGet($json, 'data.attributes.expire-date');
if (!self::isValidTimestamp($expire_date)) {
return '`expire-date` is not an ISO 8601 timestamp.';
}
}
}
private function updatePublicLink(PublicLink $resource, array $json): PublicLink
{
$get = function ($key, $default = '') use ($json) {
return self::arrayGet($json, $key, $default);
};
$resource->password = $get('data.attributes.password');
$expire_date = $get('data.attributes.expire-date');
$expireDate = self::fromISO8601($expire_date);
$resource->expire_date = $expireDate->getTimestamp();
$resource->store();
return $resource;
}
}
<?php
namespace JsonApi\Routes\Courseware;
use Courseware\StructuralElement;
use Courseware\PublicLink;
use JsonApi\Errors\AuthorizationFailedException;
use JsonApi\Errors\RecordNotFoundException;
use JsonApi\JsonApiController;
use Neomerx\JsonApi\Contracts\Http\ResponsesInterface;
use Psr\Http\Message\ResponseInterface as Response;
use Psr\Http\Message\ServerRequestInterface as Request;
/**
* Displays one StructuralElement.
*/
class PublicStructuralElementsIndex extends JsonApiController
{
protected $allowedIncludePaths = [];
/**
* @SuppressWarnings(PHPMD.UnusedFormalParameter)
*/
public function __invoke(Request $request, Response $response, $args)
{
$publicLink = PublicLink::find($args['link_id']);
if (!$publicLink) {
throw new AuthorizationFailedException();
}
$root = StructuralElement::find($publicLink->structural_element_id);
if (!$root) {
throw new RecordNotFoundException();
}
if (!$publicLink->canVisitElement($root)) {
throw new AuthorizationFailedException();
}
$resources = array_merge([$root], $root->findDescendants());
return $this->getContentResponse($resources);
}
}
\ No newline at end of file
<?php
namespace JsonApi\Routes\Courseware;
use Courseware\StructuralElement;
use Courseware\PublicLink;
use JsonApi\Errors\AuthorizationFailedException;
use JsonApi\Errors\RecordNotFoundException;
use JsonApi\JsonApiController;
use Neomerx\JsonApi\Contracts\Http\ResponsesInterface;
use Psr\Http\Message\ResponseInterface as Response;
use Psr\Http\Message\ServerRequestInterface as Request;
/**
* Displays one StructuralElement.
*/
class PublicStructuralElementsShow extends JsonApiController
{
protected $allowedIncludePaths = [
'children',
'containers',
'containers.blocks',
'course',
'parent',
];
/**
* @SuppressWarnings(PHPMD.UnusedFormalParameter)
*/
public function __invoke(Request $request, Response $response, $args)
{
$resource = StructuralElement::find($args['id']);
$publicLink = PublicLink::find($args['link_id']);
if (!$publicLink) {
throw new AuthorizationFailedException();
}
/** @var ?StructuralElement $resource*/
if (!$resource) {
throw new RecordNotFoundException();
}
if (!$publicLink->canVisitElement($resource)) {
throw new AuthorizationFailedException();
}
$meta = [];
return $this->getContentResponse($resource, ResponsesInterface::HTTP_OK, [], $meta);
}
}
......@@ -65,6 +65,7 @@ class SchemaMap
\Courseware\TaskGroup::class => Schemas\Courseware\TaskGroup::class,
\Courseware\TaskFeedback::class => Schemas\Courseware\TaskFeedback::class,
\Courseware\Template::class => Schemas\Courseware\Template::class,
\Courseware\PublicLink::class => Schemas\Courseware\PublicLink::class,
];
}
}
......@@ -101,21 +101,23 @@ class Block extends SchemaProvider
];
$user = $this->currentUser;
$userDataField = UserDataField::getUserDataField($user, $resource);
$relationships[self::REL_USERDATAFIELD] = [
self::RELATIONSHIP_LINKS => [
Link::RELATED => $this->getRelationshipRelatedLink($resource, self::REL_USERDATAFIELD),
],
self::RELATIONSHIP_DATA => $userDataField,
];
$userProgress = UserProgress::getUserProgress($user, $resource);
$relationships[self::REL_USERPROGRESS] = [
self::RELATIONSHIP_LINKS => [
Link::RELATED => $this->getRelationshipRelatedLink($resource, self::REL_USERPROGRESS),
],
self::RELATIONSHIP_DATA => $userProgress,
];
if ($user) {
$userDataField = UserDataField::getUserDataField($user, $resource);
$relationships[self::REL_USERDATAFIELD] = [
self::RELATIONSHIP_LINKS => [
Link::RELATED => $this->getRelationshipRelatedLink($resource, self::REL_USERDATAFIELD),
],
self::RELATIONSHIP_DATA => $userDataField,
];
$userProgress = UserProgress::getUserProgress($user, $resource);
$relationships[self::REL_USERPROGRESS] = [
self::RELATIONSHIP_LINKS => [
Link::RELATED => $this->getRelationshipRelatedLink($resource, self::REL_USERPROGRESS),
],
self::RELATIONSHIP_DATA => $userProgress,
];
}
if ($resource->files) {
$filesLink = $this->getRelationshipRelatedLink($resource, self::REL_FILES);
......
<?php
namespace JsonApi\Schemas\Courseware;
use JsonApi\Schemas\SchemaProvider;
use Neomerx\JsonApi\Contracts\Schema\ContextInterface;
use Neomerx\JsonApi\Schema\Link;
class PublicLink extends SchemaProvider
{
const TYPE = 'courseware-public-links';
const REL_STRUCTURAL_ELEMENT = 'structural-element';
/**
* {@inheritdoc}
*/
public function getId($resource): ?string
{
return $resource->id;
}
/**
* {@inheritdoc}
*/
public function getAttributes($resource, ContextInterface $context): array
{
return [
'password' => $resource['password'],
'expire-date' => $resource['expire_date'] ? date('Y-m-d', $resource['expire_date']) : null,
'mkdate' => date('c', $resource['mkdate']),
'chdate' => date('c', $resource['chdate']),
];
}
/**
* {@inheritdoc}
*/
public function getRelationships($resource, ContextInterface $context): iterable
{
$relationships = [];
$relationships[self::REL_STRUCTURAL_ELEMENT] = $resource['structural_element_id']
? [
self::RELATIONSHIP_LINKS => [
Link::RELATED => $this->createLinkToResource($resource['structural_element']),
],
self::RELATIONSHIP_DATA => $resource['structural_element'],
]
: [self::RELATIONSHIP_DATA => null];
return $relationships;
}
}
<?php
namespace Courseware;
/**
* Courseware's template.
*
* @author Ron Lucke <lucke@elan-ev.de>
* @license GPL2 or any later version
*
* @since Stud.IP 5.2
*
* @property string $id database column
* @property int $structural_element_id database column
* @property string $password database column
* @property int $expire_date database column
* @property int $mkdate database column
* @property int $chdate database column
*/
class PublicLink extends \SimpleORMap
{
protected static function configure($config = [])
{
$config['db_table'] = 'cw_public_links';
$config['belongs_to']['structural_element'] = [
'class_name' => StructuralElement::class,
'foreign_key' => 'structural_element_id',
];
parent::configure($config);
}
public function canVisitElement(StructuralElement $structuralElement): bool
{
if (!$structuralElement) {
return false;
}
if ($structuralElement->isRootNode()) {
return $this->structural_element_id === $structuralElement->id;
}
if ($this->structural_element_id === $structuralElement->id) {
return true;
}
return $this->canVisitElement($structuralElement->parent);
}
public function isExpired(): bool
{
if (!$this->expire_date) {
return false;
}
return time() > $this->expire_date;
}
}
......@@ -59,6 +59,10 @@ class ContentsNavigation extends Navigation
'courseware_manager',
new Navigation(_('Verwaltung persönlicher Lernmaterialien'), 'dispatch.php/contents/courseware/courseware_manager')
);
$courseware->addSubNavigation(
'releases',
new Navigation(_('Freigaben'), 'dispatch.php/contents/courseware/releases')
);
$courseware->addSubNavigation(
'bookmarks',
new Navigation(_('Lesezeichen'), 'dispatch.php/contents/courseware/bookmarks')
......
......@@ -64,4 +64,26 @@ STUDIP.domReady(() => {
});
});
}
if (document.getElementById('courseware-public-app')) {
STUDIP.Vue.load().then(({ createApp }) => {
import(
/* webpackChunkName: "courseware-public-app" */
'@/vue/courseware-public-app.js'
).then(({ default: mountApp }) => {
return mountApp(STUDIP, createApp, '#courseware-public-app');
});
});
}
if (document.getElementById('courseware-content-releases-app')) {
STUDIP.Vue.load().then(({ createApp }) => {
import(
/* webpackChunkName: "courseware-content-links-app" */
'@/vue/courseware-content-releases-app.js'
).then(({ default: mountApp }) => {
return mountApp(STUDIP, createApp, '#courseware-content-releases-app');
});
});
}
});
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment