From 75d7b6835e97624ab1e27597df904fdd167f94c3 Mon Sep 17 00:00:00 2001 From: Sebastian Rettig Date: Sat, 15 Jul 2023 14:38:38 +0200 Subject: [PATCH] feat!: impersonate users (#3042) * feat: added contextId to user data classes * feat: contextId in H5PPlayer.play * feat: contextId in h5p-express and docs * test: added integration tests for contextId * test: contextId in rest example * feat(mongo-s3): added contextId to user data storage * refactor: permission system * refactor: permissions system * test: fix tests * test: fix more tests * test: fix more tests * refactor!: moved permissions from IUser to IPermissionSystem * refactor: permission system * refactor: fine grained permission types and REST example * refactor: prettier * refactor: more fine grained user data permission handling * feat: impersonate users and read only states * test: fixed test for new content user data interface * refactor: style improvement and docs * feat: add locales * test: fix tests * docs: extended docs * test: added test for impersonation * test: added impersonation and read only state to REST example --- docs/SUMMARY.md | 4 +- docs/advanced/authorization.md | 17 + docs/advanced/impersonation.md | 30 ++ docs/usage/h5p-editor-constructor.md | 10 + packages/h5p-examples/src/User.ts | 6 - packages/h5p-examples/src/expressRoutes.ts | 19 ++ .../ContentUserDataController.ts | 37 ++- .../test/ContentUserDataRouter.test.ts | 14 +- packages/h5p-express/test/User.ts | 6 - .../test/HtmlExporter.test.ts | 15 +- packages/h5p-html-exporter/test/User.ts | 6 - .../src/MongoContentUserDataStorage.ts | 11 +- .../h5p-mongos3/src/MongoS3ContentStorage.ts | 185 +---------- .../h5p-mongos3/src/S3TemporaryFileStorage.ts | 105 +------ .../test/MongoS3ContentStorage.test.ts | 14 +- .../test/S3TemporaryFileStorage.test.ts | 11 - packages/h5p-mongos3/test/User.ts | 6 - packages/h5p-react/src/H5PPlayerUI.tsx | 22 +- packages/h5p-rest-example-client/src/App.tsx | 29 +- .../components/ContentListEntryComponent.tsx | 126 +++++++- .../src/components/Login.tsx | 14 +- .../src/services/ContentService.ts | 33 +- packages/h5p-rest-example-server/.eslintrc.js | 4 +- .../src/ExamplePermissionSystem.ts | 147 +++++++++ .../src/ExampleUser.ts | 19 ++ packages/h5p-rest-example-server/src/User.ts | 23 -- .../src/createH5PEditor.ts | 4 +- packages/h5p-rest-example-server/src/index.ts | 46 ++- .../src/indexSharedState.ts | 85 +++-- .../h5p-rest-example-server/src/routes.ts | 97 ++++-- .../mongo-s3-content-storage/bg.json | 4 - .../mongo-s3-content-storage/bs.json | 4 - .../mongo-s3-content-storage/ca.json | 4 - .../mongo-s3-content-storage/cs.json | 4 - .../mongo-s3-content-storage/de.json | 4 - .../mongo-s3-content-storage/el.json | 4 - .../mongo-s3-content-storage/en.json | 4 - .../mongo-s3-content-storage/es.json | 4 - .../mongo-s3-content-storage/et.json | 4 - .../mongo-s3-content-storage/eu.json | 4 - .../mongo-s3-content-storage/fi.json | 4 - .../mongo-s3-content-storage/fr.json | 4 - .../mongo-s3-content-storage/gl.json | 4 - .../mongo-s3-content-storage/it.json | 4 - .../mongo-s3-content-storage/ja.json | 4 - .../mongo-s3-content-storage/km.json | 4 - .../mongo-s3-content-storage/ko.json | 4 - .../mongo-s3-content-storage/nl.json | 4 - .../mongo-s3-content-storage/pt.json | 4 - .../mongo-s3-content-storage/ru.json | 4 - .../mongo-s3-content-storage/sl.json | 4 - .../mongo-s3-content-storage/sv.json | 4 - .../mongo-s3-content-storage/te.json | 4 - .../mongo-s3-content-storage/tr.json | 4 - .../mongo-s3-content-storage/zh.json | 4 - .../translations/s3-temporary-storage/bg.json | 3 - .../translations/s3-temporary-storage/bs.json | 3 - .../translations/s3-temporary-storage/ca.json | 3 - .../translations/s3-temporary-storage/cs.json | 3 - .../translations/s3-temporary-storage/de.json | 3 - .../translations/s3-temporary-storage/el.json | 3 - .../translations/s3-temporary-storage/en.json | 3 - .../translations/s3-temporary-storage/es.json | 3 - .../translations/s3-temporary-storage/et.json | 3 - .../translations/s3-temporary-storage/eu.json | 3 - .../translations/s3-temporary-storage/fi.json | 3 - .../translations/s3-temporary-storage/fr.json | 3 - .../translations/s3-temporary-storage/gl.json | 3 - .../translations/s3-temporary-storage/it.json | 3 - .../translations/s3-temporary-storage/ja.json | 3 - .../translations/s3-temporary-storage/km.json | 3 - .../translations/s3-temporary-storage/ko.json | 3 - .../translations/s3-temporary-storage/nl.json | 3 - .../translations/s3-temporary-storage/pt.json | 3 - .../translations/s3-temporary-storage/ru.json | 3 - .../translations/s3-temporary-storage/sl.json | 3 - .../translations/s3-temporary-storage/sv.json | 3 - .../translations/s3-temporary-storage/tr.json | 3 - .../translations/s3-temporary-storage/zh.json | 3 - .../assets/translations/server/bg.json | 15 +- .../assets/translations/server/bs.json | 15 +- .../assets/translations/server/ca.json | 15 +- .../assets/translations/server/cs.json | 15 +- .../assets/translations/server/de.json | 17 +- .../assets/translations/server/el.json | 15 +- .../assets/translations/server/en.json | 27 +- .../assets/translations/server/es.json | 15 +- .../assets/translations/server/et.json | 15 +- .../assets/translations/server/eu.json | 15 +- .../assets/translations/server/fi.json | 15 +- .../assets/translations/server/fr.json | 15 +- .../assets/translations/server/gl.json | 15 +- .../assets/translations/server/it.json | 15 +- .../assets/translations/server/ja.json | 15 +- .../assets/translations/server/km.json | 15 +- .../assets/translations/server/ko.json | 15 +- .../assets/translations/server/nl.json | 15 +- .../assets/translations/server/pt.json | 15 +- .../assets/translations/server/ru.json | 15 +- .../assets/translations/server/sl.json | 15 +- .../assets/translations/server/sv.json | 15 +- .../assets/translations/server/tr.json | 15 +- .../assets/translations/server/zh.json | 15 +- packages/h5p-server/src/ContentManager.ts | 245 +++++++++++++-- packages/h5p-server/src/ContentStorer.ts | 12 +- .../src/ContentTypeInformationRepository.ts | 58 +++- .../h5p-server/src/ContentUserDataManager.ts | 296 +++++++++++++++--- packages/h5p-server/src/H5PAjaxEndpoint.ts | 4 +- packages/h5p-server/src/H5PEditor.ts | 14 +- packages/h5p-server/src/H5PPlayer.ts | 123 +++++--- packages/h5p-server/src/PackageExporter.ts | 31 +- packages/h5p-server/src/PackageImporter.ts | 16 +- .../h5p-server/src/TemporaryFileManager.ts | 98 +++++- packages/h5p-server/src/UrlGenerator.ts | 32 +- .../LaissezFairePermissionSystem.ts | 42 +++ .../fs/DirectoryTemporaryFileStorage.ts | 13 +- .../implementation/fs/FileContentStorage.ts | 21 -- .../fs/FileContentUserDataStorage.ts | 8 +- packages/h5p-server/src/index.ts | 14 +- packages/h5p-server/src/types.ts | 166 +++++++--- .../test/ContentFileScanner.test.ts | 7 +- .../h5p-server/test/ContentManager.test.ts | 20 +- .../h5p-server/test/ContentScanner.test.ts | 7 +- .../ContentTypeInformationRepository.test.ts | 72 ++++- .../test/ContentUserDataManager.test.ts | 85 +++-- .../h5p-server/test/H5PPlayer.render.test.ts | 179 ++++++++++- .../h5p-server/test/PackageExporter.test.ts | 8 +- .../h5p-server/test/PackageImporter.test.ts | 28 +- .../test/TemporaryFileManager.test.ts | 13 +- packages/h5p-server/test/User.ts | 6 - .../test/__mocks__/ContentUserDataStorage.ts | 8 +- .../implementation/ContentUserDataStorage.ts | 53 ++-- .../integration/ContentFileScanner.test.ts | 8 +- packages/h5p-webcomponents/src/h5p-player.ts | 86 ++++- 134 files changed, 2441 insertions(+), 1023 deletions(-) create mode 100644 docs/advanced/authorization.md create mode 100644 docs/advanced/impersonation.md create mode 100644 packages/h5p-rest-example-server/src/ExamplePermissionSystem.ts create mode 100644 packages/h5p-rest-example-server/src/ExampleUser.ts delete mode 100644 packages/h5p-rest-example-server/src/User.ts create mode 100644 packages/h5p-server/src/implementation/LaissezFairePermissionSystem.ts diff --git a/docs/SUMMARY.md b/docs/SUMMARY.md index 7243bf848..3fcb6194e 100644 --- a/docs/SUMMARY.md +++ b/docs/SUMMARY.md @@ -7,7 +7,10 @@ * [Constructing H5PEditor](usage/h5p-editor-constructor.md) * [REST Example](examples/rest/README.md) - Advanced usage + * [Authorization](advanced/authorization.md) * [User content state](advanced/user-content-state.md) + * [Multiple user states per object](advanced/context-ids.md) + * [Impersonating users](advanced/impersonation.md) * [Basic completion tracking](advanced/completion-tracking.md) * [Localization](advanced/localization.md) * [Cluster](advanced/cluster.md) @@ -16,7 +19,6 @@ * [Performance optimizations](advanced/performance-optimizations.md) * [Privacy](advanced/privacy.md) * [Forward proxy support](advanced/proxy.md) - * [Multiple content states per object](advanced/context-ids.md) - NPM packages - h5p-mongos3 * [Mongo/S3 Content Storage](packages/h5p-mongos3/mongo-s3-content-storage.md) diff --git a/docs/advanced/authorization.md b/docs/advanced/authorization.md new file mode 100644 index 000000000..34e0d2acc --- /dev/null +++ b/docs/advanced/authorization.md @@ -0,0 +1,17 @@ +# Authorization + +Many actions users perform in the H5P system need authorization. By default the +library will allow everything to every user. You can customize who can do what, +but passing in an implementation of `IPermissionSystem` into +`options.permissionSystem` of the `H5PPlayer` or `H5PEditor` constructor. The +library then calls the methods of `IPermissionSystem` whenever a user performs an +action that requires authorization. + +See the documentation of `IPermissionSystem` for and the +[`ExamplePermissionSystem`](/packages/h5p-rest-example-server/src/ExamplePermissionSystem.ts) +for reference how to implement the permission system. + +Note that the `IPermissionSystem` is a generic. You can use any sub-type of +`IUser` as the generic type. The call of the methods of `IPermissionSystem` will +include a user of the generic type. This is the user object you've injected in +your controllers. That means you can add any arbitrary date to it, like roles. diff --git a/docs/advanced/impersonation.md b/docs/advanced/impersonation.md new file mode 100644 index 000000000..23fc83af8 --- /dev/null +++ b/docs/advanced/impersonation.md @@ -0,0 +1,30 @@ +# Impersonating users + +It is possible to impersonate users when viewing a H5P object. This means that +you can display another user's user state instead of your own. This is useful, +if you want to implement a feature in which teachers can review the work of +students. + +You do this by setting `options.asUserId` of the `H5PPlayer.render` method. Make +sure that you [authorize users](authorization.md) as required in the permission +system. + +## Read-only states + +In most cases in which your users impersonate another user, you'll want to +disable saving the user state for the impersonator. You can do this by setting +`options.readOnlyState` to true when calling `H5PPlayer.render`. This will do +the following: + +- set the save interval to the longest possible value +- adds the query parameter `ignorePost=yes` to the Ajax route responsible for + handling user states + +The query parameter is necessary, as the H5P core client doesn't support user +states that are read only. We work around this by ignoring a post calls when the +query parameter is set in h5p-express. If the query parameter is set, we simply +return a success, so the H5P core client doesn't realize we didn't save the +state. If you don't use this package, you must do this yourself. + +Obviously you also have to make sure malicious users won't change the user state +of others, by rejecting these operations in the authorization/permission system! diff --git a/docs/usage/h5p-editor-constructor.md b/docs/usage/h5p-editor-constructor.md index 74bb794f9..adc604910 100644 --- a/docs/usage/h5p-editor-constructor.md +++ b/docs/usage/h5p-editor-constructor.md @@ -115,6 +115,16 @@ for an implementation sample using the urlGenerator. Allows you to customize styles and scripts of the client. Also allows passing in a lock implementation (needed for multi-process or clustered setups). +## options.permissionSystem (optional) + +By passing in an implementation of `IPermissionSystem` you get fine-grained +control over who can do what in your system. The library calls the methods of +`IPermissionSystem` whenever a user performs an action that requires +authorization. + +If you leave options.permissionSystem `undefined`, the library will allow +everything to everyone! + ## contentUserDataStorage (optional) The `contentUserDataStorage` handles saving and loading user states, so users diff --git a/packages/h5p-examples/src/User.ts b/packages/h5p-examples/src/User.ts index 0686cdb6f..0fddeda74 100644 --- a/packages/h5p-examples/src/User.ts +++ b/packages/h5p-examples/src/User.ts @@ -7,16 +7,10 @@ export default class User implements IUser { constructor() { this.id = '1'; this.name = 'Firstname Surname'; - this.canInstallRecommended = true; - this.canUpdateAndInstallLibraries = true; - this.canCreateRestricted = true; this.type = 'local'; this.email = 'test@example.com'; } - public canCreateRestricted: boolean; - public canInstallRecommended: boolean; - public canUpdateAndInstallLibraries: boolean; public email: string; public id: string; public name: string; diff --git a/packages/h5p-examples/src/expressRoutes.ts b/packages/h5p-examples/src/expressRoutes.ts index a65306c5f..74f317c9b 100644 --- a/packages/h5p-examples/src/expressRoutes.ts +++ b/packages/h5p-examples/src/expressRoutes.ts @@ -46,6 +46,25 @@ export default function ( contextId: typeof req.query.contextId === 'string' ? req.query.contextId + : undefined, + // You can impersonate other users to view their content + // state by setting the query parameter asUserId. + // Example: + // `/h5p/play/XXXX?asUserId=YYY` + asUserId: + typeof req.query.asUserId === 'string' + ? req.query.asUserId + : undefined, + // You can disabling saving of the user state, but still + // display it by setting the query parameter + // `readOnlyState` to `yes`. This is useful if you want + // to review other users' states by setting `asUserId` + // and don't want to change their state. + // Example: + // `/h5p/play/XXXX?readOnlyState=yes` + readOnlyState: + typeof req.query.readOnlyState === 'string' + ? req.query.readOnlyState === 'yes' : undefined } ); diff --git a/packages/h5p-express/src/ContentUserDataRouter/ContentUserDataController.ts b/packages/h5p-express/src/ContentUserDataRouter/ContentUserDataController.ts index 077c59787..fc3154b4a 100644 --- a/packages/h5p-express/src/ContentUserDataRouter/ContentUserDataController.ts +++ b/packages/h5p-express/src/ContentUserDataRouter/ContentUserDataController.ts @@ -37,12 +37,18 @@ export default class ContentUserDataController { ? req.query.contextId : undefined; + const asUserId = + typeof req.query.asUserId === 'string' + ? req.query.asUserId + : undefined; + const result = await this.contentUserDataManager.getContentUserData( contentId, dataType, subContentId, req.user, - contextId + contextId, + asUserId ); if (!result || !result.userState) { @@ -69,6 +75,32 @@ export default class ContentUserDataController { typeof req.query.contextId === 'string' ? req.query.contextId : undefined; + const asUserId = + typeof req.query.asUserId === 'string' + ? req.query.asUserId + : undefined; + const ignorePost = + typeof req.query.ignorePost === 'string' + ? req.query.ignorePost + : undefined; + + // The ignorePost query parameter allows us to cancel requests that + // would fail later, when the ContentUserDataManager would deny write + // requests to user states. It is necessary, as the H5P JavaScript core + // client doesn't support displaying a state while saving is disabled. + // We implement this feature by setting a very long autosave frequency, + // rejecting write requests in the permission system and using the + // ignorePost query parameter. + if (ignorePost == 'yes') { + res.status(200).json( + new AjaxSuccessResponse( + undefined, + 'The user state was not saved, as the query parameter ignorePost was set.' + ) + ); + return; + } + const { user, body } = req; await this.contentUserDataManager.createOrUpdateContentUserData( @@ -79,7 +111,8 @@ export default class ContentUserDataController { body.invalidate === 1 || body.invalidate === '1', body.preload === 1 || body.preload === '1', user, - contextId + contextId, + asUserId ); res.status(200).json(new AjaxSuccessResponse(undefined)).end(); diff --git a/packages/h5p-express/test/ContentUserDataRouter.test.ts b/packages/h5p-express/test/ContentUserDataRouter.test.ts index 4ffa01772..e20e1a5a5 100644 --- a/packages/h5p-express/test/ContentUserDataRouter.test.ts +++ b/packages/h5p-express/test/ContentUserDataRouter.test.ts @@ -102,6 +102,7 @@ describe('ContentUserData endpoint adapter', () => { false, true, user, + undefined, undefined ); expect(res.status).toBe(200); @@ -129,7 +130,8 @@ describe('ContentUserData endpoint adapter', () => { false, true, user, - 'cid1' + 'cid1', + undefined ); expect(res.status).toBe(200); }); @@ -150,6 +152,7 @@ describe('ContentUserData endpoint adapter', () => { dataType, subContentId, user, + undefined, undefined ); expect(res.status).toBe(200); @@ -170,7 +173,14 @@ describe('ContentUserData endpoint adapter', () => { expect( mockContentUserDataManager.getContentUserData - ).toHaveBeenCalledWith(contentId, dataType, subContentId, user, 'cid1'); + ).toHaveBeenCalledWith( + contentId, + dataType, + subContentId, + user, + 'cid1', + undefined + ); expect(res.status).toBe(200); expect(res.body).toEqual({ data: mockReturnData.userState, diff --git a/packages/h5p-express/test/User.ts b/packages/h5p-express/test/User.ts index 0686cdb6f..0fddeda74 100644 --- a/packages/h5p-express/test/User.ts +++ b/packages/h5p-express/test/User.ts @@ -7,16 +7,10 @@ export default class User implements IUser { constructor() { this.id = '1'; this.name = 'Firstname Surname'; - this.canInstallRecommended = true; - this.canUpdateAndInstallLibraries = true; - this.canCreateRestricted = true; this.type = 'local'; this.email = 'test@example.com'; } - public canCreateRestricted: boolean; - public canInstallRecommended: boolean; - public canUpdateAndInstallLibraries: boolean; public email: string; public id: string; public name: string; diff --git a/packages/h5p-html-exporter/test/HtmlExporter.test.ts b/packages/h5p-html-exporter/test/HtmlExporter.test.ts index 3c5bd5f8a..3b5cd92fd 100644 --- a/packages/h5p-html-exporter/test/HtmlExporter.test.ts +++ b/packages/h5p-html-exporter/test/HtmlExporter.test.ts @@ -13,6 +13,7 @@ import LibraryManager from '../../h5p-server/src/LibraryManager'; import PackageImporter from '../../h5p-server/src/PackageImporter'; import HtmlExporter from '../src/HtmlExporter'; import { IIntegration } from '../../h5p-server/src/types'; +import { LaissezFairePermissionSystem } from '../../h5p-server'; import User from './User'; @@ -31,10 +32,12 @@ async function importAndExportHtml( await fsExtra.ensureDir(libraryDir); const user = new User(); - user.canUpdateAndInstallLibraries = true; const contentStorage = new FileContentStorage(contentDir); - const contentManager = new ContentManager(contentStorage); + const contentManager = new ContentManager( + contentStorage, + new LaissezFairePermissionSystem() + ); const libraryStorage = new FileLibraryStorage(libraryDir); const libraryManager = new LibraryManager(libraryStorage); const config = new H5PConfig(null); @@ -42,6 +45,7 @@ async function importAndExportHtml( const packageImporter = new PackageImporter( libraryManager, config, + new LaissezFairePermissionSystem(), contentManager, new ContentStorer(contentManager, libraryManager, undefined) ); @@ -222,10 +226,12 @@ describe('HtmlExporter template', () => { await fsExtra.ensureDir(libraryDir); const user = new User(); - user.canUpdateAndInstallLibraries = true; const contentStorage = new FileContentStorage(contentDir); - const contentManager = new ContentManager(contentStorage); + const contentManager = new ContentManager( + contentStorage, + new LaissezFairePermissionSystem() + ); const libraryStorage = new FileLibraryStorage(libraryDir); const libraryManager = new LibraryManager(libraryStorage); const config = new H5PConfig(null); @@ -233,6 +239,7 @@ describe('HtmlExporter template', () => { const packageImporter = new PackageImporter( libraryManager, config, + new LaissezFairePermissionSystem(), contentManager, new ContentStorer(contentManager, libraryManager, undefined) ); diff --git a/packages/h5p-html-exporter/test/User.ts b/packages/h5p-html-exporter/test/User.ts index 0686cdb6f..0fddeda74 100644 --- a/packages/h5p-html-exporter/test/User.ts +++ b/packages/h5p-html-exporter/test/User.ts @@ -7,16 +7,10 @@ export default class User implements IUser { constructor() { this.id = '1'; this.name = 'Firstname Surname'; - this.canInstallRecommended = true; - this.canUpdateAndInstallLibraries = true; - this.canCreateRestricted = true; this.type = 'local'; this.email = 'test@example.com'; } - public canCreateRestricted: boolean; - public canInstallRecommended: boolean; - public canUpdateAndInstallLibraries: boolean; public email: string; public id: string; public name: string; diff --git a/packages/h5p-mongos3/src/MongoContentUserDataStorage.ts b/packages/h5p-mongos3/src/MongoContentUserDataStorage.ts index fd89629b6..ef8be0b18 100644 --- a/packages/h5p-mongos3/src/MongoContentUserDataStorage.ts +++ b/packages/h5p-mongos3/src/MongoContentUserDataStorage.ts @@ -93,18 +93,19 @@ export default class MongoContentUserDataStorage contentId: ContentId, dataType: string, subContentId: string, - user: IUser, + userId: string, contextId?: string ): Promise { log.debug( - `getContentUserData: loading contentUserData for contentId ${contentId} and userId ${user.id} and contextId ${contextId}` + `getContentUserData: loading contentUserData for contentId ${contentId} and userId ${userId} and contextId ${contextId}` ); + return this.cleanMongoUserData( await this.userDataCollection.findOne({ contentId, dataType, subContentId, - userId: user.id, + userId: userId, contextId }) ); @@ -193,14 +194,14 @@ export default class MongoContentUserDataStorage public async getContentUserDataByContentIdAndUser( contentId: ContentId, - user: IUser, + userId: string, contextId?: string ): Promise { return ( await this.userDataCollection .find({ contentId, - userId: user.id, + userId, contextId }) .toArray() diff --git a/packages/h5p-mongos3/src/MongoS3ContentStorage.ts b/packages/h5p-mongos3/src/MongoS3ContentStorage.ts index 4ace2803a..457ed1f80 100644 --- a/packages/h5p-mongos3/src/MongoS3ContentStorage.ts +++ b/packages/h5p-mongos3/src/MongoS3ContentStorage.ts @@ -11,7 +11,6 @@ import { IContentStorage, IFileStats, IUser, - Permission, ILibraryName, H5pError, Logger @@ -37,16 +36,6 @@ export default class MongoS3ContentStorage implements IContentStorage { private s3: AWS.S3, private mongodb: MongoDB.Collection, private options: { - /** - * If set, the function is called to retrieve a list of permissions - * a user has on a certain content object. - * This function can function as an adapter to your rights and - * privileges system. - */ - getPermissions?: ( - contentId: ContentId, - user: IUser - ) => Promise; /** * These characters will be removed from files that are saved to S3. * There is a very strict default list that basically only leaves @@ -121,19 +110,6 @@ export default class MongoS3ContentStorage implements IContentStorage { user: IUser, contentId?: ContentId ): Promise { - if ( - !(await this.getUserPermissions(contentId, user)).includes( - Permission.Edit - ) - ) { - log.error(`User tried add content without proper permissions.`); - throw new H5pError( - 'mongo-s3-content-storage:missing-write-permission', - {}, - 403 - ); - } - try { if (!contentId) { log.debug(`Inserting new content into MongoDB.`); @@ -202,21 +178,6 @@ export default class MongoS3ContentStorage implements IContentStorage { ); validateFilename(filename); - if ( - !(await this.getUserPermissions(contentId, user)).includes( - Permission.Edit - ) - ) { - log.error( - `User tried to upload a file without proper permissions.` - ); - throw new H5pError( - 'mongo-s3-content-storage:missing-write-permission', - {}, - 403 - ); - } - try { await this.s3 .upload({ @@ -274,20 +235,6 @@ export default class MongoS3ContentStorage implements IContentStorage { user?: IUser ): Promise { log.debug(`Deleting content with id ${contentId}.`); - if ( - !(await this.getUserPermissions(contentId, user)).includes( - Permission.Delete - ) - ) { - log.error( - `User tried to delete a content object without proper permissions.` - ); - throw new H5pError( - 'mongo-s3-content-storage:missing-delete-permission', - {}, - 403 - ); - } try { const filesToDelete = await this.listFiles(contentId, user); log.debug( @@ -355,21 +302,6 @@ export default class MongoS3ContentStorage implements IContentStorage { log.debug( `Deleting file "${filename}" from content with id ${contentId}.` ); - if ( - !(await this.getUserPermissions(contentId, user)).includes( - Permission.Edit - ) - ) { - log.error( - `User tried to delete a file from a content object without proper permissions.` - ); - throw new H5pError( - 'mongo-s3-content-storage:missing-write-permission', - {}, - 403 - ); - } - try { await this.s3 .deleteObject({ @@ -443,21 +375,6 @@ export default class MongoS3ContentStorage implements IContentStorage { ): Promise { validateFilename(filename); - if ( - !(await this.getUserPermissions(contentId, user)).includes( - Permission.View - ) - ) { - log.error( - `User tried to get stats of file from a content object without proper permissions.` - ); - throw new H5pError( - 'mongo-s3-content-storage:missing-view-permission', - {}, - 403 - ); - } - try { const head = await this.s3 .headObject({ @@ -507,21 +424,6 @@ export default class MongoS3ContentStorage implements IContentStorage { ); } - if ( - !(await this.getUserPermissions(contentId, user)).includes( - Permission.View - ) - ) { - log.error( - `User tried to display a file from a content object without proper permissions.` - ); - throw new H5pError( - 'mongo-s3-content-storage:missing-view-permission', - {}, - 403 - ); - } - return this.s3 .getObject({ Bucket: this.options.s3Bucket, @@ -539,20 +441,6 @@ export default class MongoS3ContentStorage implements IContentStorage { user?: IUser ): Promise { log.debug(`Getting metadata for content with id ${contentId}.`); - if ( - !(await this.getUserPermissions(contentId, user)).includes( - Permission.View - ) - ) { - log.error( - `User tried to get metadata of a content object without proper permissions.` - ); - throw new H5pError( - 'mongo-s3-content-storage:missing-view-permission', - {}, - 403 - ); - } try { const ret = await this.mongodb.findOne({ @@ -568,22 +456,9 @@ export default class MongoS3ContentStorage implements IContentStorage { ); } } + public async getParameters(contentId: string, user?: IUser): Promise { log.debug(`Getting parameters for content with id ${contentId}.`); - if ( - !(await this.getUserPermissions(contentId, user)).includes( - Permission.View - ) - ) { - log.error( - `User tried to get parameters of a content object without proper permissions.` - ); - throw new H5pError( - 'mongo-s3-content-storage:missing-view-permission', - {}, - 403 - ); - } try { const ret = await this.mongodb.findOne({ @@ -669,52 +544,8 @@ export default class MongoS3ContentStorage implements IContentStorage { return { asMainLibrary, asDependency }; } - /** - * Returns an array of permissions that the user has on the piece of content - * @param contentId the content id to check - * @param user the user who wants to access the piece of content - * @returns the permissions the user has for this content (e.g. download it, delete it etc.) - */ - public async getUserPermissions( - contentId: ContentId, - user: IUser - ): Promise { - log.debug(`Getting user permissions for content with id ${contentId}.`); - if (this.options.getPermissions) { - log.debug( - `Using function passed in through constructor to get permissions.` - ); - return this.options.getPermissions(contentId, user); - } - log.debug( - `No permission function set in constructor. Allowing everything.` - ); - return [ - Permission.Delete, - Permission.Download, - Permission.Edit, - Permission.Embed, - Permission.List, - Permission.View - ]; - } - public async listContent(user?: IUser): Promise { log.debug(`Listing content objects.`); - if ( - !(await this.getUserPermissions(undefined, user)).includes( - Permission.View - ) - ) { - log.error( - `User tried to list all content objects without proper permissions.` - ); - throw new H5pError( - 'mongo-s3-content-storage:missing-list-content-permission', - {}, - 403 - ); - } try { const cursor = this.mongodb.find({}, { projection: { _id: true } }); @@ -744,20 +575,6 @@ export default class MongoS3ContentStorage implements IContentStorage { user: IUser ): Promise { log.debug(`Listing files in content object with id ${contentId}.`); - if ( - !(await this.getUserPermissions(contentId, user)).includes( - Permission.View - ) - ) { - log.error( - `User tried to get the list of files from a content object without proper permissions.` - ); - throw new H5pError( - 'mongo-s3-content-storage:missing-view-permission', - {}, - 403 - ); - } const prefix = MongoS3ContentStorage.getS3Key(contentId, ''); let files: string[] = []; diff --git a/packages/h5p-mongos3/src/S3TemporaryFileStorage.ts b/packages/h5p-mongos3/src/S3TemporaryFileStorage.ts index d0014ac42..d793dff54 100644 --- a/packages/h5p-mongos3/src/S3TemporaryFileStorage.ts +++ b/packages/h5p-mongos3/src/S3TemporaryFileStorage.ts @@ -4,7 +4,6 @@ import { ITemporaryFileStorage, IUser, ITemporaryFile, - Permission, IH5PConfig, IFileStats, H5pError, @@ -31,19 +30,6 @@ export default class S3TemporaryFileStorage implements ITemporaryFileStorage { constructor( private s3: AWS.S3, private options: { - /** - * This function is called to determine whether a user has access - * rights to a file stored in temporary storage. Returns a list - * of all permissions the user has on this file. - * - * Note: The Permissions enumeration is also used for content and - * includes more values than necessary for temporary storage. Only - * the values 'Edit', 'View' and 'Delete' are used in this class. - */ - getPermissions?: ( - userId: string, - filename?: string - ) => Promise; /** * These characters will be removed from files that are saved to S3. * There is a very strict default list that basically only leaves @@ -90,7 +76,7 @@ export default class S3TemporaryFileStorage implements ITemporaryFileStorage { * @param filename the file to delete * @param userId the user ID of the user who wants to delete the file */ - public async deleteFile(filename: string, userId: string): Promise { + public async deleteFile(filename: string, _ownerId: string): Promise { log.debug(`Deleting file "${filename}" from temporary storage.`); validateFilename(filename); @@ -99,21 +85,6 @@ export default class S3TemporaryFileStorage implements ITemporaryFileStorage { throw new H5pError('s3-temporary-storage:file-not-found', {}, 404); } - if ( - !(await this.getUserPermissions(userId, filename)).includes( - Permission.Delete - ) - ) { - log.error( - `User tried to delete a file from a temporary storage without proper permissions.` - ); - throw new H5pError( - 's3-temporary-storage:missing-delete-permission', - {}, - 403 - ); - } - try { await this.s3 .deleteObject({ @@ -138,7 +109,7 @@ export default class S3TemporaryFileStorage implements ITemporaryFileStorage { * @param filename the file to check * @param user the user who wants to access the file */ - public async fileExists(filename: string, user: IUser): Promise { + public async fileExists(filename: string, _user: IUser): Promise { log.debug(`Checking if file ${filename} exists in temporary storage.`); validateFilename(filename); @@ -179,25 +150,10 @@ export default class S3TemporaryFileStorage implements ITemporaryFileStorage { */ public async getFileStats( filename: string, - user: IUser + _user: IUser ): Promise { validateFilename(filename); - if ( - !(await this.getUserPermissions(user.id, filename)).includes( - Permission.View - ) - ) { - log.error( - `User tried to get stats of a content object without proper permissions.` - ); - throw new H5pError( - 's3-temporary-storage:missing-view-permission', - {}, - 403 - ); - } - try { const head = await this.s3 .headObject({ @@ -237,21 +193,6 @@ export default class S3TemporaryFileStorage implements ITemporaryFileStorage { throw new H5pError('s3-temporary-storage:file-not-found', {}, 404); } - if ( - !(await this.getUserPermissions(user.id, filename)).includes( - Permission.View - ) - ) { - log.error( - `User tried to display a file from a content object without proper permissions.` - ); - throw new H5pError( - 's3-temporary-storage:missing-view-permission', - {}, - 403 - ); - } - return this.s3 .getObject({ Bucket: this.options?.s3Bucket, @@ -264,31 +205,6 @@ export default class S3TemporaryFileStorage implements ITemporaryFileStorage { .createReadStream(); } - /** - * Checks if a user has access rights on file in temporary storage. - * @param userId - * @param filename - * @returns the list of permissions the user has on the file. - */ - public async getUserPermissions( - userId: string, - filename?: string - ): Promise { - log.debug( - `Getting temporary storage permissions for userId ${userId}.` - ); - if (this.options?.getPermissions) { - log.debug( - `Using function passed in through constructor to get permissions.` - ); - return this.options.getPermissions(userId, filename); - } - log.debug( - `No permission function set in constructor. Allowing everything.` - ); - return [Permission.Delete, Permission.Edit, Permission.View]; - } - /** * Theoretically lists all files either in temporary storage in general * or files which the user has stored in it. @@ -339,21 +255,6 @@ export default class S3TemporaryFileStorage implements ITemporaryFileStorage { throw new H5pError('illegal-filename', {}, 400); } - if ( - !(await this.getUserPermissions(user.id, filename)).includes( - Permission.Edit - ) - ) { - log.error( - `User tried upload file to temporary storage without proper permissions.` - ); - throw new H5pError( - 's3-temporary-storage:missing-write-permission', - {}, - 403 - ); - } - try { await this.s3 .upload({ diff --git a/packages/h5p-mongos3/test/MongoS3ContentStorage.test.ts b/packages/h5p-mongos3/test/MongoS3ContentStorage.test.ts index 273e99031..583d04714 100644 --- a/packages/h5p-mongos3/test/MongoS3ContentStorage.test.ts +++ b/packages/h5p-mongos3/test/MongoS3ContentStorage.test.ts @@ -8,7 +8,7 @@ import fsExtra from 'fs-extra'; import path from 'path'; import { BufferWritableMock, BufferReadableMock } from 'stream-mock'; import promisepipe from 'promisepipe'; -import { IContentMetadata, Permission } from '@lumieducation/h5p-server'; +import { IContentMetadata } from '@lumieducation/h5p-server'; import MongoS3ContentStorage from '../src/MongoS3ContentStorage'; import User from './User'; @@ -406,18 +406,6 @@ describe('MongoS3ContentStorage', () => { ).rejects.toThrowError('illegal-filename'); }); - it('rejects write operations for unprivileged users', async () => { - storage = new MongoS3ContentStorage(s3, mongoCollection, { - s3Bucket: bucketName, - getPermissions: async () => [Permission.View] - }); - await expect( - storage.addContent(stubMetadata, stubParameters, new User()) - ).rejects.toThrowError( - 'mongo-s3-content-storage:missing-write-permission' - ); - }); - describe('getUsage', () => { it(`doesn't count main libraries as dependencies`, async () => { await storage.addContent(stubMetadata, stubParameters, new User()); diff --git a/packages/h5p-mongos3/test/S3TemporaryFileStorage.test.ts b/packages/h5p-mongos3/test/S3TemporaryFileStorage.test.ts index aa00ef3d7..f393a5438 100644 --- a/packages/h5p-mongos3/test/S3TemporaryFileStorage.test.ts +++ b/packages/h5p-mongos3/test/S3TemporaryFileStorage.test.ts @@ -8,7 +8,6 @@ import fsExtra from 'fs-extra'; import path from 'path'; import { BufferWritableMock } from 'stream-mock'; import promisepipe from 'promisepipe'; -import { Permission, H5PConfig } from '@lumieducation/h5p-server'; import User from './User'; import initS3 from '../src/initS3'; @@ -172,14 +171,4 @@ describe('S3TemporaryFileStorage', () => { storage.saveFile('/bin/bash', undefined, stubUser, new Date()) ).rejects.toThrowError('illegal-filename'); }); - - it('rejects write operations for unprivileged users', async () => { - storage = new S3TemporaryFileStorage(s3, { - s3Bucket: bucketName, - getPermissions: async () => [Permission.View] - }); - await expect( - storage.saveFile('123', undefined, new User(), new Date()) - ).rejects.toThrowError('s3-temporary-storage:missing-write-permission'); - }); }); diff --git a/packages/h5p-mongos3/test/User.ts b/packages/h5p-mongos3/test/User.ts index 0686cdb6f..0fddeda74 100644 --- a/packages/h5p-mongos3/test/User.ts +++ b/packages/h5p-mongos3/test/User.ts @@ -7,16 +7,10 @@ export default class User implements IUser { constructor() { this.id = '1'; this.name = 'Firstname Surname'; - this.canInstallRecommended = true; - this.canUpdateAndInstallLibraries = true; - this.canCreateRestricted = true; this.type = 'local'; this.email = 'test@example.com'; } - public canCreateRestricted: boolean; - public canInstallRecommended: boolean; - public canUpdateAndInstallLibraries: boolean; public email: string; public id: string; public name: string; diff --git a/packages/h5p-react/src/H5PPlayerUI.tsx b/packages/h5p-react/src/H5PPlayerUI.tsx index 278df4dfa..6d6e5a625 100644 --- a/packages/h5p-react/src/H5PPlayerUI.tsx +++ b/packages/h5p-react/src/H5PPlayerUI.tsx @@ -18,6 +18,9 @@ declare global { interface IntrinsicElements { 'h5p-player': { 'content-id'?: string; + 'context-id'?: string; + 'read-only-state'?: boolean; + 'as-user-id'?: string; ref?: any; }; } @@ -27,9 +30,13 @@ declare global { interface IH5PPlayerUIProps { contentId: string; contextId?: string; + asUserId?: string; + readOnlyState?: boolean; loadContentCallback: ( contentId: string, - contextId?: string + contextId?: string, + asUserId?: string, + readOnlyState?: boolean ) => Promise; onInitialized?: (contentId: string) => void; onxAPIStatement?: (statement: any, context: any, event: IxAPIEvent) => void; @@ -109,6 +116,8 @@ export default class H5PPlayerUI extends Component { ref={this.h5pPlayer} content-id={this.props.contentId} context-id={this.props.contextId} + as-user-id={this.props.asUserId} + read-only-state={this.props.readOnlyState} /> ); } @@ -132,9 +141,16 @@ export default class H5PPlayerUI extends Component { private loadContentCallbackWrapper = ( contentId: string, - contextId?: string + contextId?: string, + asUserId?: string, + readOnlyState?: boolean ): Promise => - this.props.loadContentCallback(contentId, contextId); + this.props.loadContentCallback( + contentId, + contextId, + asUserId, + readOnlyState + ); private onInitialized = ( event: CustomEvent<{ contentId: string }> diff --git a/packages/h5p-rest-example-client/src/App.tsx b/packages/h5p-rest-example-client/src/App.tsx index 8a63bacfa..988cc6060 100644 --- a/packages/h5p-rest-example-client/src/App.tsx +++ b/packages/h5p-rest-example-client/src/App.tsx @@ -17,6 +17,12 @@ export default class App extends React.Component { private contentService: ContentService; + public state: { + loggedIn: boolean; + } = { + loggedIn: false + }; + render() { return (
@@ -26,16 +32,31 @@ export default class App extends React.Component {

H5P NodeJs SPA Demo

- + { + this.setState({ loggedIn: true }); + }} + onLoggedOut={() => { + this.setState({ loggedIn: false }); + }} + /> This demo is for debugging and demonstration purposes only and not suitable for production use! - + {this.state.loggedIn ? ( + + ) : ( + + Content is only visible to logged in users! Please + log in with the button on the top + + )}
); diff --git a/packages/h5p-rest-example-client/src/components/ContentListEntryComponent.tsx b/packages/h5p-rest-example-client/src/components/ContentListEntryComponent.tsx index 5f2b2c081..09846bfac 100644 --- a/packages/h5p-rest-example-client/src/components/ContentListEntryComponent.tsx +++ b/packages/h5p-rest-example-client/src/components/ContentListEntryComponent.tsx @@ -22,7 +22,9 @@ import { faFileDownload, faTrashAlt, faCopyright, - faHashtag + faHashtag, + faUser, + faLock } from '@fortawesome/free-solid-svg-icons'; import { H5PEditorUI, H5PPlayerUI } from '@lumieducation/h5p-react'; @@ -56,31 +58,38 @@ export default class ContentListEntryComponent extends React.Component<{ saveErrorMessage: '', saveError: false, showingCustomCopyright: false, - showContextIdModal: false + showContextIdModal: false, + showAsUserIdModal: false, + readOnlyState: false }; this.h5pEditor = React.createRef(); this.saveButton = React.createRef(); this.h5pPlayer = React.createRef(); this.contextIdInput = React.createRef(); + this.asUserIdSelect = React.createRef(); } public state: { + asUserId?: string; contextId?: string; editing: boolean; loading: boolean; playing: boolean; + readOnlyState: boolean; saved: boolean; - saving: boolean; saveError: boolean; saveErrorMessage: string; - showingCustomCopyright: boolean; + saving: boolean; + showAsUserIdModal: boolean; showContextIdModal: boolean; + showingCustomCopyright: boolean; }; private h5pPlayer: React.RefObject; private h5pEditor: React.RefObject; private saveButton: React.RefObject; private contextIdInput: React.RefObject; + private asUserIdSelect: React.RefObject; public render(): React.ReactNode { return ( @@ -139,6 +148,52 @@ export default class ContentListEntryComponent extends React.Component<{ /> + + + {this.state.asUserId + ? this.state.asUserId + : "displaying real user's state"} + + + + + + + this.setState({ + readOnlyState: + !this.state.readOnlyState + }) + } + > + {this.state.playing ? ( @@ -349,6 +404,8 @@ export default class ContentListEntryComponent extends React.Component<{ ref={this.h5pPlayer} contentId={this.props.data.contentId} contextId={this.state.contextId || undefined} + asUserId={this.state.asUserId || undefined} + readOnlyState={this.state.readOnlyState} loadContentCallback={ this.props.contentService.getPlay } @@ -443,6 +500,49 @@ export default class ContentListEntryComponent extends React.Component<{ + + + Impersonate user + + +
+ + Show the user state of this user: + + + + + + + + + + + You switch whose user state you want to display + here. The example permission system only allows + displaying others' user states to teachers and + administrators. + +
+
+ + + + +
); } @@ -478,6 +578,17 @@ export default class ContentListEntryComponent extends React.Component<{ this.setState({ showContextIdModal: false }); } + protected showAsUserIdModal() { + this.setState({ showAsUserIdModal: true }); + setTimeout(() => { + this.asUserIdSelect.current?.focus(); + }, 100); + } + + protected closeAsUserIdModal() { + this.setState({ showAsUserIdModal: false }); + } + protected showCopyrightNative() { this.h5pPlayer.current?.showCopyright(); } @@ -539,6 +650,13 @@ export default class ContentListEntryComponent extends React.Component<{ }); }; + protected setAsUserId = () => { + this.setState({ + asUserId: this.asUserIdSelect.current?.value, + showAsUserIdModal: false + }); + }; + private isNew() { return this.props.data.contentId === 'new'; } diff --git a/packages/h5p-rest-example-client/src/components/Login.tsx b/packages/h5p-rest-example-client/src/components/Login.tsx index 5cb1ddcbf..b9efefb78 100644 --- a/packages/h5p-rest-example-client/src/components/Login.tsx +++ b/packages/h5p-rest-example-client/src/components/Login.tsx @@ -5,7 +5,11 @@ import { Button, Col, Dropdown, Row } from 'react-bootstrap'; import { ContentService } from '../services/ContentService'; export default class Login extends React.Component< - { contentService: ContentService }, + { + contentService: ContentService; + onLoggedIn: () => void; + onLoggedOut: () => void; + }, { loginData?: { username: string; @@ -46,6 +50,7 @@ export default class Login extends React.Component< loginData.csrfToken ); } + this.props.onLoggedIn(); } else { this.setState({ ...this.state, @@ -53,6 +58,7 @@ export default class Login extends React.Component< loginMessage: await res.text() }); this.props.contentService.setCsrfToken(undefined); + this.props.onLoggedOut(); } }) .catch((reason) => { @@ -62,6 +68,7 @@ export default class Login extends React.Component< loginMessage: reason }); this.props.contentService.setCsrfToken(undefined); + this.props.onLoggedOut(); }); }; @@ -82,6 +89,7 @@ export default class Login extends React.Component< loginMessage: undefined }); this.props.contentService.setCsrfToken(undefined); + this.props.onLoggedOut(); }) .catch((reason) => { this.setState({ @@ -90,6 +98,7 @@ export default class Login extends React.Component< loginMessage: `Error logging out: ${reason}` }); this.props.contentService.setCsrfToken(undefined); + this.props.onLoggedOut(); }); }; @@ -122,6 +131,9 @@ export default class Login extends React.Component< > Student 2 + this.login('admin')}> + Administrator + ) : ( diff --git a/packages/h5p-rest-example-client/src/services/ContentService.ts b/packages/h5p-rest-example-client/src/services/ContentService.ts index 90c772c49..a2f203ece 100644 --- a/packages/h5p-rest-example-client/src/services/ContentService.ts +++ b/packages/h5p-rest-example-client/src/services/ContentService.ts @@ -14,7 +14,12 @@ export interface IContentListEntry { export interface IContentService { delete(contentId: string): Promise; getEdit(contentId: string): Promise; - getPlay(contentId: string, contextId?: string): Promise; + getPlay( + contentId: string, + contextId?: string, + asUserId?: string, + readOnlyState?: boolean + ): Promise; list(): Promise; save( contentId: string, @@ -61,16 +66,36 @@ export class ContentService implements IContentService { getPlay = async ( contentId: string, - contextId?: string + contextId?: string, + asUserId?: string, + readOnlyState?: boolean ): Promise => { console.log( `ContentService: Getting information to play ${contentId}${ - contextId ? ` and contextId ${contextId}` : '' + contextId ? `, contextId ${contextId}` : '' + }${asUserId ? `, asUserId ${asUserId}` : ''}${ + readOnlyState !== undefined + ? `, readOnlyState ${readOnlyState}` + : '' }...` ); + + const query = new URLSearchParams(); + if (contextId) { + query.append('contextId', contextId); + } + if (asUserId) { + query.append('asUserId', asUserId); + } + if (readOnlyState === true) { + query.append('readOnlyState', 'yes'); + } + + const queryString = query.toString(); + const res = await fetch( `${this.baseUrl}/${contentId}/play${ - contextId ? `?contextId=${contextId}` : '' + queryString ? `?${queryString}` : '' }` ); if (!res || !res.ok) { diff --git a/packages/h5p-rest-example-server/.eslintrc.js b/packages/h5p-rest-example-server/.eslintrc.js index e6cdb9c43..8b51f2e93 100644 --- a/packages/h5p-rest-example-server/.eslintrc.js +++ b/packages/h5p-rest-example-server/.eslintrc.js @@ -1,7 +1,9 @@ +const path = require('path'); + module.exports = { extends: ['../../.eslintrc.js'], parserOptions: { - project: './tsconfig.build.json', + project: path.join(__dirname, 'tsconfig.build.json'), sourceType: 'module' }, rules: { diff --git a/packages/h5p-rest-example-server/src/ExamplePermissionSystem.ts b/packages/h5p-rest-example-server/src/ExamplePermissionSystem.ts new file mode 100644 index 000000000..94d5efd1d --- /dev/null +++ b/packages/h5p-rest-example-server/src/ExamplePermissionSystem.ts @@ -0,0 +1,147 @@ +import { + IPermissionSystem, + GeneralPermission, + TemporaryFilePermission, + ContentPermission, + UserDataPermission +} from '@lumieducation/h5p-server'; + +import ExampleUser from './ExampleUser'; + +export default class ExamplePermissionSystem + implements IPermissionSystem +{ + async checkForUserData( + actingUser: ExampleUser, + permission: UserDataPermission, + contentId: string, + affectedUserId?: string + ): Promise { + if (!actingUser) { + return false; + } + if (actingUser.role === 'admin') { + return true; + } else if (actingUser.role === 'teacher') { + switch (permission) { + case UserDataPermission.DeleteFinished: + case UserDataPermission.DeleteState: + case UserDataPermission.EditFinished: + case UserDataPermission.EditState: + case UserDataPermission.ListStates: + case UserDataPermission.ViewFinished: + case UserDataPermission.ViewState: + return true; + default: + return false; + } + } else if (actingUser.role === 'student') { + switch (permission) { + case UserDataPermission.EditFinished: + case UserDataPermission.EditState: + case UserDataPermission.ListStates: + case UserDataPermission.ViewState: + case UserDataPermission.ViewFinished: + if (affectedUserId === actingUser.id) { + return true; + } + return false; + default: + return false; + } + } else { + return false; + } + } + + async checkForContent( + actingUser: ExampleUser | undefined, + permission: ContentPermission, + contentId?: string + ): Promise { + if (!actingUser) { + return false; + } + if (actingUser.role === 'admin') { + return true; + } else if (actingUser.role === 'teacher') { + switch (permission) { + case ContentPermission.Create: + case ContentPermission.Delete: + case ContentPermission.Download: + case ContentPermission.Edit: + case ContentPermission.Embed: + case ContentPermission.List: + case ContentPermission.View: + return true; + default: + return false; + } + } else if (actingUser.role === 'student') { + switch (permission) { + case ContentPermission.List: + case ContentPermission.View: + return true; + default: + return false; + } + } else { + return false; + } + } + + async checkForTemporaryFile( + user: ExampleUser | undefined, + permission: TemporaryFilePermission, + filename?: string + ): Promise { + if (!user || !user.role || user.role === 'anonymous') { + return false; + } + return true; + } + + async checkForGeneralAction( + actingUser: ExampleUser | undefined, + permission: GeneralPermission + ): Promise { + if (!actingUser) { + return false; + } + if (actingUser.role === 'admin') { + switch (permission) { + case GeneralPermission.InstallRecommended: + case GeneralPermission.UpdateAndInstallLibraries: + case GeneralPermission.CreateRestricted: + return true; + default: + return false; + } + } else if (actingUser.role === 'teacher') { + switch (permission) { + case GeneralPermission.InstallRecommended: + return false; + case GeneralPermission.UpdateAndInstallLibraries: + return false; + case GeneralPermission.CreateRestricted: + return false; + default: + return false; + } + } else if (actingUser.role === 'student') { + switch (permission) { + case GeneralPermission.InstallRecommended: + return false; + case GeneralPermission.UpdateAndInstallLibraries: + return false; + case GeneralPermission.CreateRestricted: + return false; + default: + return false; + } + } else { + // anonymous or completely unauthenticated + return false; + } + } +} diff --git a/packages/h5p-rest-example-server/src/ExampleUser.ts b/packages/h5p-rest-example-server/src/ExampleUser.ts new file mode 100644 index 000000000..a112c6c89 --- /dev/null +++ b/packages/h5p-rest-example-server/src/ExampleUser.ts @@ -0,0 +1,19 @@ +import { IUser } from '@lumieducation/h5p-server'; + +/** + * Example user object + */ +export default class ExampleUser implements IUser { + constructor( + public id: string, + public name: string, + public email: string, + // role is a custom property that is not required by the core; We can + // use it in ExamplePermissionSystem to evaluate individual permission + public role: 'anonymous' | 'teacher' | 'student' | 'admin' + ) { + this.type = 'local'; + } + + public type: 'local'; +} diff --git a/packages/h5p-rest-example-server/src/User.ts b/packages/h5p-rest-example-server/src/User.ts deleted file mode 100644 index 3f13da346..000000000 --- a/packages/h5p-rest-example-server/src/User.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { IUser } from '@lumieducation/h5p-server'; - -/** - * Example user object - */ -export default class User implements IUser { - constructor( - public id: string, - public name: string, - public email: string, - accessLevel: 'anonymous' | 'teacher' - ) { - this.canInstallRecommended = accessLevel === 'teacher'; - this.canUpdateAndInstallLibraries = accessLevel === 'teacher'; - this.canCreateRestricted = accessLevel === 'teacher'; - this.type = 'local'; - } - - public canCreateRestricted: boolean; - public canInstallRecommended: boolean; - public canUpdateAndInstallLibraries: boolean; - public type: 'local'; -} diff --git a/packages/h5p-rest-example-server/src/createH5PEditor.ts b/packages/h5p-rest-example-server/src/createH5PEditor.ts index 9a85454ed..b4e75a2fe 100644 --- a/packages/h5p-rest-example-server/src/createH5PEditor.ts +++ b/packages/h5p-rest-example-server/src/createH5PEditor.ts @@ -29,6 +29,7 @@ import { IContentMetadata, IUser } from '@lumieducation/h5p-server'; export default async function createH5PEditor( config: H5P.IH5PConfig, urlGenerator: H5P.IUrlGenerator, + permissionSystem: H5P.IPermissionSystem, localLibraryPath: string, localContentPath?: string, localTemporaryPath?: string, @@ -130,7 +131,8 @@ export default async function createH5PEditor( { enableHubLocalization: true, enableLibraryNameLocalization: true, - hooks + hooks, + permissionSystem }, contentUserDataStorage ); diff --git a/packages/h5p-rest-example-server/src/index.ts b/packages/h5p-rest-example-server/src/index.ts index 68b4f5b73..4f8a46eb0 100644 --- a/packages/h5p-rest-example-server/src/index.ts +++ b/packages/h5p-rest-example-server/src/index.ts @@ -19,9 +19,10 @@ import { import * as H5P from '@lumieducation/h5p-server'; import restExpressRoutes from './routes'; -import User from './User'; +import ExampleUser from './ExampleUser'; import createH5PEditor from './createH5PEditor'; import { displayIps, clearTempFiles } from './utils'; +import ExamplePermissionSystem from './ExamplePermissionSystem'; let tmpDir: DirectoryResult; @@ -30,25 +31,37 @@ const userTable = { username: 'teacher1', name: 'Teacher 1', email: 'teacher1@example.com', - type: 'teacher' + role: 'teacher' }, teacher2: { username: 'teacher2', name: 'Teacher 2', email: 'teacher2@example.com', - type: 'teacher' + role: 'teacher' }, student1: { username: 'student1', name: 'Student 1', email: 'student1@example.com', - type: 'student' + role: 'student' }, student2: { username: 'student2', name: 'Student 2', email: 'student2@example.com', - type: 'student' + role: 'student' + }, + admin: { + username: 'admin', + name: 'Administration', + email: 'admin@example.com', + role: 'admin' + }, + anonymous: { + username: 'anonymous', + name: 'Anonymous', + email: '', + role: 'anonymous' } }; @@ -142,6 +155,8 @@ const start = async (): Promise => { protectSetFinished: true }); + const permissionSystem = new ExamplePermissionSystem(); + // The H5PEditor object is central to all operations of h5p-nodejs-library // if you want to user the editor component. // @@ -155,6 +170,7 @@ const start = async (): Promise => { const h5pEditor: H5P.H5PEditor = await createH5PEditor( config, urlGenerator, + permissionSystem, path.resolve('h5p/libraries'), // the path on the local disc where // libraries should be stored) path.resolve('h5p/content'), // the path on the local disc where content @@ -177,7 +193,7 @@ const start = async (): Promise => { undefined, urlGenerator, undefined, - undefined, + { permissionSystem }, h5pEditor.contentUserDataStorage ); @@ -237,7 +253,12 @@ const start = async (): Promise => { server.use( ( req: express.Request & { user: H5P.IUser } & { - user: { username?: string; name?: string; email?: string }; + user: { + username?: string; + name?: string; + email?: string; + role?: 'anonymous' | 'teacher' | 'student' | 'admin'; + }; }, res, next @@ -245,14 +266,19 @@ const start = async (): Promise => { // Maps the user received from passport to the one expected by // h5p-express and h5p-server if (req.user) { - req.user = new User( + req.user = new ExampleUser( req.user.username, req.user.name, req.user.email, - req.user.type === 'teacher' ? 'teacher' : 'anonymous' + req.user.role ); } else { - req.user = new User('0', 'Anonymous', '', 'anonymous'); + req.user = new ExampleUser( + 'anonymous', + 'Anonymous', + '', + 'anonymous' + ); } next(); } diff --git a/packages/h5p-rest-example-server/src/indexSharedState.ts b/packages/h5p-rest-example-server/src/indexSharedState.ts index c6ca6af52..fce75964f 100644 --- a/packages/h5p-rest-example-server/src/indexSharedState.ts +++ b/packages/h5p-rest-example-server/src/indexSharedState.ts @@ -22,45 +22,51 @@ import * as H5P from '@lumieducation/h5p-server'; import SharedStateServer from '@lumieducation/h5p-shared-state-server'; import restExpressRoutes from './routes'; -import User from './User'; +import ExampleUser from './ExampleUser'; import createH5PEditor from './createH5PEditor'; import { displayIps, clearTempFiles } from './utils'; -import { IUser, Permission } from '@lumieducation/h5p-server'; +import { IUser } from '@lumieducation/h5p-server'; +import ExamplePermissionSystem from './ExamplePermissionSystem'; let tmpDir: DirectoryResult; let sharedStateServer: SharedStateServer; -const userTable: { - [username: string]: { - username: string; - name: string; - email: string; - type: 'teacher' | 'student'; - }; -} = { +const users = { teacher1: { username: 'teacher1', name: 'Teacher 1', email: 'teacher1@example.com', - type: 'teacher' + role: 'teacher' }, teacher2: { username: 'teacher2', name: 'Teacher 2', email: 'teacher2@example.com', - type: 'teacher' + role: 'teacher' }, student1: { username: 'student1', name: 'Student 1', email: 'student1@example.com', - type: 'student' + role: 'student' }, student2: { username: 'student2', name: 'Student 2', email: 'student2@example.com', - type: 'student' + role: 'student' + }, + admin: { + username: 'admin', + name: 'Administration', + email: 'admin@example.com', + role: 'admin' + }, + anonymous: { + username: 'anonymous', + name: 'Anonymous', + email: '', + role: 'anonymous' } }; @@ -69,7 +75,7 @@ const initPassport = (): void => { new LocalStrategy((username, password, callback) => { // We don't check the password. In a real application you'll perform // DB access here. - const user = userTable[username]; + const user = users[username]; if (!user) { callback('User not found in user table'); } else { @@ -94,17 +100,12 @@ const expressUserToH5PUser = (user?: { username: string; name: string; email: string; - type: 'teacher' | 'student'; + role: 'anonymous' | 'teacher' | 'student' | 'admin'; }): IUser => { if (user) { - return new User( - user.username, - user.name, - user.email, - user.type === 'teacher' ? 'teacher' : 'anonymous' - ); + return new ExampleUser(user.username, user.name, user.email, user.role); } else { - return new User('0', 'Anonymous', '', 'anonymous'); + return new ExampleUser('anonymous', 'Anonymous', '', 'anonymous'); } }; @@ -153,6 +154,8 @@ const start = async (): Promise => { new H5P.fsImplementations.JsonStorage(path.resolve('config.json')) ).load(); + const permissionSystem = new ExamplePermissionSystem(); + // The H5PEditor object is central to all operations of h5p-nodejs-library // if you want to user the editor component. // @@ -166,6 +169,7 @@ const start = async (): Promise => { const h5pEditor: H5P.H5PEditor = await createH5PEditor( config, undefined, + permissionSystem, path.resolve('h5p/libraries'), // the path on the local disc where // libraries should be stored) path.resolve('h5p/content'), // the path on the local disc where content @@ -196,25 +200,7 @@ const start = async (): Promise => { undefined, undefined, undefined, - { - getPermissions: async (contentId, user) => { - const foundUser = userTable[user.id]; - if (!foundUser) { - return []; - } - if (foundUser.type === 'teacher') { - return [ - Permission.Delete, - Permission.Download, - Permission.Edit, - Permission.Embed, - Permission.List, - Permission.View - ]; - } - return [Permission.Embed, Permission.View]; - } - } + { permissionSystem } ); h5pPlayer.setRenderer((model) => model); @@ -275,7 +261,7 @@ const start = async (): Promise => { username: string; name: string; email: string; - type: 'teacher' | 'student'; + role: 'anonymous' | 'teacher' | 'student' | 'admin'; }; }, res, @@ -383,7 +369,10 @@ const start = async (): Promise => { res.status(200).json({ level: 'anonymous' }); } else { let level: string; - if (userTable[(req.user as any)?.id]?.type === 'teacher') { + if ( + users[(req.user as any)?.id]?.role === 'teacher' || + users[(req.user as any)?.id]?.role === 'admin' + ) { level = 'privileged'; } else { level = 'user'; @@ -425,12 +414,14 @@ const start = async (): Promise => { await passportSessionPromise(req, {}); return expressUserToH5PUser(req.user as any); }, - async (user, contentId) => { - const userInTable = userTable[user.id]; + async (user, _contentId) => { + const userInTable = users[user.id]; if (!userInTable) { return undefined; } - return userInTable.type === 'teacher' ? 'privileged' : 'user'; + return userInTable.role === 'teacher' || userInTable === 'admin' + ? 'privileged' + : 'user'; }, h5pEditor.contentManager.getContentMetadata.bind( h5pEditor.contentManager diff --git a/packages/h5p-rest-example-server/src/routes.ts b/packages/h5p-rest-example-server/src/routes.ts index d6c3ef7ef..0a13392fb 100644 --- a/packages/h5p-rest-example-server/src/routes.ts +++ b/packages/h5p-rest-example-server/src/routes.ts @@ -36,13 +36,34 @@ export default function ( contextId: typeof req.query.contextId === 'string' ? req.query.contextId + : undefined, + // You can impersonate other users to view their content + // state by setting the query parameter asUserId. + // Example: + // `/h5p/play/XXXX?asUserId=YYY` + asUserId: + typeof req.query.asUserId === 'string' + ? req.query.asUserId + : undefined, + // You can disabling saving of the user state, but still + // display it by setting the query parameter + // `readOnlyState` to `yes`. This is useful if you want + // to review other users' states by setting `asUserId` + // and don't want to change their state. + // Example: + // `/h5p/play/XXXX?readOnlyState=yes` + readOnlyState: + typeof req.query.readOnlyState === 'string' + ? req.query.readOnlyState === 'yes' : undefined } ); - res.send(content); - res.status(200).end(); + res.status(200).send(content); } catch (error) { - res.status(500).end(error.message); + console.error(error); + res.status(error.httpStatusCode ? error.httpStatusCode : 500).send( + error.message + ); } }); @@ -61,19 +82,19 @@ export default function ( req.user )) as H5P.IEditorModel; if (!req.params.contentId || req.params.contentId === 'undefined') { - res.send(editorModel); + res.status(200).send(editorModel); } else { const content = await h5pEditor.getContent( - req.params.contentId + req.params.contentId, + req.user ); - res.send({ + res.status(200).send({ ...editorModel, library: content.library, metadata: content.params.metadata, params: content.params.params }); } - res.status(200).end(); } ); @@ -85,7 +106,7 @@ export default function ( !req.body.library || !req.user ) { - res.status(400).send('Malformed request').end(); + res.status(400).send('Malformed request'); return; } const { id: contentId, metadata } = @@ -97,8 +118,7 @@ export default function ( req.user ); - res.send(JSON.stringify({ contentId, metadata })); - res.status(200).end(); + res.status(200).json({ contentId, metadata }); }); router.patch('/:contentId', async (req: IRequestWithUser, res) => { @@ -109,7 +129,7 @@ export default function ( !req.body.library || !req.user ) { - res.status(400).send('Malformed request').end(); + res.status(400).send('Malformed request'); return; } const { id: contentId, metadata } = @@ -121,38 +141,51 @@ export default function ( req.user ); - res.send(JSON.stringify({ contentId, metadata })); - res.status(200).end(); + res.status(200).json({ contentId, metadata }); }); router.delete('/:contentId', async (req: IRequestWithUser, res) => { try { await h5pEditor.deleteContent(req.params.contentId, req.user); } catch (error) { - res.send( - `Error deleting content with id ${req.params.contentId}: ${error.message}` - ); - res.status(500).end(); - return; + console.error(error); + + return res + .status(500) + .send( + `Error deleting content with id ${req.params.contentId}: ${error.message}` + ); } - res.send(`Content ${req.params.contentId} successfully deleted.`); - res.status(200).end(); + res.status(200).send( + `Content ${req.params.contentId} successfully deleted.` + ); }); router.get('/', async (req: IRequestWithUser, res) => { - // TODO: check access permissions - - const contentIds = await h5pEditor.contentManager.listContent(); - const contentObjects = await Promise.all( - contentIds.map(async (id) => ({ - content: await h5pEditor.contentManager.getContentMetadata( - id, - req.user - ), - id - })) - ); + let contentObjects; + try { + const contentIds = await h5pEditor.contentManager.listContent( + req.user + ); + contentObjects = await Promise.all( + contentIds.map(async (id) => ({ + content: await h5pEditor.contentManager.getContentMetadata( + id, + req.user + ), + id + })) + ); + } catch (error) { + if (error instanceof H5P.H5pError) { + return res + .status(error.httpStatusCode) + .send(`${error.message}`); + } else { + return res.status(500).send(`Unknown error: ${error.message}`); + } + } res.status(200).send( contentObjects.map((o) => ({ diff --git a/packages/h5p-server/assets/translations/mongo-s3-content-storage/bg.json b/packages/h5p-server/assets/translations/mongo-s3-content-storage/bg.json index 7b125052f..c9fb5bb36 100644 --- a/packages/h5p-server/assets/translations/mongo-s3-content-storage/bg.json +++ b/packages/h5p-server/assets/translations/mongo-s3-content-storage/bg.json @@ -5,10 +5,6 @@ "filename-too-long": "Името на файла е твърде дълго: {{filename}}", "illegal-filename": "Името на файла {{filename}} не е разрешено.", "listing-content-error": "Възникна грешка при получаването на списъка на обектите със съдържание", - "missing-delete-permission": "Нямате разрешение да изтриете този обект на съдържание.", - "missing-list-content-permission": "Нямате разрешение да изброявате обекти със съдържание.", - "missing-view-permission": "Нямате разрешение за преглед на този обект на съдържание.", - "missing-write-permission": "Нямате разрешение да промените този обект на съдържание.", "mongo-add-update-error": "Възникна грешка при добавяне или актуализиране на съдържание в базата данни.", "mongo-replace-error": "При актуализирането на съдържанието в базата данни възникна грешка.", "s3-upload-error": "При качването на файла {{filename}} в S3 възникна грешка." diff --git a/packages/h5p-server/assets/translations/mongo-s3-content-storage/bs.json b/packages/h5p-server/assets/translations/mongo-s3-content-storage/bs.json index b33fccb1d..d96487712 100644 --- a/packages/h5p-server/assets/translations/mongo-s3-content-storage/bs.json +++ b/packages/h5p-server/assets/translations/mongo-s3-content-storage/bs.json @@ -5,10 +5,6 @@ "filename-too-long": "Ime datoteke je predugo: {{filename}}", "illegal-filename": "Ime datoteke {{filename}} nije dozvoljeno.", "listing-content-error": "Došlo je do greške prilikom dobivanja liste objekata sadržaja", - "missing-delete-permission": "Nemate dozvolu za brisanje ovog objekta sadržaja.", - "missing-list-content-permission": "Nemate dozvolu za popis objekata sadržaja.", - "missing-view-permission": "Nemate dozvolu za pregled ovog sadržaja.", - "missing-write-permission": "Nemate dozvolu za promjenu ovog objekta sadržaja.", "mongo-add-update-error": "Došlo je do greške prilikom dodavanja ili ažuriranja sadržaja u bazi podataka.", "mongo-replace-error": "Došlo je do greške prilikom ažuriranja sadržaja u bazi podataka.", "s3-upload-error": "Došlo je do greške prilikom prijenosa datoteke {{filename}} na S3." diff --git a/packages/h5p-server/assets/translations/mongo-s3-content-storage/ca.json b/packages/h5p-server/assets/translations/mongo-s3-content-storage/ca.json index 7ea48f6a2..58e0e283f 100644 --- a/packages/h5p-server/assets/translations/mongo-s3-content-storage/ca.json +++ b/packages/h5p-server/assets/translations/mongo-s3-content-storage/ca.json @@ -5,10 +5,6 @@ "filename-too-long": "El nom del fitxer és massa llarg: {{filename}}", "illegal-filename": "No es permet el nom de fitxer {{filename}}", "listing-content-error": "S'ha produït un error en obtenir la llista d'objectes de contingut", - "missing-delete-permission": "No teniu permís per suprimir aquest objecte de contingut.", - "missing-list-content-permission": "No teniu permís per llistar objectes de contingut.", - "missing-view-permission": "No teniu permís per veure aquest objecte de contingut.", - "missing-write-permission": "No teniu permís per canviar aquest objecte de contingut.", "mongo-add-update-error": "S'ha produït un error en afegir o actualitzar contingut a la base de dades.", "mongo-replace-error": "S'ha produït un error en actualitzar el contingut de la base de dades.", "s3-upload-error": "S'ha produït un error en penjar el fitxer {{filename}} a S3." diff --git a/packages/h5p-server/assets/translations/mongo-s3-content-storage/cs.json b/packages/h5p-server/assets/translations/mongo-s3-content-storage/cs.json index 83c1b9b74..8e823efc1 100644 --- a/packages/h5p-server/assets/translations/mongo-s3-content-storage/cs.json +++ b/packages/h5p-server/assets/translations/mongo-s3-content-storage/cs.json @@ -5,10 +5,6 @@ "filename-too-long": "Název souboru je příliš dlouhý: {{filename}}", "illegal-filename": "Název souboru {{filename}} není povolen.", "listing-content-error": "Při načítání seznamu objektů obsahu došlo k chybě", - "missing-delete-permission": "Nemáte oprávnění k odstranění tohoto objektu obsahu.", - "missing-list-content-permission": "Nemáte oprávnění k seznamu objektů obsahu.", - "missing-view-permission": "Pro zobrazení tohoto objektu obsahu nemáte oprávnění.", - "missing-write-permission": "Nemáte oprávnění ke změně tohoto objektu obsahu.", "mongo-add-update-error": "Při přidávání nebo aktualizaci obsahu v databázi došlo k chybě.", "mongo-replace-error": "Při aktualizaci obsahu v databázi došlo k chybě.", "s3-upload-error": "Při nahrávání souboru {{filename}} do S3 došlo k chybě." diff --git a/packages/h5p-server/assets/translations/mongo-s3-content-storage/de.json b/packages/h5p-server/assets/translations/mongo-s3-content-storage/de.json index a1dbb6ab9..65588e1f2 100644 --- a/packages/h5p-server/assets/translations/mongo-s3-content-storage/de.json +++ b/packages/h5p-server/assets/translations/mongo-s3-content-storage/de.json @@ -5,10 +5,6 @@ "filename-too-long": "Der Dateiname ist zu lang: {{filename}}", "illegal-filename": "Der Dateiname {{filename}} ist nicht erlaubt.", "listing-content-error": "Es ist ein Fehler beim Auflisten der Inhalte aufgetreten.", - "missing-delete-permission": "Du hast nicht die erforderliche Berechtigung, diesen Inhalt zu löschen.", - "missing-list-content-permission": "Du hast nicht die Berechtigung Inhalte aufzulisten.", - "missing-view-permission": "Du hast nicht die Berechtigung diesen Inhalt anzusehen.", - "missing-write-permission": "Du hast nicht die Berechtigung diesen Inhalt zu bearbeiten.", "mongo-add-update-error": "Es ist ein Fehler beim Hinzufügen oder Aktualisieren des Inhalts in der Datenbank aufgetreten.", "mongo-replace-error": "Es ist ein Fehler beim Aktualisieren des Inhalts in der Datenbank aufgetreten.", "s3-upload-error": "Es ist ein Fehler beim Hochladen der Datei {{filename}} nach S3 aufgetreten." diff --git a/packages/h5p-server/assets/translations/mongo-s3-content-storage/el.json b/packages/h5p-server/assets/translations/mongo-s3-content-storage/el.json index 5ad4a0f7a..ff4fc2061 100644 --- a/packages/h5p-server/assets/translations/mongo-s3-content-storage/el.json +++ b/packages/h5p-server/assets/translations/mongo-s3-content-storage/el.json @@ -5,10 +5,6 @@ "filename-too-long": "Το όνομα αρχείου είναι πολύ μεγάλο: {{filename}}", "illegal-filename": "Το όνομα αρχείου {{filename}} δεν επιτρέπεται.", "listing-content-error": "Παρουσιάστηκε σφάλμα κατά τη λήψη της λίστας αντικειμένων περιεχομένου", - "missing-delete-permission": "Δεν έχετε άδεια να διαγράψετε αυτό το αντικείμενο περιεχομένου.", - "missing-list-content-permission": "Δεν έχετε άδεια για τη λίστα αντικειμένων περιεχομένου.", - "missing-view-permission": "Δεν έχετε άδεια για προβολή αυτού του αντικειμένου περιεχομένου.", - "missing-write-permission": "Δεν έχετε άδεια να αλλάξετε αυτό το αντικείμενο περιεχομένου.", "mongo-add-update-error": "Παρουσιάστηκε σφάλμα κατά την προσθήκη ή την ενημέρωση περιεχομένου στη βάση δεδομένων.", "mongo-replace-error": "Παρουσιάστηκε σφάλμα κατά την ενημέρωση του περιεχομένου στη βάση δεδομένων.", "s3-upload-error": "Παρουσιάστηκε σφάλμα κατά τη μεταφόρτωση του αρχείου {{filename}} στο S3." diff --git a/packages/h5p-server/assets/translations/mongo-s3-content-storage/en.json b/packages/h5p-server/assets/translations/mongo-s3-content-storage/en.json index 93b42ed93..89e5b2043 100644 --- a/packages/h5p-server/assets/translations/mongo-s3-content-storage/en.json +++ b/packages/h5p-server/assets/translations/mongo-s3-content-storage/en.json @@ -5,10 +5,6 @@ "filename-too-long": "The filename is too long: {{filename}}", "illegal-filename": "The filename {{filename}} is not allowed.", "listing-content-error": "There was an error while getting the list of content objects", - "missing-delete-permission": "You do not have permission to delete this content object.", - "missing-list-content-permission": "You do not have permission to list content objects.", - "missing-view-permission": "You do not have permission to view this content object.", - "missing-write-permission": "You do not have permission to change this content object.", "mongo-add-update-error": "There was an error while adding or updating content in the database.", "mongo-replace-error": "There was an error while updating content in the database.", "s3-upload-error": "There was an error while uploading the file {{filename}} to S3." diff --git a/packages/h5p-server/assets/translations/mongo-s3-content-storage/es.json b/packages/h5p-server/assets/translations/mongo-s3-content-storage/es.json index 36987e261..8e473acf4 100644 --- a/packages/h5p-server/assets/translations/mongo-s3-content-storage/es.json +++ b/packages/h5p-server/assets/translations/mongo-s3-content-storage/es.json @@ -5,10 +5,6 @@ "filename-too-long": "El nombre del archivo es demasiado largo: {{filename}}", "illegal-filename": "El nombre de archivo {{filename}} no está permitido.", "listing-content-error": "Se produjo un error al obtener la lista de objetos de contenido", - "missing-delete-permission": "No tiene permiso para eliminar este objeto de contenido.", - "missing-list-content-permission": "No tiene permiso para mostrar objetos de contenido.", - "missing-view-permission": "No tiene permiso para ver este objeto de contenido.", - "missing-write-permission": "No tiene permiso para cambiar este objeto de contenido.", "mongo-add-update-error": "Hubo un error al agregar o actualizar contenido en la base de datos.", "mongo-replace-error": "Hubo un error al actualizar el contenido de la base de datos.", "s3-upload-error": "Hubo un error al cargar el archivo {{filename}} a S3." diff --git a/packages/h5p-server/assets/translations/mongo-s3-content-storage/et.json b/packages/h5p-server/assets/translations/mongo-s3-content-storage/et.json index d7edb75ab..bb3ae1b73 100644 --- a/packages/h5p-server/assets/translations/mongo-s3-content-storage/et.json +++ b/packages/h5p-server/assets/translations/mongo-s3-content-storage/et.json @@ -5,10 +5,6 @@ "filename-too-long": "Failinimi on liiga pikk: {{filename}}", "illegal-filename": "Failinimi {{filename}} pole lubatud.", "listing-content-error": "Sisuobjektide loendi hankimisel ilmnes viga", - "missing-delete-permission": "Teil pole luba selle sisuobjekti kustutamiseks.", - "missing-list-content-permission": "Teil pole luba sisuobjektide loetlemiseks.", - "missing-view-permission": "Teil pole selle sisuobjekti vaatamiseks luba.", - "missing-write-permission": "Teil pole luba selle sisuobjekti muutmiseks.", "mongo-add-update-error": "Andmebaasi sisu lisamisel või värskendamisel tekkis viga.", "mongo-replace-error": "Andmebaasi sisu värskendamisel ilmnes viga.", "s3-upload-error": "{{filename}} S3 üleslaadimisel ilmnes viga." diff --git a/packages/h5p-server/assets/translations/mongo-s3-content-storage/eu.json b/packages/h5p-server/assets/translations/mongo-s3-content-storage/eu.json index 3bb2db796..0b2a5cd91 100644 --- a/packages/h5p-server/assets/translations/mongo-s3-content-storage/eu.json +++ b/packages/h5p-server/assets/translations/mongo-s3-content-storage/eu.json @@ -5,10 +5,6 @@ "filename-too-long": "Fitxategi izena luzeegia da: {{filename}}", "illegal-filename": "{{filename}} fitxategi-izena ez da onartzen.", "listing-content-error": "Akats bat izan da eduki objektuen zerrenda eskuratzean", - "missing-delete-permission": "Ez duzu eduki-objektu hau ezabatzeko baimenik.", - "missing-list-content-permission": "Ez duzu edukiaren objektuak zerrendatzeko baimenik.", - "missing-view-permission": "Ez duzu eduki-objektu hau ikusteko baimenik.", - "missing-write-permission": "Ez duzu baimenik eduki objektu hau aldatzeko.", "mongo-add-update-error": "Errore bat gertatu da datu basean edukia gehitzean edo eguneratzean.", "mongo-replace-error": "Errore bat gertatu da datu-baseko edukia eguneratzean.", "s3-upload-error": "Errorea gertatu da {{filename}} fitxategia S3ra kargatzean." diff --git a/packages/h5p-server/assets/translations/mongo-s3-content-storage/fi.json b/packages/h5p-server/assets/translations/mongo-s3-content-storage/fi.json index 1cc78c70d..d7b2ca13d 100644 --- a/packages/h5p-server/assets/translations/mongo-s3-content-storage/fi.json +++ b/packages/h5p-server/assets/translations/mongo-s3-content-storage/fi.json @@ -5,10 +5,6 @@ "filename-too-long": "Tiedostonimi on liian pitkä: {{filename}}", "illegal-filename": "Tiedostonimi {{filename}} ei ole sallittu.", "listing-content-error": "Sisältöobjektien luettelon noutamisessa tapahtui virhe", - "missing-delete-permission": "Sinulla ei ole lupaa poistaa tätä sisältöobjektia.", - "missing-list-content-permission": "Sinulla ei ole lupaa luetteloida sisältöobjekteja.", - "missing-view-permission": "Sinulla ei ole lupaa tarkastella tätä sisältöobjektia.", - "missing-write-permission": "Sinulla ei ole lupaa muuttaa tätä sisältöobjektia.", "mongo-add-update-error": "Tietokannan sisältöä lisätessä tai päivitettäessä tapahtui virhe.", "mongo-replace-error": "Tietokannan sisältöä päivitettäessä tapahtui virhe.", "s3-upload-error": "{{filename}} lataamisessa S3: een tapahtui virhe." diff --git a/packages/h5p-server/assets/translations/mongo-s3-content-storage/fr.json b/packages/h5p-server/assets/translations/mongo-s3-content-storage/fr.json index b12d431db..7c3d97727 100644 --- a/packages/h5p-server/assets/translations/mongo-s3-content-storage/fr.json +++ b/packages/h5p-server/assets/translations/mongo-s3-content-storage/fr.json @@ -5,10 +5,6 @@ "filename-too-long": "Le nom du fichier est trop long: {{filename}}", "illegal-filename": "Le nom de fichier {{filename}} n'est pas autorisé.", "listing-content-error": "Une erreur s'est produite lors de l'obtention de la liste des objets de contenu", - "missing-delete-permission": "Vous n'êtes pas autorisé à supprimer cet objet de contenu.", - "missing-list-content-permission": "Vous n'êtes pas autorisé à répertorier les objets de contenu.", - "missing-view-permission": "Vous n'êtes pas autorisé à afficher cet objet de contenu.", - "missing-write-permission": "Vous n'êtes pas autorisé à modifier cet objet de contenu.", "mongo-add-update-error": "Une erreur s'est produite lors de l'ajout ou de la mise à jour du contenu dans la base de données.", "mongo-replace-error": "Une erreur s'est produite lors de la mise à jour du contenu de la base de données.", "s3-upload-error": "Une erreur s'est produite lors du téléchargement du fichier {{filename}} vers S3." diff --git a/packages/h5p-server/assets/translations/mongo-s3-content-storage/gl.json b/packages/h5p-server/assets/translations/mongo-s3-content-storage/gl.json index a07257f4b..dd0b782c9 100644 --- a/packages/h5p-server/assets/translations/mongo-s3-content-storage/gl.json +++ b/packages/h5p-server/assets/translations/mongo-s3-content-storage/gl.json @@ -5,10 +5,6 @@ "filename-too-long": "O nome do ficheiro é demasiado longo: {{filename}}", "illegal-filename": "O nome de ficheiro ({{filename}})non está permitido.", "listing-content-error": "Produciuse un erro ao obter a lista de obxectos de contido", - "missing-delete-permission": "Non ten permiso para eliminar este obxecto de contido.", - "missing-list-content-permission": "Non ten permiso para listar obxectos de contido.", - "missing-view-permission": "Non ten permiso para ver este obxecto de contido.", - "missing-write-permission": "Non ten permiso para cambiar este obxecto de contido.", "mongo-add-update-error": "Produciuse un erro ao engadir ou actualizar contido na base de datos.", "mongo-replace-error": "Produciuse un erro ao actualizar o contido da base de datos.", "s3-upload-error": "Produciuse un erro ao enviar o ficheiro {{filename}} a S3." diff --git a/packages/h5p-server/assets/translations/mongo-s3-content-storage/it.json b/packages/h5p-server/assets/translations/mongo-s3-content-storage/it.json index 25617ea34..a66e8800a 100644 --- a/packages/h5p-server/assets/translations/mongo-s3-content-storage/it.json +++ b/packages/h5p-server/assets/translations/mongo-s3-content-storage/it.json @@ -5,10 +5,6 @@ "filename-too-long": "Il nome del file è troppo lungo: {{filename}}", "illegal-filename": "Il nome del file {{filename}} non è consentito.", "listing-content-error": "Si è verificato un errore durante il recupero dell'elenco degli oggetti contenuto", - "missing-delete-permission": "Non disponi dell'autorizzazione per eliminare questo oggetto contenuto.", - "missing-list-content-permission": "Non si dispone dell'autorizzazione per elencare gli oggetti contenuto.", - "missing-view-permission": "Non disponi dell'autorizzazione per visualizzare questo oggetto contenuto.", - "missing-write-permission": "Non disponi dell'autorizzazione per modificare questo oggetto contenuto.", "mongo-add-update-error": "Si è verificato un errore durante l'aggiunta o l'aggiornamento del contenuto nel database.", "mongo-replace-error": "Si è verificato un errore durante l'aggiornamento del contenuto nel database.", "s3-upload-error": "Si è verificato un errore durante il caricamento del file {{filename}} su S3." diff --git a/packages/h5p-server/assets/translations/mongo-s3-content-storage/ja.json b/packages/h5p-server/assets/translations/mongo-s3-content-storage/ja.json index 1fd90a773..e043b1382 100644 --- a/packages/h5p-server/assets/translations/mongo-s3-content-storage/ja.json +++ b/packages/h5p-server/assets/translations/mongo-s3-content-storage/ja.json @@ -5,10 +5,6 @@ "filename-too-long": "ファイル名が長すぎます: {{filename}}", "illegal-filename": "ファイル名{{filename}}は許可されていません。", "listing-content-error": "コンテンツオブジェクトのリストの取得中にエラーが発生しました", - "missing-delete-permission": "このコンテンツオブジェクトを削除する権限がありません。", - "missing-list-content-permission": "コンテンツオブジェクトを一覧表示する権限がありません。", - "missing-view-permission": "このコンテンツオブジェクトを表示する権限がありません。", - "missing-write-permission": "このコンテンツオブジェクトを変更する権限がありません。", "mongo-add-update-error": "データベースのコンテンツの追加または更新中にエラーが発生しました。", "mongo-replace-error": "データベースのコンテンツの更新中にエラーが発生しました。", "s3-upload-error": "{{filename}}をS3にアップロード中にエラーが発生しました。" diff --git a/packages/h5p-server/assets/translations/mongo-s3-content-storage/km.json b/packages/h5p-server/assets/translations/mongo-s3-content-storage/km.json index 88886c1d0..d4ee6da9e 100644 --- a/packages/h5p-server/assets/translations/mongo-s3-content-storage/km.json +++ b/packages/h5p-server/assets/translations/mongo-s3-content-storage/km.json @@ -5,10 +5,6 @@ "filename-too-long": "ឈ្មោះឯកសារវែងពេក៖ {{filename}}", "illegal-filename": "ឈ្មោះឯកសារ {{filename}} មិនត្រូវបានអនុញ្ញាតទេ។", "listing-content-error": "មានកំហុសពេលកំពុងទទួលបញ្ជីវត្ថុមាតិកា", - "missing-delete-permission": "អ្នកមិនមានសិទ្ធិក្នុងការលុបវត្ថុមាតិកានេះទេ។", - "missing-list-content-permission": "អ្នកមិនមានសិទ្ធិក្នុងការរាយវត្ថុមាតិកា។", - "missing-view-permission": "អ្នកគ្មានសិទ្ធិមើលវត្ថុមាតិកានេះទេ។", - "missing-write-permission": "អ្នកគ្មានសិទ្ធិផ្លាស់ប្តូរវត្ថុមាតិកានេះទេ។", "mongo-add-update-error": "មានកំហុសមួយខណៈពេលបន្ថែមឬធ្វើបច្ចុប្បន្នភាពមាតិកានៅក្នុងឃ្លាំងទិន្នន័យ។", "mongo-replace-error": "មានកំហុសពេលកំពុងធ្វើបច្ចុប្បន្នភាពមាតិកាក្នុងឃ្លាំងទិន្នន័យ។", "s3-upload-error": "មានកំហុសពេលផ្ទុកឯកសារ {{filename}} ទៅស៊ី ៣ ។" diff --git a/packages/h5p-server/assets/translations/mongo-s3-content-storage/ko.json b/packages/h5p-server/assets/translations/mongo-s3-content-storage/ko.json index 22974bacd..fcf32a1a5 100644 --- a/packages/h5p-server/assets/translations/mongo-s3-content-storage/ko.json +++ b/packages/h5p-server/assets/translations/mongo-s3-content-storage/ko.json @@ -5,10 +5,6 @@ "filename-too-long": "파일 이름이 너무 깁니다 : {{filename}}", "illegal-filename": "파일 이름 {{filename}} 은 허용되지 않습니다.", "listing-content-error": "콘텐츠 개체 목록을 가져 오는 동안 오류가 발생했습니다.", - "missing-delete-permission": "이 콘텐츠 개체를 삭제할 권한이 없습니다.", - "missing-list-content-permission": "콘텐츠 개체를 나열 할 권한이 없습니다.", - "missing-view-permission": "이 콘텐츠 개체를 볼 수있는 권한이 없습니다.", - "missing-write-permission": "이 콘텐츠 개체를 변경할 권한이 없습니다.", "mongo-add-update-error": "데이터베이스에서 콘텐츠를 추가하거나 업데이트하는 동안 오류가 발생했습니다.", "mongo-replace-error": "데이터베이스의 콘텐츠를 업데이트하는 동안 오류가 발생했습니다.", "s3-upload-error": "{{filename}} 파일을 S3에 업로드하는 동안 오류가 발생했습니다." diff --git a/packages/h5p-server/assets/translations/mongo-s3-content-storage/nl.json b/packages/h5p-server/assets/translations/mongo-s3-content-storage/nl.json index 7096dd58a..f0294a06e 100644 --- a/packages/h5p-server/assets/translations/mongo-s3-content-storage/nl.json +++ b/packages/h5p-server/assets/translations/mongo-s3-content-storage/nl.json @@ -5,10 +5,6 @@ "filename-too-long": "De bestandsnaam is te lang: {{filename}}", "illegal-filename": "De bestandsnaam {{filename}} is niet toegestaan.", "listing-content-error": "Er is een fout opgetreden bij het ophalen van de lijst met inhoudsobjecten", - "missing-delete-permission": "U heeft geen toestemming om dit inhoudsobject te verwijderen.", - "missing-list-content-permission": "U bent niet gemachtigd om inhoudsobjecten weer te geven.", - "missing-view-permission": "U heeft geen toestemming om dit inhoudsobject te bekijken.", - "missing-write-permission": "U heeft geen toestemming om dit inhoudsobject te wijzigen.", "mongo-add-update-error": "Er is een fout opgetreden bij het toevoegen of bijwerken van inhoud in de database.", "mongo-replace-error": "Er is een fout opgetreden bij het bijwerken van inhoud in de database.", "s3-upload-error": "Er is een fout opgetreden bij het uploaden van het bestand {{filename}} naar S3." diff --git a/packages/h5p-server/assets/translations/mongo-s3-content-storage/pt.json b/packages/h5p-server/assets/translations/mongo-s3-content-storage/pt.json index b468f83b9..65863473d 100644 --- a/packages/h5p-server/assets/translations/mongo-s3-content-storage/pt.json +++ b/packages/h5p-server/assets/translations/mongo-s3-content-storage/pt.json @@ -5,10 +5,6 @@ "filename-too-long": "O nome do arquivo é muito longo: {{filename}}", "illegal-filename": "O nome de arquivo {{filename}} não é permitido.", "listing-content-error": "Ocorreu um erro ao obter a lista de objetos de conteúdo", - "missing-delete-permission": "Você não tem permissão para excluir este objeto de conteúdo.", - "missing-list-content-permission": "Você não tem permissão para listar objetos de conteúdo.", - "missing-view-permission": "Você não tem permissão para visualizar este objeto de conteúdo.", - "missing-write-permission": "Você não tem permissão para alterar este objeto de conteúdo.", "mongo-add-update-error": "Ocorreu um erro ao adicionar ou atualizar o conteúdo no banco de dados.", "mongo-replace-error": "Ocorreu um erro ao atualizar o conteúdo do banco de dados.", "s3-upload-error": "Ocorreu um erro ao enviar o arquivo {{filename}} para S3." diff --git a/packages/h5p-server/assets/translations/mongo-s3-content-storage/ru.json b/packages/h5p-server/assets/translations/mongo-s3-content-storage/ru.json index a92d048c8..0546d74d3 100644 --- a/packages/h5p-server/assets/translations/mongo-s3-content-storage/ru.json +++ b/packages/h5p-server/assets/translations/mongo-s3-content-storage/ru.json @@ -5,10 +5,6 @@ "filename-too-long": "Имя файла слишком длинное: {{filename}}", "illegal-filename": "Имя файла {{filename}} не допускается.", "listing-content-error": "При получении списка объектов содержимого произошла ошибка", - "missing-delete-permission": "У вас нет разрешения на удаление этого объекта содержимого.", - "missing-list-content-permission": "У вас нет разрешения на перечисление объектов содержимого.", - "missing-view-permission": "У вас нет разрешения на просмотр этого объекта содержимого.", - "missing-write-permission": "У вас нет разрешения на изменение этого объекта содержимого.", "mongo-add-update-error": "Произошла ошибка при добавлении или обновлении содержимого в базе данных.", "mongo-replace-error": "При обновлении содержимого в базе данных произошла ошибка.", "s3-upload-error": "При загрузке файла {{filename}} на S3 произошла ошибка." diff --git a/packages/h5p-server/assets/translations/mongo-s3-content-storage/sl.json b/packages/h5p-server/assets/translations/mongo-s3-content-storage/sl.json index f0ad94152..9468291e4 100644 --- a/packages/h5p-server/assets/translations/mongo-s3-content-storage/sl.json +++ b/packages/h5p-server/assets/translations/mongo-s3-content-storage/sl.json @@ -5,10 +5,6 @@ "filename-too-long": "Ime datoteke je predolgo: {{filename}}", "illegal-filename": "Ime datoteke {{filename}} ni dovoljeno.", "listing-content-error": "Pri pridobivanju seznama predmetov vsebine je prišlo do napake", - "missing-delete-permission": "Nimate dovoljenja za brisanje tega predmeta vsebine.", - "missing-list-content-permission": "Nimate dovoljenja za naštevanje predmetov vsebine.", - "missing-view-permission": "Nimate dovoljenja za ogled tega vsebinskega predmeta.", - "missing-write-permission": "Nimate dovoljenja za spreminjanje tega predmeta vsebine.", "mongo-add-update-error": "Pri dodajanju ali posodabljanju vsebine v zbirki podatkov je prišlo do napake.", "mongo-replace-error": "Pri posodabljanju vsebine v bazi je prišlo do napake.", "s3-upload-error": "{{filename}} na S3 je prišlo do napake." diff --git a/packages/h5p-server/assets/translations/mongo-s3-content-storage/sv.json b/packages/h5p-server/assets/translations/mongo-s3-content-storage/sv.json index a9b55bac4..f54ad1e00 100644 --- a/packages/h5p-server/assets/translations/mongo-s3-content-storage/sv.json +++ b/packages/h5p-server/assets/translations/mongo-s3-content-storage/sv.json @@ -5,10 +5,6 @@ "filename-too-long": "Filnamnet är för långt: {{filename}}", "illegal-filename": "Filnamnet {{filename}} är inte tillåtet.", "listing-content-error": "Det uppstod ett fel när listan över innehållsobjekt hämtades", - "missing-delete-permission": "Du har inte behörighet att radera detta innehållsobjekt.", - "missing-list-content-permission": "Du har inte behörighet att lista innehållsobjekt.", - "missing-view-permission": "Du har inte behörighet att visa detta innehållsobjekt.", - "missing-write-permission": "Du har inte behörighet att ändra detta innehållsobjekt.", "mongo-add-update-error": "Det uppstod ett fel när du lade till eller uppdaterade innehåll i databasen.", "mongo-replace-error": "Det uppstod ett fel när innehållet i databasen uppdaterades.", "s3-upload-error": "Det uppstod ett fel när filen {{filename}} till S3 överfördes." diff --git a/packages/h5p-server/assets/translations/mongo-s3-content-storage/te.json b/packages/h5p-server/assets/translations/mongo-s3-content-storage/te.json index 7be74d201..ae77aa0b5 100644 --- a/packages/h5p-server/assets/translations/mongo-s3-content-storage/te.json +++ b/packages/h5p-server/assets/translations/mongo-s3-content-storage/te.json @@ -5,10 +5,6 @@ "filename-too-long": "ఫైల్ పేరు చాలా పొడవుగా ఉంది: {{filename}}", "illegal-filename": "ఫైల్ పేరు {{filename}} అనుమతించబడదు.", "listing-content-error": "కంటెంట్ ఆబ్జెక్ట్‌ల జాబితాను తీసుకువస్తున్నప్పుడు లోపం ఏర్పడింది", - "missing-delete-permission": "ఈ కంటెంట్ ఆబ్జెక్ట్‌ని తొలగించడానికి మీకు అనుమతి లేదు.", - "missing-list-content-permission": "కంటెంట్ ఆబ్జెక్ట్‌లను జాబితా చేయడానికి మీకు అనుమతి లేదు.", - "missing-view-permission": "ఈ కంటెంట్ ఆబ్జెక్ట్‌ని వీక్షించడానికి మీకు అనుమతి లేదు.", - "missing-write-permission": "ఈ కంటెంట్ ఆబ్జెక్ట్‌ని మార్చడానికి మీకు అనుమతి లేదు.", "mongo-add-update-error": "డేటాబేస్‌లో కంటెంట్‌ని జోడించేటప్పుడు లేదా అప్‌డేట్ చేస్తున్నప్పుడు లోపం ఏర్పడింది.", "mongo-replace-error": "డేటాబేస్‌లో కంటెంట్‌ని అప్‌డేట్ చేస్తున్నప్పుడు లోపం ఏర్పడింది.", "s3-upload-error": "ఫైల్ {{filename}}ని S3కి అప్‌లోడ్ చేస్తున్నప్పుడు లోపం ఏర్పడింది." diff --git a/packages/h5p-server/assets/translations/mongo-s3-content-storage/tr.json b/packages/h5p-server/assets/translations/mongo-s3-content-storage/tr.json index 17ad219a2..6763f8f88 100644 --- a/packages/h5p-server/assets/translations/mongo-s3-content-storage/tr.json +++ b/packages/h5p-server/assets/translations/mongo-s3-content-storage/tr.json @@ -5,10 +5,6 @@ "filename-too-long": "Dosya adı çok uzun: {{filename}}", "illegal-filename": "{{filename}} dosya adına izin verilmiyor.", "listing-content-error": "İçerik nesnelerinin listesi alınırken bir hata oluştu", - "missing-delete-permission": "Bu içerik nesnesini silme izniniz yok.", - "missing-list-content-permission": "İçerik nesnelerini listeleme izniniz yok.", - "missing-view-permission": "Bu dosyayı görüntüleme izniniz yok.", - "missing-write-permission": "Bu içerik nesnesini değiştirme izniniz yok.", "mongo-add-update-error": "Veritabanına içerik eklenirken veya güncellenirken bir hata oluştu.", "mongo-replace-error": "Veritabanındaki içerik güncellenirken bir hata oluştu.", "s3-upload-error": "{{filename}} dosyasını S3'e yüklerken bir hata oluştu." diff --git a/packages/h5p-server/assets/translations/mongo-s3-content-storage/zh.json b/packages/h5p-server/assets/translations/mongo-s3-content-storage/zh.json index d66c060c9..2587ab5bb 100644 --- a/packages/h5p-server/assets/translations/mongo-s3-content-storage/zh.json +++ b/packages/h5p-server/assets/translations/mongo-s3-content-storage/zh.json @@ -5,10 +5,6 @@ "filename-too-long": "文件名太长: {{filename}}", "illegal-filename": "不允许使用文件名{{filename}}", "listing-content-error": "获取内容对象列表时出错", - "missing-delete-permission": "您无权删除此内容对象。", - "missing-list-content-permission": "您无权列出内容对象。", - "missing-view-permission": "您无权查看此内容对象。", - "missing-write-permission": "您无权更改此内容对象。", "mongo-add-update-error": "在数据库中添加或更新内容时出错。", "mongo-replace-error": "更新数据库中的内容时出错。", "s3-upload-error": "将文件{{filename}}上载到S3时出错。" diff --git a/packages/h5p-server/assets/translations/s3-temporary-storage/bg.json b/packages/h5p-server/assets/translations/s3-temporary-storage/bg.json index 07281034a..5323850b5 100644 --- a/packages/h5p-server/assets/translations/s3-temporary-storage/bg.json +++ b/packages/h5p-server/assets/translations/s3-temporary-storage/bg.json @@ -1,8 +1,5 @@ { "deleting-file-error": "При изтриването на този файл възникна грешка.", "file-not-found": "Файлът не съществува.", - "missing-delete-permission": "Нямате разрешение да изтриете този файл.", - "missing-view-permission": "Нямате разрешение за преглед на този файл.", - "missing-write-permission": "Нямате разрешение за запазване на временни файлове.", "s3-upload-error": "При качването на временния файл в S3 възникна грешка." } diff --git a/packages/h5p-server/assets/translations/s3-temporary-storage/bs.json b/packages/h5p-server/assets/translations/s3-temporary-storage/bs.json index ed5f94a51..a008a8e7f 100644 --- a/packages/h5p-server/assets/translations/s3-temporary-storage/bs.json +++ b/packages/h5p-server/assets/translations/s3-temporary-storage/bs.json @@ -1,8 +1,5 @@ { "deleting-file-error": "Došlo je do greške prilikom brisanja ove datoteke.", "file-not-found": "Datoteka ne postoji.", - "missing-delete-permission": "Nemate dozvolu za brisanje ove datoteke.", - "missing-view-permission": "Nemate dozvolu za pregled ove datoteke.", - "missing-write-permission": "Nemate dozvolu za spremanje privremenih datoteka.", "s3-upload-error": "Došlo je do greške prilikom prijenosa privremene datoteke na S3." } diff --git a/packages/h5p-server/assets/translations/s3-temporary-storage/ca.json b/packages/h5p-server/assets/translations/s3-temporary-storage/ca.json index 436c457b0..8f4321508 100644 --- a/packages/h5p-server/assets/translations/s3-temporary-storage/ca.json +++ b/packages/h5p-server/assets/translations/s3-temporary-storage/ca.json @@ -1,8 +1,5 @@ { "deleting-file-error": "S'ha produït un error en suprimir aquest fitxer.", "file-not-found": "El fitxer no existeix.", - "missing-delete-permission": "No teniu permís per suprimir aquest fitxer.", - "missing-view-permission": "No teniu permís per veure aquest fitxer.", - "missing-write-permission": "No teniu permís per desar fitxers temporals.", "s3-upload-error": "S'ha produït un error en carregar el fitxer temporal a S3." } diff --git a/packages/h5p-server/assets/translations/s3-temporary-storage/cs.json b/packages/h5p-server/assets/translations/s3-temporary-storage/cs.json index abcb70316..ce672ad96 100644 --- a/packages/h5p-server/assets/translations/s3-temporary-storage/cs.json +++ b/packages/h5p-server/assets/translations/s3-temporary-storage/cs.json @@ -1,8 +1,5 @@ { "deleting-file-error": "Při mazání tohoto souboru došlo k chybě.", "file-not-found": "Soubor neexistuje.", - "missing-delete-permission": "K odstranění tohoto souboru nemáte oprávnění.", - "missing-view-permission": "K prohlížení tohoto souboru nemáte oprávnění.", - "missing-write-permission": "Nemáte oprávnění k ukládání dočasných souborů.", "s3-upload-error": "Při nahrávání dočasného souboru na S3 došlo k chybě." } diff --git a/packages/h5p-server/assets/translations/s3-temporary-storage/de.json b/packages/h5p-server/assets/translations/s3-temporary-storage/de.json index 01fefa1ed..d49c4464a 100644 --- a/packages/h5p-server/assets/translations/s3-temporary-storage/de.json +++ b/packages/h5p-server/assets/translations/s3-temporary-storage/de.json @@ -1,8 +1,5 @@ { "deleting-file-error": "Es ist beim Löschen der Datei ein Fehler aufgetreten.", "file-not-found": "Diese Datei existiert nicht.", - "missing-delete-permission": "Du hast nicht die erforderliche Berechtigung, diese Datei zu löschen.", - "missing-view-permission": "Du hast nicht die Berechtigung, diese Datei anzusehen.", - "missing-write-permission": "Du hast nicht die Berechtigung, temporäre Dateien zu speichern.", "s3-upload-error": "Es ist ein Fehler beim Hochladen der temporären Datei nach S3 aufgetreten." } diff --git a/packages/h5p-server/assets/translations/s3-temporary-storage/el.json b/packages/h5p-server/assets/translations/s3-temporary-storage/el.json index 208a3c83f..eef7168e3 100644 --- a/packages/h5p-server/assets/translations/s3-temporary-storage/el.json +++ b/packages/h5p-server/assets/translations/s3-temporary-storage/el.json @@ -1,8 +1,5 @@ { "deleting-file-error": "Παρουσιάστηκε σφάλμα κατά τη διαγραφή αυτού του αρχείου.", "file-not-found": "Το αρχείο δεν υπάρχει.", - "missing-delete-permission": "Δεν έχετε άδεια διαγραφής αυτού του αρχείου.", - "missing-view-permission": "Δεν έχετε άδεια για προβολή αυτού του αρχείου.", - "missing-write-permission": "Δεν έχετε άδεια αποθήκευσης προσωρινών αρχείων.", "s3-upload-error": "Παρουσιάστηκε σφάλμα κατά τη μεταφόρτωση του προσωρινού αρχείου στο S3." } diff --git a/packages/h5p-server/assets/translations/s3-temporary-storage/en.json b/packages/h5p-server/assets/translations/s3-temporary-storage/en.json index 7b78a9e26..8c9e64a5e 100644 --- a/packages/h5p-server/assets/translations/s3-temporary-storage/en.json +++ b/packages/h5p-server/assets/translations/s3-temporary-storage/en.json @@ -1,8 +1,5 @@ { "deleting-file-error": "There was an error while deleting this file.", "file-not-found": "The file does not exist.", - "missing-delete-permission": "You do not have permission to delete this file.", - "missing-view-permission": "You do not have permission to view this file.", - "missing-write-permission": "You do not have permission to save temporary files.", "s3-upload-error": "There was an error uploading the temporary file to S3." } diff --git a/packages/h5p-server/assets/translations/s3-temporary-storage/es.json b/packages/h5p-server/assets/translations/s3-temporary-storage/es.json index 98911f481..0251bcf52 100644 --- a/packages/h5p-server/assets/translations/s3-temporary-storage/es.json +++ b/packages/h5p-server/assets/translations/s3-temporary-storage/es.json @@ -1,8 +1,5 @@ { "deleting-file-error": "Hubo un error al eliminar este archivo.", "file-not-found": "El archivo no existe.", - "missing-delete-permission": "No tienes permiso para eliminar este archivo.", - "missing-view-permission": "No tienes permiso para ver este archivo.", - "missing-write-permission": "No tienes permiso para guardar archivos temporales.", "s3-upload-error": "Hubo un error al cargar el archivo temporal en S3." } diff --git a/packages/h5p-server/assets/translations/s3-temporary-storage/et.json b/packages/h5p-server/assets/translations/s3-temporary-storage/et.json index f72375a51..4479a600e 100644 --- a/packages/h5p-server/assets/translations/s3-temporary-storage/et.json +++ b/packages/h5p-server/assets/translations/s3-temporary-storage/et.json @@ -1,8 +1,5 @@ { "deleting-file-error": "Selle faili kustutamisel ilmnes viga.", "file-not-found": "Faili pole olemas.", - "missing-delete-permission": "Teil pole luba seda faili kustutada.", - "missing-view-permission": "Teil pole selle faili vaatamiseks luba.", - "missing-write-permission": "Teil pole luba ajutiste failide salvestamiseks.", "s3-upload-error": "Ajutise faili S3 üleslaadimisel ilmnes viga." } diff --git a/packages/h5p-server/assets/translations/s3-temporary-storage/eu.json b/packages/h5p-server/assets/translations/s3-temporary-storage/eu.json index 64c18f53b..5ba30fed5 100644 --- a/packages/h5p-server/assets/translations/s3-temporary-storage/eu.json +++ b/packages/h5p-server/assets/translations/s3-temporary-storage/eu.json @@ -1,8 +1,5 @@ { "deleting-file-error": "Errore bat gertatu da fitxategia ezabatzean.", "file-not-found": "Fitxategia ez da existitzen.", - "missing-delete-permission": "Ez duzu fitxategi hau ezabatzeko baimenik.", - "missing-view-permission": "Ez duzu fitxategi hau ikusteko baimenik.", - "missing-write-permission": "Ez duzu baimenik aldi baterako fitxategiak gordetzeko.", "s3-upload-error": "Errore bat gertatu da aldi baterako fitxategia S3ra kargatzean." } diff --git a/packages/h5p-server/assets/translations/s3-temporary-storage/fi.json b/packages/h5p-server/assets/translations/s3-temporary-storage/fi.json index 26dcb5a82..7f555b8d2 100644 --- a/packages/h5p-server/assets/translations/s3-temporary-storage/fi.json +++ b/packages/h5p-server/assets/translations/s3-temporary-storage/fi.json @@ -1,8 +1,5 @@ { "deleting-file-error": "Tiedoston poistamisessa tapahtui virhe.", "file-not-found": "Tiedostoa ei ole olemassa.", - "missing-delete-permission": "Sinulla ei ole lupaa poistaa tätä tiedostoa.", - "missing-view-permission": "Sinulla ei ole lupaa tarkastella tätä tiedostoa.", - "missing-write-permission": "Sinulla ei ole lupaa tallentaa väliaikaisia tiedostoja.", "s3-upload-error": "Tilapäisen tiedoston lataamisessa S3: een tapahtui virhe." } diff --git a/packages/h5p-server/assets/translations/s3-temporary-storage/fr.json b/packages/h5p-server/assets/translations/s3-temporary-storage/fr.json index 06331e415..0688202b6 100644 --- a/packages/h5p-server/assets/translations/s3-temporary-storage/fr.json +++ b/packages/h5p-server/assets/translations/s3-temporary-storage/fr.json @@ -1,8 +1,5 @@ { "deleting-file-error": "Une erreur s'est produite lors de la suppression de ce fichier.", "file-not-found": "Le fichier n'existe pas.", - "missing-delete-permission": "Vous n'êtes pas autorisé à supprimer ce fichier.", - "missing-view-permission": "Vous n'êtes pas autorisé à afficher ce fichier.", - "missing-write-permission": "Vous n'êtes pas autorisé à enregistrer des fichiers temporaires.", "s3-upload-error": "Une erreur s'est produite lors du téléchargement du fichier temporaire vers S3." } diff --git a/packages/h5p-server/assets/translations/s3-temporary-storage/gl.json b/packages/h5p-server/assets/translations/s3-temporary-storage/gl.json index 741d5009f..fc098a072 100644 --- a/packages/h5p-server/assets/translations/s3-temporary-storage/gl.json +++ b/packages/h5p-server/assets/translations/s3-temporary-storage/gl.json @@ -1,8 +1,5 @@ { "deleting-file-error": "Produciuse un erro ao eliminar este ficheiro.", "file-not-found": "O ficheiro non existe.", - "missing-delete-permission": "Non ten permiso para eliminar este ficheiro.", - "missing-view-permission": "Non ten permiso para ver este ficheiro.", - "missing-write-permission": "Non ten permiso para gardar ficheiros temporais.", "s3-upload-error": "Produciuse un erro ao enviar o ficheiro temporal a S3." } diff --git a/packages/h5p-server/assets/translations/s3-temporary-storage/it.json b/packages/h5p-server/assets/translations/s3-temporary-storage/it.json index 4b954e0a2..6331addac 100644 --- a/packages/h5p-server/assets/translations/s3-temporary-storage/it.json +++ b/packages/h5p-server/assets/translations/s3-temporary-storage/it.json @@ -1,8 +1,5 @@ { "deleting-file-error": "Si è verificato un errore durante l'eliminazione di questo file.", "file-not-found": "Il file non esiste.", - "missing-delete-permission": "Non disponi dell'autorizzazione per eliminare questo file.", - "missing-view-permission": "Non disponi dell'autorizzazione per visualizzare questo file.", - "missing-write-permission": "Non disponi dell'autorizzazione per salvare i file temporanei.", "s3-upload-error": "Si è verificato un errore durante il caricamento del file temporaneo su S3." } diff --git a/packages/h5p-server/assets/translations/s3-temporary-storage/ja.json b/packages/h5p-server/assets/translations/s3-temporary-storage/ja.json index f49ad753f..5b69cd2f0 100644 --- a/packages/h5p-server/assets/translations/s3-temporary-storage/ja.json +++ b/packages/h5p-server/assets/translations/s3-temporary-storage/ja.json @@ -1,8 +1,5 @@ { "deleting-file-error": "このファイルの削除中にエラーが発生しました。", "file-not-found": "ファイルが存在しません。", - "missing-delete-permission": "このファイルを削除する権限がありません。", - "missing-view-permission": "このファイルを表示する権限がありません。", - "missing-write-permission": "一時ファイルを保存する権限がありません。", "s3-upload-error": "S3への一時ファイルのアップロード中にエラーが発生しました。" } diff --git a/packages/h5p-server/assets/translations/s3-temporary-storage/km.json b/packages/h5p-server/assets/translations/s3-temporary-storage/km.json index 147cd358a..853a91c59 100644 --- a/packages/h5p-server/assets/translations/s3-temporary-storage/km.json +++ b/packages/h5p-server/assets/translations/s3-temporary-storage/km.json @@ -1,8 +1,5 @@ { "deleting-file-error": "មានកំហុសពេលលុបឯកសារនេះ។", "file-not-found": "មិនមានឯកសារទេ។", - "missing-delete-permission": "អ្នកគ្មានសិទ្ធិលុបឯកសារនេះទេ។", - "missing-view-permission": "អ្នកគ្មានសិទ្ធិមើលឯកសារនេះទេ។", - "missing-write-permission": "អ្នកមិនមានសិទ្ធិរក្សាទុកឯកសារបណ្តោះអាសន្នទេ។", "s3-upload-error": "មានកំហុសក្នុងការផ្ទុកឯកសារបណ្តោះអាសន្នទៅអេស ៣ ។" } diff --git a/packages/h5p-server/assets/translations/s3-temporary-storage/ko.json b/packages/h5p-server/assets/translations/s3-temporary-storage/ko.json index 75904f198..88bdbfb90 100644 --- a/packages/h5p-server/assets/translations/s3-temporary-storage/ko.json +++ b/packages/h5p-server/assets/translations/s3-temporary-storage/ko.json @@ -1,8 +1,5 @@ { "deleting-file-error": "이 파일을 삭제하는 중에 오류가 발생했습니다.", "file-not-found": "파일이 없습니다.", - "missing-delete-permission": "이 파일을 삭제할 권한이 없습니다.", - "missing-view-permission": "이 파일을 볼 수있는 권한이 없습니다.", - "missing-write-permission": "임시 파일을 저장할 권한이 없습니다.", "s3-upload-error": "S3에 임시 파일을 업로드하는 중에 오류가 발생했습니다." } diff --git a/packages/h5p-server/assets/translations/s3-temporary-storage/nl.json b/packages/h5p-server/assets/translations/s3-temporary-storage/nl.json index 645c8f8f0..a5ea26b3e 100644 --- a/packages/h5p-server/assets/translations/s3-temporary-storage/nl.json +++ b/packages/h5p-server/assets/translations/s3-temporary-storage/nl.json @@ -1,8 +1,5 @@ { "deleting-file-error": "Er is een fout opgetreden bij het verwijderen van dit bestand.", "file-not-found": "Het bestand bestaat niet.", - "missing-delete-permission": "U heeft geen toestemming om dit bestand te verwijderen.", - "missing-view-permission": "U heeft geen toestemming om dit bestand te bekijken.", - "missing-write-permission": "U heeft geen toestemming om tijdelijke bestanden op te slaan.", "s3-upload-error": "Er is een fout opgetreden bij het uploaden van het tijdelijke bestand naar S3." } diff --git a/packages/h5p-server/assets/translations/s3-temporary-storage/pt.json b/packages/h5p-server/assets/translations/s3-temporary-storage/pt.json index cc39eff41..454c2d4b0 100644 --- a/packages/h5p-server/assets/translations/s3-temporary-storage/pt.json +++ b/packages/h5p-server/assets/translations/s3-temporary-storage/pt.json @@ -1,8 +1,5 @@ { "deleting-file-error": "Ocorreu um erro ao excluir este arquivo.", "file-not-found": "O arquivo não existe.", - "missing-delete-permission": "Você não tem permissão para excluir este arquivo.", - "missing-view-permission": "Você não tem permissão para visualizar este arquivo.", - "missing-write-permission": "Você não tem permissão para salvar arquivos temporários.", "s3-upload-error": "Ocorreu um erro ao enviar o arquivo temporário para S3." } diff --git a/packages/h5p-server/assets/translations/s3-temporary-storage/ru.json b/packages/h5p-server/assets/translations/s3-temporary-storage/ru.json index 13c6c68dc..e412a5409 100644 --- a/packages/h5p-server/assets/translations/s3-temporary-storage/ru.json +++ b/packages/h5p-server/assets/translations/s3-temporary-storage/ru.json @@ -1,8 +1,5 @@ { "deleting-file-error": "При удалении этого файла произошла ошибка.", "file-not-found": "Файл не существует.", - "missing-delete-permission": "У вас нет разрешения на удаление этого файла.", - "missing-view-permission": "У вас нет разрешения на просмотр этого файла.", - "missing-write-permission": "У вас нет разрешения на сохранение временных файлов.", "s3-upload-error": "При загрузке временного файла на S3 произошла ошибка." } diff --git a/packages/h5p-server/assets/translations/s3-temporary-storage/sl.json b/packages/h5p-server/assets/translations/s3-temporary-storage/sl.json index ece9a4874..4cc0ba3b0 100644 --- a/packages/h5p-server/assets/translations/s3-temporary-storage/sl.json +++ b/packages/h5p-server/assets/translations/s3-temporary-storage/sl.json @@ -1,8 +1,5 @@ { "deleting-file-error": "Pri brisanju te datoteke je prišlo do napake.", "file-not-found": "Datoteka ne obstaja.", - "missing-delete-permission": "Nimate dovoljenja za brisanje te datoteke.", - "missing-view-permission": "Nimate dovoljenja za ogled te datoteke.", - "missing-write-permission": "Nimate dovoljenja za shranjevanje začasnih datotek.", "s3-upload-error": "Pri nalaganju začasne datoteke v S3 je prišlo do napake." } diff --git a/packages/h5p-server/assets/translations/s3-temporary-storage/sv.json b/packages/h5p-server/assets/translations/s3-temporary-storage/sv.json index 6f7b190ea..db11909ee 100644 --- a/packages/h5p-server/assets/translations/s3-temporary-storage/sv.json +++ b/packages/h5p-server/assets/translations/s3-temporary-storage/sv.json @@ -1,8 +1,5 @@ { "deleting-file-error": "Det uppstod ett fel när filen skulle raderas.", "file-not-found": "Filen finns inte.", - "missing-delete-permission": "Du har inte behörighet att radera den här filen.", - "missing-view-permission": "Du har inte behörighet att visa den här filen.", - "missing-write-permission": "Du har inte behörighet att spara tillfälliga filer.", "s3-upload-error": "Det gick inte att ladda upp den tillfälliga filen till S3." } diff --git a/packages/h5p-server/assets/translations/s3-temporary-storage/tr.json b/packages/h5p-server/assets/translations/s3-temporary-storage/tr.json index 33e4642eb..e3073c72a 100644 --- a/packages/h5p-server/assets/translations/s3-temporary-storage/tr.json +++ b/packages/h5p-server/assets/translations/s3-temporary-storage/tr.json @@ -1,8 +1,5 @@ { "deleting-file-error": "Bu dosya silinirken bir hata oluştu.", "file-not-found": "Dosya bulunamadı.", - "missing-delete-permission": "Bu dosyayı silme yetkiniz yok.", - "missing-view-permission": "Bu dosyayı görüntüleme izniniz yok.", - "missing-write-permission": "Geçici dosyaları kaydetme izniniz yok.", "s3-upload-error": "Geçici dosya S3'e yüklenirken bir hata oluştu." } diff --git a/packages/h5p-server/assets/translations/s3-temporary-storage/zh.json b/packages/h5p-server/assets/translations/s3-temporary-storage/zh.json index 5ebb61742..8b726d089 100644 --- a/packages/h5p-server/assets/translations/s3-temporary-storage/zh.json +++ b/packages/h5p-server/assets/translations/s3-temporary-storage/zh.json @@ -1,8 +1,5 @@ { "deleting-file-error": "删除此文件时出错。", "file-not-found": "该文件不存在。", - "missing-delete-permission": "您无权删除此文件。", - "missing-view-permission": "您无权查看此文件。", - "missing-write-permission": "您无权保存临时文件。", "s3-upload-error": "将临时文件上传到S3时出错。" } diff --git a/packages/h5p-server/assets/translations/server/bg.json b/packages/h5p-server/assets/translations/server/bg.json index e095323ee..857cba74d 100644 --- a/packages/h5p-server/assets/translations/server/bg.json +++ b/packages/h5p-server/assets/translations/server/bg.json @@ -60,5 +60,18 @@ "content-hub-download-error": "Възникна грешка при свързването към H5P Content Hub. Моля, опитайте отново по-късно.", "install-missing-libraries": "Не може да се зареди съдържанието, тъй като следните библиотеки не са инсталирани в тази система: {{libraries}}", "install-library-lock-timeout": "Библиотеката {{ubername}} не можа да бъде инсталирана, тъй като същите библиотеки се инсталират от друго място (изчаква се {{limit}} ms за заключване на библиотеката).", - "install-library-lock-max-time-exceeded": "Библиотеката {{ubername}} не можа да бъде инсталирана, защото отне повече от разрешените {{limit}} ms." + "install-library-lock-max-time-exceeded": "Библиотеката {{ubername}} не можа да бъде инсталирана, защото отне повече от разрешените {{limit}} ms.", + "content-missing-create-permission": "Нямате право да създавате ново съдържание.", + "content-missing-delete-permission": "Нямате право да изтриете това съдържание.", + "content-missing-edit-permission": "Нямате право да редактирате това съдържание.", + "content-missing-list-permission": "Нямате право да изброявате цялото съдържание.", + "content-missing-view-permission": "Нямате право да преглеждате това съдържание.", + "finished-data-missing-delete-permission": "Нямате право да изтриете тези готови данни.", + "finished-data-missing-edit-permission": "Нямате право да редактирате тези готови данни.", + "temporary-file-missing-delete-permission": "Нямате право да изтриете този временен файл.", + "temporary-file-missing-view-permission": "Нямате право да преглеждате този временен файл.", + "temporary-file-missing-write-permission": "Нямате право да създавате временен файл.", + "user-state-missing-delete-permission": "Нямате право да изтриете това потребителско състояние.", + "user-state-missing-edit-permission": "Нямате право да редактирате това потребителско състояние.", + "user-state-missing-view-permission": "Нямате право да преглеждате това потребителско състояние." } diff --git a/packages/h5p-server/assets/translations/server/bs.json b/packages/h5p-server/assets/translations/server/bs.json index 6ecc33144..d2058d210 100644 --- a/packages/h5p-server/assets/translations/server/bs.json +++ b/packages/h5p-server/assets/translations/server/bs.json @@ -60,5 +60,18 @@ "content-hub-download-error": "Došlo je do greške prilikom povezivanja sa H5P Content Hubom. Pokušajte ponovo kasnije.", "install-missing-libraries": "Nije moguće učitati sadržaj jer sljedeće biblioteke nisu instalirane na ovom sistemu: {{libraries}}", "install-library-lock-timeout": "Biblioteka {{ubername}} se nije mogla instalirati, jer se iste biblioteke instaliraju sa drugog mjesta (čekalo se {{limit}} ms za zaključavanje biblioteke).", - "install-library-lock-max-time-exceeded": "Biblioteku {{ubername}} nije bilo moguće instalirati, jer je trebalo duže od dozvoljenog {{limit}} ms." + "install-library-lock-max-time-exceeded": "Biblioteku {{ubername}} nije bilo moguće instalirati, jer je trebalo duže od dozvoljenog {{limit}} ms.", + "content-missing-create-permission": "Nije vam dozvoljeno kreirati novi sadržaj.", + "content-missing-delete-permission": "Nije vam dozvoljeno brisanje ovog sadržaja.", + "content-missing-edit-permission": "Nije vam dozvoljeno uređivati ovaj sadržaj.", + "content-missing-list-permission": "Nije vam dozvoljeno da navedete sav sadržaj.", + "content-missing-view-permission": "Nije vam dozvoljeno da vidite ovaj sadržaj.", + "finished-data-missing-delete-permission": "Nije vam dozvoljeno brisanje ovih gotovih podataka.", + "finished-data-missing-edit-permission": "Nije vam dozvoljeno uređivati ove gotove podatke.", + "temporary-file-missing-delete-permission": "Nije vam dozvoljeno da izbrišete ovaj privremeni fajl.", + "temporary-file-missing-view-permission": "Nije vam dozvoljeno da vidite ovaj privremeni fajl.", + "temporary-file-missing-write-permission": "Nije vam dozvoljeno da kreirate privremeni fajl.", + "user-state-missing-delete-permission": "Nije vam dozvoljeno da izbrišete ovo korisničko stanje.", + "user-state-missing-edit-permission": "Nije vam dozvoljeno uređivati ovo korisničko stanje.", + "user-state-missing-view-permission": "Nije vam dozvoljeno da vidite ovo korisničko stanje." } diff --git a/packages/h5p-server/assets/translations/server/ca.json b/packages/h5p-server/assets/translations/server/ca.json index 1c75c94d3..0328f723e 100644 --- a/packages/h5p-server/assets/translations/server/ca.json +++ b/packages/h5p-server/assets/translations/server/ca.json @@ -60,5 +60,18 @@ "content-hub-download-error": "S'ha produït un error en connectar-se al centre de contingut H5P. Siusplau, intenta-ho més tard.", "install-missing-libraries": "No s'ha pogut carregar el contingut ja que les biblioteques següents no estan instal·lades en aquest sistema: {{libraries}}", "install-library-lock-timeout": "No s'ha pogut instal·lar la biblioteca {{ubername}} , perquè s'està instal·lant les mateixes biblioteques des d'un altre lloc (s'ha esperat {{limit}} ms per bloquejar la biblioteca).", - "install-library-lock-max-time-exceeded": "La biblioteca {{ubername}} no s'ha pogut instal·lar perquè ha trigat més que els {{limit}} ms permès." + "install-library-lock-max-time-exceeded": "La biblioteca {{ubername}} no s'ha pogut instal·lar perquè ha trigat més que els {{limit}} ms permès.", + "content-missing-create-permission": "No teniu permís per crear contingut nou.", + "content-missing-delete-permission": "No teniu permís per suprimir aquest contingut.", + "content-missing-edit-permission": "No teniu permís per editar aquest contingut.", + "content-missing-list-permission": "No teniu permís per llistar tot el contingut.", + "content-missing-view-permission": "No teniu permís per veure aquest contingut.", + "finished-data-missing-delete-permission": "No teniu permís per suprimir aquestes dades acabades.", + "finished-data-missing-edit-permission": "No teniu permís per editar aquestes dades acabades.", + "temporary-file-missing-delete-permission": "No teniu permís per suprimir aquest fitxer temporal.", + "temporary-file-missing-view-permission": "No teniu permís per veure aquest fitxer temporal.", + "temporary-file-missing-write-permission": "No teniu permís per crear un fitxer temporal.", + "user-state-missing-delete-permission": "No teniu permís per suprimir aquest estat d'usuari.", + "user-state-missing-edit-permission": "No teniu permís per editar aquest estat d'usuari.", + "user-state-missing-view-permission": "No teniu permís per veure aquest estat d'usuari." } diff --git a/packages/h5p-server/assets/translations/server/cs.json b/packages/h5p-server/assets/translations/server/cs.json index 60564ece9..cbdce19d7 100644 --- a/packages/h5p-server/assets/translations/server/cs.json +++ b/packages/h5p-server/assets/translations/server/cs.json @@ -60,5 +60,18 @@ "content-hub-download-error": "Při připojování k H5P Content Hub došlo k chybě. Prosím zkuste to znovu později.", "install-missing-libraries": "Obsah nelze načíst, protože v tomto systému nejsou nainstalovány následující knihovny: {{libraries}}", "install-library-lock-timeout": "Knihovnu {{ubername}} nelze nainstalovat, protože se stejné knihovny instalují z jiného místa (čekalo se {{limit}} ms na uzamčení knihovny).", - "install-library-lock-max-time-exceeded": "Knihovnu {{ubername}} nebylo možné nainstalovat, protože to trvalo déle než povolenou {{limit}} ms." + "install-library-lock-max-time-exceeded": "Knihovnu {{ubername}} nebylo možné nainstalovat, protože to trvalo déle než povolenou {{limit}} ms.", + "content-missing-create-permission": "Nemáte oprávnění vytvářet nový obsah.", + "content-missing-delete-permission": "Nemáte oprávnění smazat tento obsah.", + "content-missing-edit-permission": "Nemáte oprávnění upravovat tento obsah.", + "content-missing-list-permission": "Nemáte povoleno uvádět veškerý obsah.", + "content-missing-view-permission": "Nemáte oprávnění prohlížet tento obsah.", + "finished-data-missing-delete-permission": "Tato hotová data nesmíte smazat.", + "finished-data-missing-edit-permission": "Tato hotová data nemáte povoleno upravovat.", + "temporary-file-missing-delete-permission": "Nemáte oprávnění smazat tento dočasný soubor.", + "temporary-file-missing-view-permission": "Nemáte oprávnění prohlížet tento dočasný soubor.", + "temporary-file-missing-write-permission": "Nemáte oprávnění vytvořit dočasný soubor.", + "user-state-missing-delete-permission": "Nemáte oprávnění smazat tento uživatelský stav.", + "user-state-missing-edit-permission": "Nemáte oprávnění upravovat tento stav uživatele.", + "user-state-missing-view-permission": "Nemáte oprávnění zobrazit tento stav uživatele." } diff --git a/packages/h5p-server/assets/translations/server/de.json b/packages/h5p-server/assets/translations/server/de.json index dbf58fc70..95b14e5cd 100644 --- a/packages/h5p-server/assets/translations/server/de.json +++ b/packages/h5p-server/assets/translations/server/de.json @@ -60,5 +60,18 @@ "content-hub-download-error": "Beim Herstellen einer Verbindung zum H5P Content Hub ist ein Fehler aufgetreten. Bitte versuchen es später noch einmal.", "install-missing-libraries": "Der Inhalt konnte nicht geladen werden, da die folgenden Bibliotheken auf diesem System nicht installiert sind: {{libraries}}", "install-library-lock-timeout": "Die Bibliothek {{ubername}} konnte nicht installiert werden, da die gleichen Bibliotheken von einem anderen Ort installiert werden ( {{limit}} ms auf Sperre der Bibliothek gewartet).", - "install-library-lock-max-time-exceeded": "Die Bibliothek {{ubername}} konnte nicht installiert werden, da es länger als die erlaubten {{limit}} ms gedauert hat." -} + "install-library-lock-max-time-exceeded": "Die Bibliothek {{ubername}} konnte nicht installiert werden, da es länger als die erlaubten {{limit}} ms gedauert hat.", + "content-missing-create-permission": "Es ist dir nicht gestattet, neue Inhalte zu erstellen.", + "content-missing-delete-permission": "Es ist dir nicht gestattet, diesen Inhalt zu löschen.", + "content-missing-edit-permission": "Es ist dir nicht gestattet, diesen Inhalt zu bearbeiten.", + "content-missing-list-permission": "Es ist dir nicht gestattet, alle Inhalte aufzulisten.", + "content-missing-view-permission": "Es ist dir nicht gestattet, diesen Inhalt anzuzeigen.", + "finished-data-missing-delete-permission": "Du darfst diese Daten der Abschlussverfolgung nicht löschen.", + "finished-data-missing-edit-permission": "Du darfst diese Daten der Abschlussverfolgung nicht bearbeiten.", + "temporary-file-missing-delete-permission": "Du darfst diese temporäre Datei nicht löschen.", + "temporary-file-missing-view-permission": "Du darfst diese temporäre Datei nicht anzeigen.", + "temporary-file-missing-write-permission": "Es ist dir nicht gestattet, eine temporäre Datei zu erstellen.", + "user-state-missing-delete-permission": "Du darfst diesen Benutzerstatus nicht löschen.", + "user-state-missing-edit-permission": "Du darfst diesen Benutzerstatus nicht bearbeiten.", + "user-state-missing-view-permission": "Du bist nicht berechtigt, diesen Benutzerstatus anzuzeigen." +} \ No newline at end of file diff --git a/packages/h5p-server/assets/translations/server/el.json b/packages/h5p-server/assets/translations/server/el.json index 06d3a0f0f..96eae97c6 100644 --- a/packages/h5p-server/assets/translations/server/el.json +++ b/packages/h5p-server/assets/translations/server/el.json @@ -60,5 +60,18 @@ "content-hub-download-error": "Παρουσιάστηκε σφάλμα κατά τη σύνδεση στο H5P Content Hub. Παρακαλώ προσπάθησε ξανά αργότερα.", "install-missing-libraries": "Δεν ήταν δυνατή η φόρτωση του περιεχομένου καθώς οι ακόλουθες βιβλιοθήκες δεν είναι εγκατεστημένες σε αυτό το σύστημα: {{libraries}}", "install-library-lock-timeout": "Δεν {{ubername}} , επειδή οι ίδιες βιβλιοθήκες εγκαθίστανται από άλλο μέρος (περιμέναμε {{limit}} ms για να κλειδωθεί στη βιβλιοθήκη).", - "install-library-lock-max-time-exceeded": "Δεν {{ubername}} , επειδή χρειάστηκε περισσότερος χρόνος από το επιτρεπόμενο {{limit}} ms." + "install-library-lock-max-time-exceeded": "Δεν {{ubername}} , επειδή χρειάστηκε περισσότερος χρόνος από το επιτρεπόμενο {{limit}} ms.", + "content-missing-create-permission": "Δεν επιτρέπεται να δημιουργείτε νέο περιεχόμενο.", + "content-missing-delete-permission": "Δεν επιτρέπεται να διαγράψετε αυτό το περιεχόμενο.", + "content-missing-edit-permission": "Δεν επιτρέπεται να επεξεργαστείτε αυτό το περιεχόμενο.", + "content-missing-list-permission": "Δεν επιτρέπεται να καταχωρίσετε όλο το περιεχόμενο.", + "content-missing-view-permission": "Δεν επιτρέπεται να δείτε αυτό το περιεχόμενο.", + "finished-data-missing-delete-permission": "Δεν επιτρέπεται να διαγράψετε αυτά τα ολοκληρωμένα δεδομένα.", + "finished-data-missing-edit-permission": "Δεν επιτρέπεται να επεξεργαστείτε αυτά τα ολοκληρωμένα δεδομένα.", + "temporary-file-missing-delete-permission": "Δεν επιτρέπεται να διαγράψετε αυτό το προσωρινό αρχείο.", + "temporary-file-missing-view-permission": "Δεν επιτρέπεται να δείτε αυτό το προσωρινό αρχείο.", + "temporary-file-missing-write-permission": "Δεν επιτρέπεται να δημιουργήσετε ένα προσωρινό αρχείο.", + "user-state-missing-delete-permission": "Δεν επιτρέπεται να διαγράψετε αυτήν την κατάσταση χρήστη.", + "user-state-missing-edit-permission": "Δεν επιτρέπεται να επεξεργαστείτε αυτήν την κατάσταση χρήστη.", + "user-state-missing-view-permission": "Δεν επιτρέπεται να δείτε αυτήν την κατάσταση χρήστη." } diff --git a/packages/h5p-server/assets/translations/server/en.json b/packages/h5p-server/assets/translations/server/en.json index b3e8bb3dc..0e35481bb 100644 --- a/packages/h5p-server/assets/translations/server/en.json +++ b/packages/h5p-server/assets/translations/server/en.json @@ -1,6 +1,13 @@ { "api-version-unsupported": "The system was unable to install the {{component}} component from the package, it requires a newer version of the H5P plugin. This site is currently running version {{current}}, whereas the required version is {{required}} or higher. You should consider upgrading and then try again.", "content-file-missing": "File {{filename}} does not exist in {{contentId}}", + "content-hub-download-error-with-message": "There was an error connecting to the H5P Content Hub ({{message}}). Please try again later.", + "content-hub-download-error": "There was an error connecting to the H5P Content Hub. Please try again later.", + "content-missing-create-permission": "You are not allowed to create new content.", + "content-missing-delete-permission": "You are not allowed to delete this content.", + "content-missing-edit-permission": "You are not allowed to edit this content.", + "content-missing-list-permission": "You are not allowed to list all content.", + "content-missing-view-permission": "You are not allowed to view this content.", "corrupt-file": "The file {{file}} could not be read from the H5P package. The package seems to be corrupt.", "download-content-forbidden": "You do not have permission to download content with id {{contentId}}.", "download-content-not-found": "Content can't be downloaded as no content with id {{contentId}} exists.", @@ -13,12 +20,17 @@ "error-registering-at-hub-no-status": "Could not register this site at the H5P Hub.", "error-registering-at-hub": "Could not register this site at the H5P Hub. (HTTP status: {{statusCode}}: {{statusText}})", "file-size-too-large": "One of the files inside the package exceeds the maximum file size allowed. ({{file}} {{used}} > {{max}})", + "finished-data-missing-delete-permission": "You are not allowed to delete this finished data.", + "finished-data-missing-edit-permission": "You are not allowed to edit this finished data.", + "h5p-hub-connection-failed": "There was an error connecting to the H5P Content Hub. Please try again later.", "hub-install-denied": "You do not have permission to install content types. Contact the administrator of your site.", "hub-install-download-failed": "Could not download package from the Hub to temporary file.", "hub-install-invalid-content-type": "The chosen content type is invalid.", "hub-install-no-content-type": "No content type was specified.", "illegal-filename": "The filename you used is not allowed.", "import-package-no-id-assigned": "Something went wrong when storing the package: no content id was assigned.", + "install-library-lock-max-time-exceeded": "The library {{ubername}} could not be installed, because it took longer than the allowed {{limit}} ms.", + "install-library-lock-timeout": "The library {{ubername}} could not be installed, because the same libraries is being installed from another place (waited {{limit}} ms for lock on the library).", "install-missing-libraries": "Could not load the content as the following libraries are not installed on this system: {{libraries}}", "installed-libraries_plural": "Added {{count}} new H5P libraries.", "installed-libraries": "Added {{count}} new H5P library.", @@ -44,21 +56,22 @@ "library-used": "The library {{library}} cannot be deleted as it is still in use.", "malformed-request": "The request sent by the client is malformed: {{error}}", "missing-h5p-extension": "The file you uploaded is not a valid HTML5 Package (It does not have the .h5p file extension)", + "multipart-ranges-unsupported": "Multipart range requests are not supported on this server.", "not-in-whitelist": "File \"{{filename}}\" not allowed. Only files with the following extensions are allowed: {{files-allowed}}.", "package-validation-failed": "Validating h5p package failed.", + "temporary-file-missing-delete-permission": "You are not allowed to delete this temporary file.", + "temporary-file-missing-view-permission": "You are not allowed to view this temporary file.", + "temporary-file-missing-write-permission": "You are not allowed to create a temporary file.", "total-size-too-large": "The total size of the unpacked files exceeds the maximum size allowed. ({{used}} > {{max}})", "unable-to-parse-package": "Unable to parse JSON file from the package: {{fileName}}", "unable-to-read-package-file": "Unable to read file from the package: {{fileName}}", "unable-to-unzip": "The file you uploaded is not a valid HTML5 Package (We are unable to unzip it)", + "unsatisfiable-range": "The requested range of the file is not within the size of the actual file.", "updated-libraries_plural": "Updated {{count}} old libraries.", "updated-libraries": "Updated {{count}} old library.", "upload-package-failed-tmp": "Could not save uploaded package to temporary file.", "upload-validation-error": "The file you've uploaded is invalid.", - "unsatisfiable-range": "The requested range of the file is not within the size of the actual file.", - "multipart-ranges-unsupported": "Multipart range requests are not supported on this server.", - "h5p-hub-connection-failed": "There was an error connecting to the H5P Content Hub. Please try again later.", - "content-hub-download-error-with-message": "There was an error connecting to the H5P Content Hub ({{message}}). Please try again later.", - "content-hub-download-error": "There was an error connecting to the H5P Content Hub. Please try again later.", - "install-library-lock-timeout": "The library {{ubername}} could not be installed, because the same libraries is being installed from another place (waited {{limit}} ms for lock on the library).", - "install-library-lock-max-time-exceeded": "The library {{ubername}} could not be installed, because it took longer than the allowed {{limit}} ms." + "user-state-missing-delete-permission": "You are not allowed to delete this user state.", + "user-state-missing-edit-permission": "You are not allowed to edit this user state.", + "user-state-missing-view-permission": "You are not allowed to view this user state." } diff --git a/packages/h5p-server/assets/translations/server/es.json b/packages/h5p-server/assets/translations/server/es.json index b562a36e4..bd5b9790c 100644 --- a/packages/h5p-server/assets/translations/server/es.json +++ b/packages/h5p-server/assets/translations/server/es.json @@ -60,5 +60,18 @@ "content-hub-download-error": "Hubo un error al conectarse al H5P Content Hub. Por favor, inténtelo de nuevo más tarde.", "install-missing-libraries": "No se pudo cargar el contenido porque las siguientes bibliotecas no están instaladas en este sistema: {{libraries}}", "install-library-lock-timeout": "La biblioteca {{ubername}} no se pudo instalar, porque la misma biblioteca está siendo instalada desde otro lugar (esperamos {{limit}} ms para el bloqueo de la biblioteca).", - "install-library-lock-max-time-exceeded": "La biblioteca {{ubername}} no se pudo instalar, porque tardó más que el tiempo permitido de {{limit}} ms." + "install-library-lock-max-time-exceeded": "La biblioteca {{ubername}} no se pudo instalar, porque tardó más que el tiempo permitido de {{limit}} ms.", + "content-missing-create-permission": "No se le permite crear contenido nuevo.", + "content-missing-delete-permission": "No tienes permiso para eliminar este contenido.", + "content-missing-edit-permission": "No tienes permiso para editar este contenido.", + "content-missing-list-permission": "No se le permite enumerar todo el contenido.", + "content-missing-view-permission": "No tienes permiso para ver este contenido.", + "finished-data-missing-delete-permission": "No se le permite eliminar estos datos terminados.", + "finished-data-missing-edit-permission": "No se le permite editar estos datos terminados.", + "temporary-file-missing-delete-permission": "No tiene permiso para eliminar este archivo temporal.", + "temporary-file-missing-view-permission": "No tiene permiso para ver este archivo temporal.", + "temporary-file-missing-write-permission": "No se le permite crear un archivo temporal.", + "user-state-missing-delete-permission": "No tiene permiso para eliminar este estado de usuario.", + "user-state-missing-edit-permission": "No tiene permiso para editar este estado de usuario.", + "user-state-missing-view-permission": "No tiene permiso para ver este estado de usuario." } diff --git a/packages/h5p-server/assets/translations/server/et.json b/packages/h5p-server/assets/translations/server/et.json index bf27a1a09..becb5b8cf 100644 --- a/packages/h5p-server/assets/translations/server/et.json +++ b/packages/h5p-server/assets/translations/server/et.json @@ -60,5 +60,18 @@ "content-hub-download-error": "H5P sisukeskusega ühenduse loomisel ilmnes viga. Palun proovi hiljem uuesti.", "install-missing-libraries": "Sisu ei saanud laadida, kuna sellesse süsteemi pole installitud järgmisi teeke: {{libraries}}", "install-library-lock-timeout": "Teeki {{ubername}} ei saanud installida, kuna samu teeke installitakse teisest kohast (ootas {{limit}} ms teegi lukustamist).", - "install-library-lock-max-time-exceeded": "Teeki {{ubername}} ei saanud installida, kuna see võttis kauem aega kui lubatud {{limit}} ms." + "install-library-lock-max-time-exceeded": "Teeki {{ubername}} ei saanud installida, kuna see võttis kauem aega kui lubatud {{limit}} ms.", + "content-missing-create-permission": "Teil ei ole lubatud uut sisu luua.", + "content-missing-delete-permission": "Teil ei ole lubatud seda sisu kustutada.", + "content-missing-edit-permission": "Teil ei ole lubatud seda sisu muuta.", + "content-missing-list-permission": "Teil ei ole lubatud kogu sisu loetleda.", + "content-missing-view-permission": "Teil ei ole lubatud seda sisu vaadata.", + "finished-data-missing-delete-permission": "Teil ei ole lubatud neid valmis andmeid kustutada.", + "finished-data-missing-edit-permission": "Teil ei ole lubatud neid valmis andmeid redigeerida.", + "temporary-file-missing-delete-permission": "Teil ei ole lubatud seda ajutist faili kustutada.", + "temporary-file-missing-view-permission": "Teil ei ole lubatud seda ajutist faili vaadata.", + "temporary-file-missing-write-permission": "Teil ei ole lubatud ajutist faili luua.", + "user-state-missing-delete-permission": "Teil ei ole lubatud seda kasutaja olekut kustutada.", + "user-state-missing-edit-permission": "Teil ei ole lubatud seda kasutaja olekut muuta.", + "user-state-missing-view-permission": "Teil ei ole lubatud seda kasutaja olekut vaadata." } diff --git a/packages/h5p-server/assets/translations/server/eu.json b/packages/h5p-server/assets/translations/server/eu.json index fabd2059b..07756e72c 100644 --- a/packages/h5p-server/assets/translations/server/eu.json +++ b/packages/h5p-server/assets/translations/server/eu.json @@ -60,5 +60,18 @@ "content-hub-download-error": "Errore bat gertatu da H5P Eduki Zentrora konektatzean. Saiatu berriro geroago.", "install-missing-libraries": "Ezin izan da edukia kargatu liburutegi hauek ez daudelako instalatuta sistema honetan: {{libraries}}", "install-library-lock-timeout": "Ezin {{ubername}} liburutegia instalatu, liburutegi berdinak beste leku batetik instalatzen ari direlako ( {{limit}} ms itxaron liburutegian blokeatzeko).", - "install-library-lock-max-time-exceeded": "Ezin izan da {{ubername}} liburutegia instalatu {{limit}} ms baino gehiago behar izan duelako." + "install-library-lock-max-time-exceeded": "Ezin izan da {{ubername}} liburutegia instalatu {{limit}} ms baino gehiago behar izan duelako.", + "content-missing-create-permission": "Ez duzu eduki berririk sortzeko baimenik.", + "content-missing-delete-permission": "Ez duzu eduki hau ezabatzeko baimenik.", + "content-missing-edit-permission": "Ez duzu eduki hau editatzeko baimenik.", + "content-missing-list-permission": "Ez duzu eduki guztia zerrendatzeko baimenik.", + "content-missing-view-permission": "Ez duzu eduki hau ikusteko baimenik.", + "finished-data-missing-delete-permission": "Ezin duzu amaitutako datu hauek ezabatzeko baimenik.", + "finished-data-missing-edit-permission": "Ezin duzu amaitutako datu hauek editatzeko baimenik.", + "temporary-file-missing-delete-permission": "Ezin duzu aldi baterako fitxategi hau ezabatzeko baimenik.", + "temporary-file-missing-view-permission": "Ez duzu aldi baterako fitxategi hau ikusteko baimenik.", + "temporary-file-missing-write-permission": "Ezin duzu aldi baterako fitxategirik sortzeko baimenik.", + "user-state-missing-delete-permission": "Ez duzu erabiltzaile-egoera hau ezabatzeko baimenik.", + "user-state-missing-edit-permission": "Ez duzu erabiltzaile-egoera hau editatzeko baimenik.", + "user-state-missing-view-permission": "Ez duzu erabiltzaile-egoera hau ikusteko baimenik." } diff --git a/packages/h5p-server/assets/translations/server/fi.json b/packages/h5p-server/assets/translations/server/fi.json index 9b5fcd4b6..87f6fe8a1 100644 --- a/packages/h5p-server/assets/translations/server/fi.json +++ b/packages/h5p-server/assets/translations/server/fi.json @@ -60,5 +60,18 @@ "content-hub-download-error": "H5P-sisältökeskukseen yhdistämisessä tapahtui virhe. Yritä uudelleen myöhemmin.", "install-missing-libraries": "Sisällön lataaminen epäonnistui, koska seuraavia kirjastoja ei ole asennettu tähän järjestelmään: {{libraries}}", "install-library-lock-timeout": "Kirjastoa {{ubername}} ei voitu asentaa, koska samoja kirjastoja asennetaan toisesta paikasta (odotti {{limit}} ms lukitusta kirjastoon).", - "install-library-lock-max-time-exceeded": "Kirjastoa {{ubername}} ei voitu asentaa, koska se kesti kauemmin kuin sallittu {{limit}} ms." + "install-library-lock-max-time-exceeded": "Kirjastoa {{ubername}} ei voitu asentaa, koska se kesti kauemmin kuin sallittu {{limit}} ms.", + "content-missing-create-permission": "Et saa luoda uutta sisältöä.", + "content-missing-delete-permission": "Sinulla ei ole lupaa poistaa tätä sisältöä.", + "content-missing-edit-permission": "Sinulla ei ole lupaa muokata tätä sisältöä.", + "content-missing-list-permission": "Et saa listata kaikkea sisältöä.", + "content-missing-view-permission": "Sinulla ei ole lupaa tarkastella tätä sisältöä.", + "finished-data-missing-delete-permission": "Sinulla ei ole lupaa poistaa näitä valmiita tietoja.", + "finished-data-missing-edit-permission": "Sinulla ei ole lupaa muokata näitä valmiita tietoja.", + "temporary-file-missing-delete-permission": "Sinulla ei ole lupaa poistaa tätä väliaikaista tiedostoa.", + "temporary-file-missing-view-permission": "Sinulla ei ole lupaa tarkastella tätä väliaikaista tiedostoa.", + "temporary-file-missing-write-permission": "Sinulla ei ole lupaa luoda väliaikaista tiedostoa.", + "user-state-missing-delete-permission": "Sinulla ei ole oikeutta poistaa tätä käyttäjän tilaa.", + "user-state-missing-edit-permission": "Sinulla ei ole lupaa muokata tätä käyttäjän tilaa.", + "user-state-missing-view-permission": "Sinulla ei ole lupaa tarkastella tätä käyttäjän tilaa." } diff --git a/packages/h5p-server/assets/translations/server/fr.json b/packages/h5p-server/assets/translations/server/fr.json index c849c8ca0..b631a6476 100644 --- a/packages/h5p-server/assets/translations/server/fr.json +++ b/packages/h5p-server/assets/translations/server/fr.json @@ -60,5 +60,18 @@ "content-hub-download-error": "Une erreur s'est produite lors de la connexion au H5P Content Hub. Veuillez réessayer plus tard.", "install-missing-libraries": "Impossible de charger le contenu car les bibliothèques suivantes ne sont pas installées sur ce système : {{libraries}}", "install-library-lock-timeout": "La bibliothèque {{ubername}} n'a pas pu être installée, car les mêmes bibliothèques sont installées à partir d'un autre endroit (attendu {{limit}} ms pour le verrouillage sur la bibliothèque).", - "install-library-lock-max-time-exceeded": "La bibliothèque {{ubername}} n'a pas pu être installée, car elle a pris plus de temps que les {{limit}} ms autorisées." + "install-library-lock-max-time-exceeded": "La bibliothèque {{ubername}} n'a pas pu être installée, car elle a pris plus de temps que les {{limit}} ms autorisées.", + "content-missing-create-permission": "Vous n'êtes pas autorisé à créer de nouveau contenu.", + "content-missing-delete-permission": "Vous n'êtes pas autorisé à supprimer ce contenu.", + "content-missing-edit-permission": "Vous n'êtes pas autorisé à modifier ce contenu.", + "content-missing-list-permission": "Vous n'êtes pas autorisé à répertorier tout le contenu.", + "content-missing-view-permission": "Vous n'êtes pas autorisé à voir ce contenu.", + "finished-data-missing-delete-permission": "Vous n'êtes pas autorisé à supprimer ces données finies.", + "finished-data-missing-edit-permission": "Vous n'êtes pas autorisé à modifier ces données finies.", + "temporary-file-missing-delete-permission": "Vous n'êtes pas autorisé à supprimer ce fichier temporaire.", + "temporary-file-missing-view-permission": "Vous n'êtes pas autorisé à afficher ce fichier temporaire.", + "temporary-file-missing-write-permission": "Vous n'êtes pas autorisé à créer un fichier temporaire.", + "user-state-missing-delete-permission": "Vous n'êtes pas autorisé à supprimer cet état utilisateur.", + "user-state-missing-edit-permission": "Vous n'êtes pas autorisé à modifier cet état utilisateur.", + "user-state-missing-view-permission": "Vous n'êtes pas autorisé à afficher cet état d'utilisateur." } diff --git a/packages/h5p-server/assets/translations/server/gl.json b/packages/h5p-server/assets/translations/server/gl.json index 97e3f6bc1..2b37eeffe 100644 --- a/packages/h5p-server/assets/translations/server/gl.json +++ b/packages/h5p-server/assets/translations/server/gl.json @@ -60,5 +60,18 @@ "not-in-whitelist": "Non se permite o ficheiro «{{filename}}». Só se permiten ficheiros coas seguintes extensións: {{files-allowed}}.", "content-hub-download-error-with-message": "Produciuse un erro ao conectarse ao Hub de contido de H5P ({{message}}). Ténteo de novo máis tarde.", "install-library-lock-timeout": "Non foi posíbel instalar a biblioteca {{ubername}} porque as mesmas bibliotecas estanse instalando desde outro lugar (agardáronse {{limit}} ms para o bloqueo da biblioteca).", - "install-library-lock-max-time-exceeded": "Non foi posíbel instalar a biblioteca {{ubername}} porque levou máis tempo do permitido {{limit}} ms." + "install-library-lock-max-time-exceeded": "Non foi posíbel instalar a biblioteca {{ubername}} porque levou máis tempo do permitido {{limit}} ms.", + "content-missing-create-permission": "Non tes permiso para crear contido novo.", + "content-missing-delete-permission": "Non tes permiso para eliminar este contido.", + "content-missing-edit-permission": "Non tes permiso para editar este contido.", + "content-missing-list-permission": "Non tes permiso para enumerar todo o contido.", + "content-missing-view-permission": "Non tes permiso para ver este contido.", + "finished-data-missing-delete-permission": "Non tes permiso para eliminar estes datos rematados.", + "finished-data-missing-edit-permission": "Non tes permiso para editar estes datos rematados.", + "temporary-file-missing-delete-permission": "Non tes permiso para eliminar este ficheiro temporal.", + "temporary-file-missing-view-permission": "Non tes permiso para ver este ficheiro temporal.", + "temporary-file-missing-write-permission": "Non tes permiso para crear un ficheiro temporal.", + "user-state-missing-delete-permission": "Non tes permiso para eliminar este estado de usuario.", + "user-state-missing-edit-permission": "Non tes permiso para editar este estado de usuario.", + "user-state-missing-view-permission": "Non tes permiso para ver este estado de usuario." } diff --git a/packages/h5p-server/assets/translations/server/it.json b/packages/h5p-server/assets/translations/server/it.json index e7ea4983b..62f3c0ef0 100644 --- a/packages/h5p-server/assets/translations/server/it.json +++ b/packages/h5p-server/assets/translations/server/it.json @@ -60,5 +60,18 @@ "content-hub-download-error": "Si è verificato un errore durante la connessione all'H5P Content Hub. Per favore riprova più tardi.", "install-missing-libraries": "Impossibile caricare il contenuto poiché le seguenti librerie non sono installate su questo sistema: {{libraries}}", "install-library-lock-timeout": "Non è stato possibile installare la libreria {{ubername}} , perché le stesse librerie vengono installate da un'altra posizione (ha atteso {{limit}} ms per il blocco della libreria).", - "install-library-lock-max-time-exceeded": "Non è stato possibile installare la libreria {{ubername}} {{limit}} ms consentito." + "install-library-lock-max-time-exceeded": "Non è stato possibile installare la libreria {{ubername}} {{limit}} ms consentito.", + "content-missing-create-permission": "Non sei autorizzato a creare nuovi contenuti.", + "content-missing-delete-permission": "Non sei autorizzato a eliminare questo contenuto.", + "content-missing-edit-permission": "Non sei autorizzato a modificare questo contenuto.", + "content-missing-list-permission": "Non è consentito elencare tutti i contenuti.", + "content-missing-view-permission": "Non sei autorizzato a visualizzare questo contenuto.", + "finished-data-missing-delete-permission": "Non sei autorizzato a cancellare questi dati finiti.", + "finished-data-missing-edit-permission": "Non sei autorizzato a modificare questi dati finiti.", + "temporary-file-missing-delete-permission": "Non sei autorizzato a eliminare questo file temporaneo.", + "temporary-file-missing-view-permission": "Non sei autorizzato a visualizzare questo file temporaneo.", + "temporary-file-missing-write-permission": "Non sei autorizzato a creare un file temporaneo.", + "user-state-missing-delete-permission": "Non sei autorizzato a eliminare questo stato utente.", + "user-state-missing-edit-permission": "Non sei autorizzato a modificare questo stato utente.", + "user-state-missing-view-permission": "Non sei autorizzato a visualizzare questo stato utente." } diff --git a/packages/h5p-server/assets/translations/server/ja.json b/packages/h5p-server/assets/translations/server/ja.json index 5a28bc359..0b9a2db4c 100644 --- a/packages/h5p-server/assets/translations/server/ja.json +++ b/packages/h5p-server/assets/translations/server/ja.json @@ -60,5 +60,18 @@ "content-hub-download-error-with-message": "{{message}} )への接続中にエラーが発生しました。後でもう一度やり直してください。", "content-hub-download-error": "H5Pコンテンツハブへの接続中にエラーが発生しました。後でもう一度やり直してください。", "install-library-lock-timeout": "同じライブラリが別の場所からインストールされているため、ライブラリ{{ubername}} {{limit}}ミリ秒待機しました)。", - "install-library-lock-max-time-exceeded": "ライブラリ{{ubername}} {{limit}}ミリ秒より長くかかったため、インストールできませんでした。" + "install-library-lock-max-time-exceeded": "ライブラリ{{ubername}} {{limit}}ミリ秒より長くかかったため、インストールできませんでした。", + "content-missing-create-permission": "新しいコンテンツを作成することはできません。", + "content-missing-delete-permission": "このコンテンツを削除することはできません。", + "content-missing-edit-permission": "このコンテンツを編集することはできません。", + "content-missing-list-permission": "すべてのコンテンツをリストすることは許可されていません。", + "content-missing-view-permission": "このコンテンツを閲覧することはできません。", + "finished-data-missing-delete-permission": "この完成したデータを削除することはできません。", + "finished-data-missing-edit-permission": "この完成データを編集することはできません。", + "temporary-file-missing-delete-permission": "この一時ファイルを削除することはできません。", + "temporary-file-missing-view-permission": "この一時ファイルを表示することはできません。", + "temporary-file-missing-write-permission": "一時ファイルを作成することはできません。", + "user-state-missing-delete-permission": "このユーザー状態を削除することはできません。", + "user-state-missing-edit-permission": "このユーザー状態を編集することはできません。", + "user-state-missing-view-permission": "このユーザー状態を表示することは許可されていません。" } diff --git a/packages/h5p-server/assets/translations/server/km.json b/packages/h5p-server/assets/translations/server/km.json index 245e2db20..3b2b0f088 100644 --- a/packages/h5p-server/assets/translations/server/km.json +++ b/packages/h5p-server/assets/translations/server/km.json @@ -60,5 +60,18 @@ "content-hub-download-error": "មានកំហុសក្នុងការភ្ជាប់ទៅនឹង H5P Content Hub ។ សូម​ព្យាយាម​ម្តង​ទៀត​នៅ​ពេល​ក្រោយ។", "install-missing-libraries": "មិនអាចផ្ទុកមាតិកាបានទេព្រោះបណ្ណាល័យខាងក្រោមនេះមិនត្រូវបានតំឡើងនៅលើប្រព័ន្ធនេះ៖ {{libraries}}", "install-library-lock-timeout": "បណ្ណាល័យ {{ubername}} មិន​អាច​ត្រូវ​បាន​ដំឡើង​ទេ ព្រោះ​បណ្ណាល័យ​ដូចគ្នា​កំពុង​ត្រូវ​បាន​ដំឡើង​ពី​កន្លែង​ផ្សេង (រង់ចាំ {{limit}} ms សម្រាប់​ចាក់សោ​បណ្ណាល័យ)។", - "install-library-lock-max-time-exceeded": "បណ្ណាល័យ {{ubername}} មិន​អាច​ត្រូវ​បាន​ដំឡើង​ទេ ព្រោះ​វា​ចំណាយ​ពេល​យូរ​ជាង​ការ​អនុញ្ញាត {{limit}} ms ។" + "install-library-lock-max-time-exceeded": "បណ្ណាល័យ {{ubername}} មិន​អាច​ត្រូវ​បាន​ដំឡើង​ទេ ព្រោះ​វា​ចំណាយ​ពេល​យូរ​ជាង​ការ​អនុញ្ញាត {{limit}} ms ។", + "content-missing-create-permission": "អ្នកមិនត្រូវបានអនុញ្ញាតឱ្យបង្កើតមាតិកាថ្មីទេ។", + "content-missing-delete-permission": "អ្នកមិនត្រូវបានអនុញ្ញាតឱ្យលុបមាតិកានេះទេ។", + "content-missing-edit-permission": "អ្នកមិនត្រូវបានអនុញ្ញាតឱ្យកែសម្រួលខ្លឹមសារនេះទេ។", + "content-missing-list-permission": "អ្នកមិនត្រូវបានអនុញ្ញាតឱ្យរាយបញ្ជីមាតិកាទាំងអស់ទេ។", + "content-missing-view-permission": "អ្នកមិនត្រូវបានអនុញ្ញាតឱ្យមើលខ្លឹមសារនេះទេ។", + "finished-data-missing-delete-permission": "អ្នកមិនត្រូវបានអនុញ្ញាតឱ្យលុបទិន្នន័យដែលបានបញ្ចប់នេះទេ។", + "finished-data-missing-edit-permission": "អ្នកមិនត្រូវបានអនុញ្ញាតឱ្យកែសម្រួលទិន្នន័យដែលបានបញ្ចប់នេះទេ។", + "temporary-file-missing-delete-permission": "អ្នកមិនត្រូវបានអនុញ្ញាតឱ្យលុបឯកសារបណ្តោះអាសន្ននេះទេ។", + "temporary-file-missing-view-permission": "អ្នកមិនត្រូវបានអនុញ្ញាតឱ្យមើលឯកសារបណ្តោះអាសន្ននេះទេ។", + "temporary-file-missing-write-permission": "អ្នកមិនត្រូវបានអនុញ្ញាតឱ្យបង្កើតឯកសារបណ្តោះអាសន្នទេ។", + "user-state-missing-delete-permission": "អ្នកមិនត្រូវបានអនុញ្ញាតឱ្យលុបស្ថានភាពអ្នកប្រើប្រាស់នេះទេ។", + "user-state-missing-edit-permission": "អ្នកមិនត្រូវបានអនុញ្ញាតឱ្យកែសម្រួលស្ថានភាពអ្នកប្រើប្រាស់នេះទេ។", + "user-state-missing-view-permission": "អ្នកមិនត្រូវបានអនុញ្ញាតឱ្យមើលស្ថានភាពអ្នកប្រើប្រាស់នេះទេ។" } diff --git a/packages/h5p-server/assets/translations/server/ko.json b/packages/h5p-server/assets/translations/server/ko.json index e2063c138..c7f769a6d 100644 --- a/packages/h5p-server/assets/translations/server/ko.json +++ b/packages/h5p-server/assets/translations/server/ko.json @@ -60,5 +60,18 @@ "content-hub-download-error": "H5P 콘텐츠 허브에 연결하는 중에 오류가 발생했습니다. 나중에 다시 시도 해주십시오.", "install-missing-libraries": "이 시스템에 다음 라이브러리가 설치되어 있지 않으므로 콘텐츠를 로드할 수 없습니다. {{libraries}}", "install-library-lock-timeout": "도서관 {{ubername}} 같은 라이브러리가 다른 장소에서 설치되고 있기 때문에, 설치할 수 없습니다 (기다렸다 {{limit}} 라이브러리에 잠금 밀리 초).", - "install-library-lock-max-time-exceeded": "라이브러리 {{ubername}} 는이 허용 된 것보다 더 오래 걸렸다 때문에, 설치할 수없는 {{limit}} 밀리." + "install-library-lock-max-time-exceeded": "라이브러리 {{ubername}} 는이 허용 된 것보다 더 오래 걸렸다 때문에, 설치할 수없는 {{limit}} 밀리.", + "content-missing-create-permission": "새 콘텐츠를 만들 수 없습니다.", + "content-missing-delete-permission": "이 콘텐츠를 삭제할 권한이 없습니다.", + "content-missing-edit-permission": "이 콘텐츠를 편집할 권한이 없습니다.", + "content-missing-list-permission": "모든 콘텐츠를 나열할 수 없습니다.", + "content-missing-view-permission": "이 콘텐츠를 볼 수 없습니다.", + "finished-data-missing-delete-permission": "이 완료된 데이터를 삭제할 수 없습니다.", + "finished-data-missing-edit-permission": "이 완료된 데이터를 편집할 수 없습니다.", + "temporary-file-missing-delete-permission": "이 임시 파일을 삭제할 권한이 없습니다.", + "temporary-file-missing-view-permission": "이 임시 파일을 볼 수 없습니다.", + "temporary-file-missing-write-permission": "임시 파일을 만들 수 없습니다.", + "user-state-missing-delete-permission": "이 사용자 상태를 삭제할 권한이 없습니다.", + "user-state-missing-edit-permission": "이 사용자 상태를 편집할 권한이 없습니다.", + "user-state-missing-view-permission": "이 사용자 상태를 볼 수 없습니다." } diff --git a/packages/h5p-server/assets/translations/server/nl.json b/packages/h5p-server/assets/translations/server/nl.json index 15cb7e3d4..b04ce2ed0 100644 --- a/packages/h5p-server/assets/translations/server/nl.json +++ b/packages/h5p-server/assets/translations/server/nl.json @@ -60,5 +60,18 @@ "content-hub-download-error": "Er is een fout opgetreden bij het verbinden met de H5P Content Hub. Probeer het later nog eens.", "install-missing-libraries": "Kon de inhoud niet laden omdat de volgende bibliotheken niet op dit systeem zijn geïnstalleerd: {{libraries}}", "install-library-lock-timeout": "De bibliotheek {{ubername}} kon niet worden geïnstalleerd, omdat dezelfde bibliotheken vanaf een andere plaats worden geïnstalleerd ( {{limit}} ms gewacht op vergrendeling op de bibliotheek).", - "install-library-lock-max-time-exceeded": "De bibliotheek {{ubername}} kon niet worden geïnstalleerd, omdat het langer duurde dan de toegestane {{limit}} ms." + "install-library-lock-max-time-exceeded": "De bibliotheek {{ubername}} kon niet worden geïnstalleerd, omdat het langer duurde dan de toegestane {{limit}} ms.", + "content-missing-create-permission": "U mag geen nieuwe inhoud maken.", + "content-missing-delete-permission": "U mag deze inhoud niet verwijderen.", + "content-missing-edit-permission": "U mag deze inhoud niet bewerken.", + "content-missing-list-permission": "U mag niet alle inhoud vermelden.", + "content-missing-view-permission": "U mag deze inhoud niet bekijken.", + "finished-data-missing-delete-permission": "U mag deze voltooide gegevens niet verwijderen.", + "finished-data-missing-edit-permission": "U mag deze voltooide gegevens niet bewerken.", + "temporary-file-missing-delete-permission": "U mag dit tijdelijke bestand niet verwijderen.", + "temporary-file-missing-view-permission": "U mag dit tijdelijke bestand niet bekijken.", + "temporary-file-missing-write-permission": "U mag geen tijdelijk bestand aanmaken.", + "user-state-missing-delete-permission": "U mag deze gebruikersstatus niet verwijderen.", + "user-state-missing-edit-permission": "U mag deze gebruikersstatus niet bewerken.", + "user-state-missing-view-permission": "U mag deze gebruikersstatus niet bekijken." } diff --git a/packages/h5p-server/assets/translations/server/pt.json b/packages/h5p-server/assets/translations/server/pt.json index 97d50cac8..c888fe975 100644 --- a/packages/h5p-server/assets/translations/server/pt.json +++ b/packages/h5p-server/assets/translations/server/pt.json @@ -60,5 +60,18 @@ "content-hub-download-error": "Ocorreu um erro ao conectar ao H5P Content Hub. Por favor, tente novamente mais tarde.", "install-missing-libraries": "Não foi possível carregar o conteúdo porque as seguintes bibliotecas não estão instaladas neste sistema: {{libraries}}", "install-library-lock-timeout": "A biblioteca {{ubername}} não pôde ser instalada, porque as mesmas bibliotecas estão sendo instaladas de outro local (esperou {{limit}} ms pelo bloqueio na biblioteca).", - "install-library-lock-max-time-exceeded": "A biblioteca {{ubername}} não pôde ser instalada porque demorou mais do que o permitido {{limit}} ms." + "install-library-lock-max-time-exceeded": "A biblioteca {{ubername}} não pôde ser instalada porque demorou mais do que o permitido {{limit}} ms.", + "content-missing-create-permission": "Você não tem permissão para criar novos conteúdos.", + "content-missing-delete-permission": "Você não tem permissão para excluir este conteúdo.", + "content-missing-edit-permission": "Você não tem permissão para editar este conteúdo.", + "content-missing-list-permission": "Você não tem permissão para listar todo o conteúdo.", + "content-missing-view-permission": "Você não tem permissão para visualizar este conteúdo.", + "finished-data-missing-delete-permission": "Você não tem permissão para excluir esses dados concluídos.", + "finished-data-missing-edit-permission": "Você não tem permissão para editar esses dados finalizados.", + "temporary-file-missing-delete-permission": "Você não tem permissão para excluir este arquivo temporário.", + "temporary-file-missing-view-permission": "Você não tem permissão para visualizar este arquivo temporário.", + "temporary-file-missing-write-permission": "Você não tem permissão para criar um arquivo temporário.", + "user-state-missing-delete-permission": "Você não tem permissão para excluir este estado de usuário.", + "user-state-missing-edit-permission": "Você não tem permissão para editar este estado de usuário.", + "user-state-missing-view-permission": "Você não tem permissão para visualizar este estado de usuário." } diff --git a/packages/h5p-server/assets/translations/server/ru.json b/packages/h5p-server/assets/translations/server/ru.json index 342727b45..4c2ed6225 100644 --- a/packages/h5p-server/assets/translations/server/ru.json +++ b/packages/h5p-server/assets/translations/server/ru.json @@ -60,5 +60,18 @@ "content-hub-download-error": "При подключении к H5P Content Hub произошла ошибка. Пожалуйста, повторите попытку позже.", "install-missing-libraries": "Не удалось загрузить контент, поскольку в этой системе не установлены следующие библиотеки: {{libraries}}", "install-library-lock-timeout": "Не удалось установить библиотеку {{ubername}} , потому что те же библиотеки устанавливаются из другого места (ожидание блокировки библиотеки в течение {{limit}}", - "install-library-lock-max-time-exceeded": "Библиотеку {{ubername}} установить не удалось, потому что на это ушло больше разрешенной {{limit}} мс." + "install-library-lock-max-time-exceeded": "Библиотеку {{ubername}} установить не удалось, потому что на это ушло больше разрешенной {{limit}} мс.", + "content-missing-create-permission": "Вам не разрешено создавать новый контент.", + "content-missing-delete-permission": "Вам не разрешено удалять этот контент.", + "content-missing-edit-permission": "Вам не разрешено редактировать этот контент.", + "content-missing-list-permission": "Вы не можете перечислить весь контент.", + "content-missing-view-permission": "Вам не разрешено просматривать этот контент.", + "finished-data-missing-delete-permission": "Вы не можете удалить эти готовые данные.", + "finished-data-missing-edit-permission": "Вам не разрешено редактировать эти готовые данные.", + "temporary-file-missing-delete-permission": "Вам не разрешено удалять этот временный файл.", + "temporary-file-missing-view-permission": "Вам не разрешено просматривать этот временный файл.", + "temporary-file-missing-write-permission": "Вам не разрешено создавать временный файл.", + "user-state-missing-delete-permission": "Вам не разрешено удалять это состояние пользователя.", + "user-state-missing-edit-permission": "Вам не разрешено редактировать это состояние пользователя.", + "user-state-missing-view-permission": "Вам не разрешено просматривать это состояние пользователя." } diff --git a/packages/h5p-server/assets/translations/server/sl.json b/packages/h5p-server/assets/translations/server/sl.json index 50d53abb8..929bb618e 100644 --- a/packages/h5p-server/assets/translations/server/sl.json +++ b/packages/h5p-server/assets/translations/server/sl.json @@ -60,5 +60,18 @@ "content-hub-download-error": "Prišlo je do napake pri povezovanju s H5P Content Hub. Prosim poskusite kasneje.", "install-missing-libraries": "Vsebine ni bilo mogoče naložiti, ker v tem sistemu niso nameščene naslednje knjižnice: {{libraries}}", "install-library-lock-timeout": "Knjižnice {{ubername}} ni bilo mogoče namestiti, ker se iste knjižnice nameščajo z drugega mesta (čakal {{limit}} ms za zaklepanje knjižnice).", - "install-library-lock-max-time-exceeded": "Knjižnice {{ubername}} ni bilo mogoče namestiti, ker je trajalo dlje od dovoljene {{limit}} ms." + "install-library-lock-max-time-exceeded": "Knjižnice {{ubername}} ni bilo mogoče namestiti, ker je trajalo dlje od dovoljene {{limit}} ms.", + "content-missing-create-permission": "Ni vam dovoljeno ustvarjati nove vsebine.", + "content-missing-delete-permission": "Nimate dovoljenja za brisanje te vsebine.", + "content-missing-edit-permission": "Nimate dovoljenja za urejanje te vsebine.", + "content-missing-list-permission": "Nimate dovoljenja za seznam vseh vsebin.", + "content-missing-view-permission": "Nimate dovoljenja za ogled te vsebine.", + "finished-data-missing-delete-permission": "Teh končanih podatkov ne smete izbrisati.", + "finished-data-missing-edit-permission": "Nimate dovoljenja za urejanje teh končnih podatkov.", + "temporary-file-missing-delete-permission": "Nimate dovoljenja za brisanje te začasne datoteke.", + "temporary-file-missing-view-permission": "Nimate dovoljenja za ogled te začasne datoteke.", + "temporary-file-missing-write-permission": "Nimate dovoljenja za ustvarjanje začasne datoteke.", + "user-state-missing-delete-permission": "Nimate dovoljenja za brisanje tega uporabniškega stanja.", + "user-state-missing-edit-permission": "Nimate dovoljenja za urejanje tega uporabniškega stanja.", + "user-state-missing-view-permission": "Nimate dovoljenja za ogled tega uporabniškega stanja." } diff --git a/packages/h5p-server/assets/translations/server/sv.json b/packages/h5p-server/assets/translations/server/sv.json index db4f7f4db..78d5b7cc7 100644 --- a/packages/h5p-server/assets/translations/server/sv.json +++ b/packages/h5p-server/assets/translations/server/sv.json @@ -60,5 +60,18 @@ "content-hub-download-error": "Det uppstod ett fel vid anslutning till H5P Content Hub. Vänligen försök igen senare.", "install-missing-libraries": "Det gick inte att ladda innehållet eftersom följande bibliotek inte är installerade på det här systemet: {{libraries}}", "install-library-lock-timeout": "Biblioteket {{ubername}} kunde inte installeras, eftersom samma bibliotek installeras från en annan plats (väntade {{limit}} ms för låsning av biblioteket).", - "install-library-lock-max-time-exceeded": "Biblioteket {{ubername}} kunde inte installeras eftersom det tog längre tid än tillåtna {{limit}} ms." + "install-library-lock-max-time-exceeded": "Biblioteket {{ubername}} kunde inte installeras eftersom det tog längre tid än tillåtna {{limit}} ms.", + "content-missing-create-permission": "Du får inte skapa nytt innehåll.", + "content-missing-delete-permission": "Du får inte radera detta innehåll.", + "content-missing-edit-permission": "Du får inte redigera detta innehåll.", + "content-missing-list-permission": "Du får inte lista allt innehåll.", + "content-missing-view-permission": "Du får inte se detta innehåll.", + "finished-data-missing-delete-permission": "Du får inte radera denna färdiga data.", + "finished-data-missing-edit-permission": "Du får inte redigera denna färdiga data.", + "temporary-file-missing-delete-permission": "Du får inte ta bort den här temporära filen.", + "temporary-file-missing-view-permission": "Du får inte se den här temporära filen.", + "temporary-file-missing-write-permission": "Du får inte skapa en temporär fil.", + "user-state-missing-delete-permission": "Du får inte ta bort denna användarstatus.", + "user-state-missing-edit-permission": "Du får inte redigera denna användarstatus.", + "user-state-missing-view-permission": "Du får inte se denna användarstatus." } diff --git a/packages/h5p-server/assets/translations/server/tr.json b/packages/h5p-server/assets/translations/server/tr.json index 81498093a..14ac09a50 100644 --- a/packages/h5p-server/assets/translations/server/tr.json +++ b/packages/h5p-server/assets/translations/server/tr.json @@ -60,5 +60,18 @@ "content-hub-download-error": "H5P İçerik Merkezine bağlanırken bir hata oluştu. Lütfen daha sonra tekrar deneyiniz.", "install-missing-libraries": "Bu sistemde aşağıdaki kütüphaneler kurulu olmadığı için içerik yüklenemedi: {{libraries}}", "install-library-lock-timeout": "{{ubername}} kütüphanesi yüklenemedi, çünkü aynı kütüphaneler başka bir yerden yükleniyor ( {{limit}} ms beklendi).", - "install-library-lock-max-time-exceeded": "{{limit}} ms'den daha uzun sürdüğü için {{ubername}} kitaplığı yüklenemedi." + "install-library-lock-max-time-exceeded": "{{limit}} ms'den daha uzun sürdüğü için {{ubername}} kitaplığı yüklenemedi.", + "content-missing-create-permission": "Yeni içerik oluşturmanıza izin verilmiyor.", + "content-missing-delete-permission": "Bu içeriği silmenize izin verilmiyor.", + "content-missing-edit-permission": "Bu içeriği düzenlemenize izin verilmiyor.", + "content-missing-list-permission": "Tüm içeriği listelemenize izin verilmiyor.", + "content-missing-view-permission": "Bu içeriği görüntülemenize izin verilmiyor.", + "finished-data-missing-delete-permission": "Bu tamamlanmış verileri silmenize izin verilmez.", + "finished-data-missing-edit-permission": "Bu tamamlanmış verileri düzenlemenize izin verilmiyor.", + "temporary-file-missing-delete-permission": "Bu geçici dosyayı silmenize izin verilmiyor.", + "temporary-file-missing-view-permission": "Bu geçici dosyayı görüntüleme izniniz yok.", + "temporary-file-missing-write-permission": "Geçici bir dosya oluşturmanıza izin verilmez.", + "user-state-missing-delete-permission": "Bu kullanıcı durumunu silme izniniz yok.", + "user-state-missing-edit-permission": "Bu kullanıcı durumunu düzenleme izniniz yok.", + "user-state-missing-view-permission": "Bu kullanıcı durumunu görüntüleme izniniz yok." } diff --git a/packages/h5p-server/assets/translations/server/zh.json b/packages/h5p-server/assets/translations/server/zh.json index 39e1b339e..5bbeaee33 100644 --- a/packages/h5p-server/assets/translations/server/zh.json +++ b/packages/h5p-server/assets/translations/server/zh.json @@ -60,5 +60,18 @@ "content-hub-download-error": "连接到H5P内容中心时出错。请稍后再试。", "install-missing-libraries": "无法加载内容,因为此系统上未安装以下库: {{libraries}}", "install-library-lock-timeout": "无法安装库{{ubername}} ,因为正在从另一个地方安装相同的库(等待{{limit}}毫秒以锁定库)。", - "install-library-lock-max-time-exceeded": "无法安装库{{ubername}} ,因为它花费的时间超过了允许的{{limit}}毫秒。" + "install-library-lock-max-time-exceeded": "无法安装库{{ubername}} ,因为它花费的时间超过了允许的{{limit}}毫秒。", + "content-missing-create-permission": "您不得创建新内容。", + "content-missing-delete-permission": "您无权删除该内容。", + "content-missing-edit-permission": "您无权编辑此内容。", + "content-missing-list-permission": "您不能列出所有内容。", + "content-missing-view-permission": "您无权查看此内容。", + "finished-data-missing-delete-permission": "您不能删除此已完成的数据。", + "finished-data-missing-edit-permission": "您不能编辑此完成的数据。", + "temporary-file-missing-delete-permission": "您不能删除该临时文件。", + "temporary-file-missing-view-permission": "您无权查看此临时文件。", + "temporary-file-missing-write-permission": "不允许您创建临时文件。", + "user-state-missing-delete-permission": "您不能删除该用户状态。", + "user-state-missing-edit-permission": "您无权编辑此用户状态。", + "user-state-missing-view-permission": "您无权查看该用户状态。" } diff --git a/packages/h5p-server/src/ContentManager.ts b/packages/h5p-server/src/ContentManager.ts index 9b5b1b00d..fc8daadc1 100644 --- a/packages/h5p-server/src/ContentManager.ts +++ b/packages/h5p-server/src/ContentManager.ts @@ -7,11 +7,15 @@ import { IContentMetadata, IContentStorage, IContentUserDataStorage, + IFileStats, + IPermissionSystem, IUser, - Permission + ContentPermission } from './types'; import Logger from './helpers/Logger'; +import H5pError from './helpers/H5pError'; +import ContentUserDataManager from './ContentUserDataManager'; const log = new Logger('ContentManager'); @@ -26,11 +30,21 @@ export default class ContentManager { */ constructor( public contentStorage: IContentStorage, + private permissionSystem: IPermissionSystem, public contentUserDataStorage?: IContentUserDataStorage ) { log.info('initialize'); + + if (contentUserDataStorage) { + this.contentUserDataManager = new ContentUserDataManager( + contentUserDataStorage, + permissionSystem + ); + } } + private contentUserDataManager: ContentUserDataManager; + /** * Adds a content file to an existing content object. The content object has to be created with createContent(...) first. * @param contentId The id of the content to add the file to @@ -46,6 +60,24 @@ export default class ContentManager { user: IUser ): Promise { log.info(`adding file ${filename} to content ${contentId}`); + + if ( + !(await this.permissionSystem.checkForContent( + user, + ContentPermission.Edit, + contentId + )) + ) { + log.error( + `User tried to upload a file without proper permissions.` + ); + throw new H5pError( + 'h5p-server:content-missing-edit-permission', + {}, + 403 + ); + } + return this.contentStorage.addFile(contentId, filename, stream, user); } @@ -85,6 +117,42 @@ export default class ContentManager { contentId?: ContentId ): Promise { log.info(`creating content for ${contentId}`); + if (contentId) { + if ( + !(await this.permissionSystem.checkForContent( + user, + ContentPermission.Edit, + contentId + )) + ) { + log.error( + `User tried edit content without proper permissions.` + ); + throw new H5pError( + 'h5p-server:content-missing-edit-permission', + {}, + 403 + ); + } + } else { + if ( + !(await this.permissionSystem.checkForContent( + user, + ContentPermission.Create, + undefined + )) + ) { + log.error( + `User tried create content without proper permissions.` + ); + throw new H5pError( + 'h5p-server:content-missing-create-permission', + {}, + 403 + ); + } + } + return this.contentStorage.addContent( metadata, content, @@ -102,12 +170,30 @@ export default class ContentManager { contentId: ContentId, user: IUser ): Promise { + if ( + !(await this.permissionSystem.checkForContent( + user, + ContentPermission.Delete, + contentId + )) + ) { + log.error( + `User tried to delete a content object without proper permissions.` + ); + throw new H5pError( + 'h5p-server:content-missing-delete-permission', + {}, + 403 + ); + } + await this.contentStorage.deleteContent(contentId, user); - if (this.contentUserDataStorage) { + if (this.contentUserDataManager) { try { - await this.contentUserDataStorage.deleteAllContentUserDataByContentId( - contentId + await this.contentUserDataManager.deleteAllContentUserDataByContentId( + contentId, + user ); } catch (error) { log.error( @@ -116,8 +202,9 @@ export default class ContentManager { log.error(error); } try { - await this.contentUserDataStorage.deleteFinishedDataByContentId( - contentId + await this.contentUserDataManager.deleteFinishedDataByContentId( + contentId, + user ); } catch (error) { log.error( @@ -135,8 +222,26 @@ export default class ContentManager { */ public async deleteContentFile( contentId: ContentId, - filename: string + filename: string, + user?: IUser ): Promise { + if ( + !(await this.permissionSystem.checkForContent( + user, + ContentPermission.Edit, + contentId + )) + ) { + log.error( + `User tried to delete a file from a content object without proper permissions.` + ); + throw new H5pError( + 'h5p-server:content-missing-edit-permission', + {}, + 403 + ); + } + return this.contentStorage.deleteFile(contentId, filename); } @@ -158,6 +263,23 @@ export default class ContentManager { ): Promise { log.debug(`loading ${filename} for ${contentId}`); + if ( + !(await this.permissionSystem.checkForContent( + user, + ContentPermission.View, + contentId + )) + ) { + log.error( + `User tried to display a file from a content object without proper permissions.` + ); + throw new H5pError( + 'h5p-server:content-missing-view-permission', + {}, + 403 + ); + } + return this.contentStorage.getFileStream( contentId, filename, @@ -177,6 +299,23 @@ export default class ContentManager { contentId: ContentId, user: IUser ): Promise { + if ( + !(await this.permissionSystem.checkForContent( + user, + ContentPermission.View, + contentId + )) + ) { + log.error( + `User tried to get metadata of a content object without proper permissions.` + ); + throw new H5pError( + 'h5p-server:content-missing-view-permission', + {}, + 403 + ); + } + // We don't directly return the h5p.json file content as // we have to make sure it conforms to the schema. return new ContentMetadata( @@ -184,6 +323,31 @@ export default class ContentManager { ); } + public async getContentFileStats( + contentId: string, + filename: string, + user: IUser + ): Promise { + if ( + !(await this.permissionSystem.checkForContent( + user, + ContentPermission.View, + contentId + )) + ) { + log.error( + `User tried to get stats of file from a content object without view permissions.` + ); + throw new H5pError( + 'h5p-server:content-missing-view-permission', + {}, + 403 + ); + } + + return this.contentStorage.getFileStats(contentId, filename, user); + } + /** * Returns the content object (=contents of content/content.json) of a piece of content. * @param contentId the content id @@ -194,21 +358,24 @@ export default class ContentManager { contentId: ContentId, user: IUser ): Promise { - return this.contentStorage.getParameters(contentId, user); - } + if ( + !(await this.permissionSystem.checkForContent( + user, + ContentPermission.View, + contentId + )) + ) { + log.error( + `User tried to get parameters of a content object without view permissions.` + ); + throw new H5pError( + 'h5p-server:content-missing-view-permission', + {}, + 403 + ); + } - /** - * Returns an array of permissions a user has on a piece of content. - * @param contentId the content to check - * @param user the user who wants to access the piece of content - * @returns an array of permissions - */ - public async getUserPermissions( - contentId: ContentId, - user: IUser - ): Promise { - log.info(`checking user permissions for ${contentId}`); - return this.contentStorage.getUserPermissions(contentId, user); + return this.contentStorage.getParameters(contentId, user); } /** @@ -216,7 +383,24 @@ export default class ContentManager { * @param user (optional) the user who owns the content * @returns a list of contentIds */ - public listContent(user?: IUser): Promise { + public async listContent(user?: IUser): Promise { + if ( + !(await this.permissionSystem.checkForContent( + user, + ContentPermission.List, + undefined + )) + ) { + log.error( + `User tried to list all content objects without proper permissions.` + ); + throw new H5pError( + 'h5p-server:content-missing-list-permission', + {}, + 403 + ); + } + return this.contentStorage.listContent(user); } @@ -231,6 +415,23 @@ export default class ContentManager { user: IUser ): Promise { log.info(`loading content files for ${contentId}`); + if ( + !(await this.permissionSystem.checkForContent( + user, + ContentPermission.View, + contentId + )) + ) { + log.error( + `User tried to get the list of files from a content object without view permissions.` + ); + throw new H5pError( + 'h5p-server:content-missing-view-permission', + {}, + 403 + ); + } + return this.contentStorage.listFiles(contentId, user); } diff --git a/packages/h5p-server/src/ContentStorer.ts b/packages/h5p-server/src/ContentStorer.ts index b61a9ae33..2e43f0293 100644 --- a/packages/h5p-server/src/ContentStorer.ts +++ b/packages/h5p-server/src/ContentStorer.ts @@ -152,7 +152,6 @@ export default class ContentStorer { /** * Adds content from a H5P package (in a temporary directory) to the system. - * It does not check whether the user has permissions to save content. * @deprecated The method should not be used as it anymore, as there might * be issues with invalid filenames! * @param packageDirectory The absolute path containing the package (the @@ -181,7 +180,7 @@ export default class ContentStorer { ); const newContentId: ContentId = - await this.contentManager.contentStorage.addContent( + await this.contentManager.createOrUpdateContent( metadata, parameters, user, @@ -195,18 +194,17 @@ export default class ContentStorer { const readStream: Stream = fsExtra.createReadStream(file); const localPath: string = file.substr(contentPathLength); log.debug(`adding ${file} to ${packageDirectory}`); - return this.contentManager.contentStorage.addFile( + return this.contentManager.addContentFile( newContentId, localPath, - readStream + readStream, + user ); }) ); } catch (error) { log.error(error); - await this.contentManager.contentStorage.deleteContent( - newContentId - ); + await this.contentManager.deleteContent(newContentId, user); throw error; } return { id: newContentId, metadata, parameters }; diff --git a/packages/h5p-server/src/ContentTypeInformationRepository.ts b/packages/h5p-server/src/ContentTypeInformationRepository.ts index cc1b64a8d..88d248622 100644 --- a/packages/h5p-server/src/ContentTypeInformationRepository.ts +++ b/packages/h5p-server/src/ContentTypeInformationRepository.ts @@ -14,8 +14,10 @@ import { IHubInfo, IInstalledLibrary, ILibraryInstallResult, + IPermissionSystem, ITranslationFunction, - IUser + IUser, + GeneralPermission } from './types'; import Logger from './helpers/Logger'; import TranslatorWithFallback from './helpers/TranslatorWithFallback'; @@ -47,6 +49,7 @@ export default class ContentTypeInformationRepository { private contentTypeCache: ContentTypeCache, private libraryManager: LibraryManager, private config: IH5PConfig, + private permissionSystem: IPermissionSystem, translationCallback?: ITranslationFunction ) { log.info(`initialize`); @@ -89,8 +92,14 @@ export default class ContentTypeInformationRepository { libraries: hubInfoWithLocalInfo, outdated: (await this.contentTypeCache.isOutdated()) && - (user.canInstallRecommended || - user.canUpdateAndInstallLibraries), + ((await this.permissionSystem.checkForGeneralAction( + user, + GeneralPermission.InstallRecommended + )) || + (await this.permissionSystem.checkForGeneralAction( + user, + GeneralPermission.UpdateAndInstallLibraries + ))), recentlyUsed: [], // TODO: store this somewhere user: user.type }; @@ -124,7 +133,7 @@ export default class ContentTypeInformationRepository { } // Reject installation of content types that the user has no permission to - if (!this.canInstallLibrary(localContentType[0], user)) { + if (!(await this.canInstallLibrary(localContentType[0], user))) { log.warn( `rejecting installation of content type ${machineName}: user has no permission` ); @@ -152,7 +161,8 @@ export default class ContentTypeInformationRepository { const packageImporter = new PackageImporter( this.libraryManager, - this.config + this.config, + this.permissionSystem ); installedLibraries = await packageImporter.installLibrariesFromPackage( @@ -214,7 +224,10 @@ export default class ContentTypeInformationRepository { patchVersion: localLib.patchVersion, restricted: this.libraryIsRestricted(localLib) && - !user.canCreateRestricted, + !(await this.permissionSystem.checkForGeneralAction( + user, + GeneralPermission.CreateRestricted + )), title: localLib.title })); const finalLocalLibs = await Promise.all(localLibs); @@ -264,17 +277,26 @@ export default class ContentTypeInformationRepository { ); if (!localLib) { hubLib.installed = false; - hubLib.restricted = !this.canInstallLibrary(hubLib, user); - hubLib.canInstall = this.canInstallLibrary(hubLib, user); + hubLib.restricted = !(await this.canInstallLibrary( + hubLib, + user + )); + hubLib.canInstall = await this.canInstallLibrary( + hubLib, + user + ); hubLib.isUpToDate = true; } else { hubLib.installed = true; hubLib.restricted = this.libraryIsRestricted(localLib) && - !user.canCreateRestricted; + !(await this.permissionSystem.checkForGeneralAction( + user, + GeneralPermission.CreateRestricted + )); hubLib.canInstall = !this.libraryIsRestricted(localLib) && - this.canInstallLibrary(hubLib, user); + (await this.canInstallLibrary(hubLib, user)); hubLib.isUpToDate = !(await this.libraryManager.libraryHasUpgrade(hubLib)); hubLib.localMajorVersion = localLib.majorVersion; @@ -290,13 +312,23 @@ export default class ContentTypeInformationRepository { * Checks if users can install library due to their rights. * @param library */ - private canInstallLibrary(library: IHubContentType, user: IUser): boolean { + private async canInstallLibrary( + library: IHubContentType, + user: IUser + ): Promise { log.verbose( `checking if user can install library ${library.machineName}` ); return ( - user.canUpdateAndInstallLibraries || - (library.isRecommended && user.canInstallRecommended) + (await this.permissionSystem.checkForGeneralAction( + user, + GeneralPermission.UpdateAndInstallLibraries + )) || + (library.isRecommended && + (await this.permissionSystem.checkForGeneralAction( + user, + GeneralPermission.InstallRecommended + ))) ); } diff --git a/packages/h5p-server/src/ContentUserDataManager.ts b/packages/h5p-server/src/ContentUserDataManager.ts index 985c9c06d..488576e13 100644 --- a/packages/h5p-server/src/ContentUserDataManager.ts +++ b/packages/h5p-server/src/ContentUserDataManager.ts @@ -3,9 +3,12 @@ import { ISerializedContentUserData, IUser, IContentUserDataStorage, - IContentUserData + IContentUserData, + IPermissionSystem, + UserDataPermission } from './types'; import Logger from './helpers/Logger'; +import H5pError from './helpers/H5pError'; const log = new Logger('ContentUserDataManager'); @@ -17,32 +20,71 @@ const log = new Logger('ContentUserDataManager'); export default class ContentUserDataManager { /** * @param contentUserDataStorage The storage object + * @param permissionSystem grants or rejects permissions */ - constructor(private contentUserDataStorage: IContentUserDataStorage) { + constructor( + private contentUserDataStorage: IContentUserDataStorage, + private permissionSystem: IPermissionSystem + ) { log.info('initialize'); } /** - * Deletes a contentUserData object for given contentId and userId. Throws + * Deletes a contentUserData object for given contentId and user id. Throws * errors if something goes wrong. - * @param user the user for which the contentUserData object should be + * @param forUserId the user for which the contentUserData object should be * deleted + * @param actingUser the user who is currently active */ - public async deleteAllContentUserDataByUser(user: IUser): Promise { - if (this.contentUserDataStorage) { - log.debug(`deleting contentUserData for userId ${user.id}`); - return this.contentUserDataStorage.deleteAllContentUserDataByUser( - user + public async deleteAllContentUserDataByUser( + forUserId: string, + actingUser: IUser + ): Promise { + if (!this.contentUserDataStorage) { + return; + } + + log.debug(`Deleting contentUserData for userId ${forUserId}`); + + if ( + !(await this.permissionSystem.checkForUserData( + actingUser, + UserDataPermission.DeleteState, + undefined, + forUserId + )) + ) { + log.error( + `User tried delete content states without proper permissions.` + ); + throw new H5pError( + 'h5p-server:user-state-missing-delete-permission', + {}, + 403 ); } + + return this.contentUserDataStorage.deleteAllContentUserDataByUser( + actingUser + ); } + /** + * Deletes all user data of a content object, if its "invalidate" flag is + * set. This method is normally called, if a content object was changed and + * the user data has become invalid because of that. + * @param contentId + */ public async deleteInvalidatedContentUserDataByContentId( contentId: ContentId ): Promise { - if (this.contentUserDataStorage && contentId) { + if (!this.contentUserDataStorage) { + return; + } + + if (contentId) { log.debug( - `deleting invalidated contentUserData for contentId ${contentId}` + `Deleting invalidated contentUserData for contentId ${contentId}` ); return this.contentUserDataStorage.deleteInvalidatedContentUserData( contentId @@ -50,17 +92,43 @@ export default class ContentUserDataManager { } } + /** + * Deletes all states of a content object. Normally called when the content + * object is deleted. + * @param contentId + * @param actingUser + */ public async deleteAllContentUserDataByContentId( - contentId: ContentId + contentId: ContentId, + actingUser: IUser ): Promise { - if (this.contentUserDataStorage) { - log.debug( - `deleting all content user data for contentId ${contentId}` + if (!this.contentUserDataStorage) { + return; + } + + log.debug(`Deleting all content user data for contentId ${contentId}`); + + if ( + !(await this.permissionSystem.checkForUserData( + actingUser, + UserDataPermission.DeleteState, + contentId, + undefined + )) + ) { + log.error( + `User tried delete content user state without proper permissions.` ); - return this.contentUserDataStorage.deleteAllContentUserDataByContentId( - contentId + throw new H5pError( + 'h5p-server:user-state-missing-delete-permission', + {}, + 403 ); } + + return this.contentUserDataStorage.deleteAllContentUserDataByContentId( + contentId + ); } /** @@ -68,65 +136,111 @@ export default class ContentUserDataManager { * @param contentId The id of the content to load user data from * @param dataType Used by the h5p.js client * @param subContentId The id provided by the h5p.js client call - * @param user The user who is accessing the h5p + * @param actingUser The user who is accessing the h5p. Normally this is + * also the user for who the state should be fetched. * @param contextId an arbitrary value that can be used to save multiple * states for one content - user tuple + * @param asUserId If set, the state of this user will be fetched instead of + * the one of `actingUser' * @returns the saved state as string or undefined when not found */ public async getContentUserData( contentId: ContentId, dataType: string, subContentId: string, - user: IUser, - contextId?: string + actingUser: IUser, + contextId?: string, + asUserId?: string ): Promise { if (!this.contentUserDataStorage) { - return undefined; + return; } log.debug( - `loading contentUserData for user with id ${user.id}, contentId ${contentId}, subContentId ${subContentId}, dataType ${dataType}, contextId ${contextId}` + `Loading content user data for user with id ${ + asUserId ?? actingUser.id + }, contentId ${contentId}, subContentId ${subContentId}, dataType ${dataType}, contextId ${contextId}` ); + if ( + !(await this.permissionSystem.checkForUserData( + actingUser, + UserDataPermission.ViewState, + contentId, + asUserId ?? actingUser.id + )) + ) { + log.error( + `User tried view user content state without proper permissions.` + ); + throw new H5pError( + 'h5p-server:user-state-missing-view-permission', + {}, + 403 + ); + } + return this.contentUserDataStorage.getContentUserData( contentId, dataType, subContentId, - user, + asUserId ?? actingUser.id, contextId ); } /** - * Loads the content user data for given contentId and user. The returned data - * is an array of IContentUserData where the position in the array - * corresponds with the subContentId or undefined if there is no - * content user data. + * Loads the content user data for given contentId and user. The returned + * data is an array of IContentUserData where the position in the array + * corresponds with the subContentId or undefined if there is no content + * user data. * * @param contentId The id of the content to load user data from - * @param user The user who is accessing the h5p + * @param actingUser The user who is accessing the h5p. Normally this is + * also the user for who the integration should be generated. * @param contextId an arbitrary value that can be used to save multiple * states for one content - user tuple - * @returns an array of IContentUserData or undefined if no content user data - * is found. + * @param asUserId the user for which the integration should be generated, + * if they are different from the user who is accessing the state + * @returns an array of IContentUserData or undefined if no content user + * data is found. */ public async generateContentUserDataIntegration( contentId: ContentId, - user: IUser, - contextId?: string - ): Promise { + actingUser: IUser, + contextId?: string, + asUserId?: string + ): Promise { + if (!this.contentUserDataStorage) { + return; + } + log.debug( - `Generating contentUserDataIntegration for user with id ${user.id}, contentId ${contentId} and contextId ${contextId}.` + `Generating contentUserDataIntegration for user with id ${actingUser.id}, contentId ${contentId} and contextId ${contextId}.` ); - if (!this.contentUserDataStorage) { - return undefined; + if ( + !(await this.permissionSystem.checkForUserData( + actingUser, + UserDataPermission.ViewState, + contentId, + asUserId ?? actingUser.id + )) + ) { + log.error( + `User tried viewing user content state without proper permissions.` + ); + throw new H5pError( + 'h5p-server:user-state-missing-view-permission', + {}, + 403 + ); } let states = await this.contentUserDataStorage.getContentUserDataByContentIdAndUser( contentId, - user, + asUserId ?? actingUser.id, contextId ); @@ -161,7 +275,7 @@ export default class ContentUserDataManager { * time * @param completionTime the time the user needed to complete the content * (as integer) - * @param user The user who triggers this method via /setFinished + * @param actingUser The user who triggers this method via /setFinished */ public async setFinished( contentId: ContentId, @@ -170,14 +284,32 @@ export default class ContentUserDataManager { openedTimestamp: number, finishedTimestamp: number, completionTime: number, - user: IUser + actingUser: IUser ): Promise { + if (!this.contentUserDataStorage) { + return; + } + log.debug( - `saving finished data for ${user.id} and contentId ${contentId}` + `saving finished data for ${actingUser.id} and contentId ${contentId}` ); - if (!this.contentUserDataStorage) { - return undefined; + if ( + !(await this.permissionSystem.checkForUserData( + actingUser, + UserDataPermission.EditFinished, + contentId, + actingUser.id + )) + ) { + log.error( + `User tried add finished data without proper permissions.` + ); + throw new H5pError( + 'h5p-server:finished-data-missing-edit-permission', + {}, + 403 + ); } await this.contentUserDataStorage.createOrUpdateFinishedData({ @@ -187,7 +319,7 @@ export default class ContentUserDataManager { openedTimestamp, finishedTimestamp, completionTime, - userId: user.id + userId: actingUser.id }); } @@ -197,9 +329,12 @@ export default class ContentUserDataManager { * @param dataType Used by the h5p.js client * @param subContentId The id provided by the h5p.js client call * @param userState The userState as string - * @param user The user who owns this object + * @param actingUser The user who is currently active; normally this is also + * the owner of the user data * @param contextId an arbitrary value that can be used to save multiple * states for one content - user tuple + * @param asUserId if the acting user is different from the owner of the + * user data, you can specify the owner here * @returns the saved state as string */ public async createOrUpdateContentUserData( @@ -209,19 +344,43 @@ export default class ContentUserDataManager { userState: string, invalidate: boolean, preload: boolean, - user: IUser, - contextId?: string + actingUser: IUser, + contextId?: string, + asUserId?: string ): Promise { - log.debug( - `saving contentUserData for user with id ${user.id} and contentId ${contentId}` - ); - if (typeof invalidate !== 'boolean' || typeof preload !== 'boolean') { log.error(`invalid arguments passed for contentId ${contentId}`); throw new Error( "createOrUpdateContentUserData received invalid arguments: invalidate or preload weren't boolean" ); } + if (!this.contentUserDataStorage) { + return; + } + + log.debug( + `Saving contentUserData for user with id ${ + asUserId ?? actingUser.id + } and contentId ${contentId}` + ); + + if ( + !(await this.permissionSystem.checkForUserData( + actingUser, + UserDataPermission.EditState, + contentId, + asUserId ?? actingUser.id + )) + ) { + log.error( + `User tried add / edit user content state without proper permissions.` + ); + throw new H5pError( + 'h5p-server:user-state-missing-edit-permission', + {}, + 403 + ); + } if (this.contentUserDataStorage) { return this.contentUserDataStorage.createOrUpdateContentUserData({ @@ -232,8 +391,43 @@ export default class ContentUserDataManager { preload, subContentId, userState, - userId: user.id + userId: asUserId ?? actingUser.id }); } } + + /** + * Deletes all finished data for a content object + * @param contentId the id of the content object + * @param actingUser the currently active user + */ + public async deleteFinishedDataByContentId( + contentId: string, + actingUser: IUser + ): Promise { + if (!this.contentUserDataStorage) { + return; + } + + if ( + !(await this.permissionSystem.checkForUserData( + actingUser, + UserDataPermission.DeleteFinished, + contentId, + undefined + )) + ) { + log.error( + `User tried add delete finished data for content without proper permissions.` + ); + throw new H5pError( + 'h5p-server:finished-data-missing-delete-permission', + {}, + 403 + ); + } + await this.contentUserDataStorage.deleteFinishedDataByContentId( + contentId + ); + } } diff --git a/packages/h5p-server/src/H5PAjaxEndpoint.ts b/packages/h5p-server/src/H5PAjaxEndpoint.ts index bdf57aacf..a2ef21a9b 100644 --- a/packages/h5p-server/src/H5PAjaxEndpoint.ts +++ b/packages/h5p-server/src/H5PAjaxEndpoint.ts @@ -198,7 +198,7 @@ export default class H5PAjaxEndpoint { } // getFileStats validates filenames itself, so we don't do it here. - const stats = await this.h5pEditor.contentStorage.getFileStats( + const stats = await this.h5pEditor.contentManager.getContentFileStats( contentId, filename, user @@ -384,7 +384,7 @@ export default class H5PAjaxEndpoint { } // Filenames are validated in the storage classes, so we don't validate // them here. - const stats = await this.h5pEditor.temporaryStorage.getFileStats( + const stats = await this.h5pEditor.temporaryFileManager.getFileStats( filename, user ); diff --git a/packages/h5p-server/src/H5PEditor.ts b/packages/h5p-server/src/H5PEditor.ts index 4f49e7cab..3de60693d 100644 --- a/packages/h5p-server/src/H5PEditor.ts +++ b/packages/h5p-server/src/H5PEditor.ts @@ -65,6 +65,7 @@ import SimpleTranslator from './helpers/SimpleTranslator'; import DependencyGetter from './DependencyGetter'; import ContentHub from './ContentHub'; import { downloadFile } from './helpers/downloadFile'; +import { LaissezFairePermissionSystem } from './implementation/LaissezFairePermissionSystem'; const log = new Logger('H5PEditor'); @@ -106,6 +107,9 @@ export default class H5PEditor { ) { log.info('initialize'); + const permissionSystem = + options?.permissionSystem ?? new LaissezFairePermissionSystem(); + this.config = config; this.renderer = defaultRenderer; @@ -128,20 +132,25 @@ export default class H5PEditor { ); this.contentManager = new ContentManager( contentStorage, + permissionSystem, contentUserDataStorage ); this.contentTypeRepository = new ContentTypeInformationRepository( this.contentTypeCache, this.libraryManager, config, + permissionSystem, options?.enableHubLocalization ? translationCallback : undefined ); this.temporaryFileManager = new TemporaryFileManager( temporaryStorage, - this.config + this.config, + permissionSystem ); + this.contentUserDataManager = new ContentUserDataManager( - contentUserDataStorage + contentUserDataStorage, + permissionSystem ); this.contentStorer = new ContentStorer( this.contentManager, @@ -151,6 +160,7 @@ export default class H5PEditor { this.packageImporter = new PackageImporter( this.libraryManager, this.config, + permissionSystem, this.contentManager, this.contentStorer ); diff --git a/packages/h5p-server/src/H5PPlayer.ts b/packages/h5p-server/src/H5PPlayer.ts index e16ad0e07..f10b49754 100644 --- a/packages/h5p-server/src/H5PPlayer.ts +++ b/packages/h5p-server/src/H5PPlayer.ts @@ -31,6 +31,8 @@ import LibraryManager from './LibraryManager'; import SemanticsLocalizer from './SemanticsLocalizer'; import SimpleTranslator from './helpers/SimpleTranslator'; import ContentUserDataManager from './ContentUserDataManager'; +import ContentManager from './ContentManager'; +import { LaissezFairePermissionSystem } from './implementation/LaissezFairePermissionSystem'; const log = new Logger('Player'); @@ -77,7 +79,17 @@ export default class H5PPlayer { this.config ); + const permissionSystem = + options?.permissionSystem ?? new LaissezFairePermissionSystem(); + this.contentUserDataManager = new ContentUserDataManager( + contentUserDataStorage, + permissionSystem + ); + + this.contentManager = new ContentManager( + contentStorage, + permissionSystem, contentUserDataStorage ); @@ -103,6 +115,7 @@ export default class H5PPlayer { private globalCustomScripts: string[] = []; private globalCustomStyles: string[] = []; private libraryManager: LibraryManager; + private contentManager: ContentManager; private contentUserDataManager: ContentUserDataManager; private renderer: (model: IPlayerModel) => string | any; @@ -112,24 +125,36 @@ export default class H5PPlayer { * with the content id. Only call it with parameters and metadata if don't * want to use the IContentStorage object passed into the constructor. * @param contentId the content id - * @param user the user who wants to access the content - * @param ignoreUserPermission (optional) If set to true, the user object - * won't be passed to the storage classes for permission checks. You can use - * this option if you have already checked the user's permission in a - * different layer. - * @param parametersOverride (optional) the parameters of a piece of content - * (=content.json); if you use this option, the parameters won't be loaded - * from storage - * @param metadataOverride (optional) the metadata of a piece of content - * (=h5p.json); if you use this option, the parameters won't be loaded from - * storage - * @param contextId (optional) allows implementations to have multiple - * content states for a single content object and user tuple + * @param actingUser the user who wants to access the content + * @param options.ignoreUserPermission (optional) If set to true, the user + * object won't be passed to the storage classes for permission checks. You + * can use this option if you have already checked the user's permission in + * a different layer. + * @param options.parametersOverride (optional) the parameters of a piece of + * content (=content.json); if you use this option, the parameters won't be + * loaded from storage + * @param options.metadataOverride (optional) the metadata of a piece of + * content (=h5p.json); if you use this option, the parameters won't be + * loaded from storage + * @param options.contextId (optional) allows implementations to have + * multiple content states for a single content object and user tuple + * @param options.asUserId (optional) allows you to impersonate another + * user. You will see their user state instead of yours. + * @param options.readOnlyState (optional) allows you to disable saving of + the user state. You will still see the state, but changes won't be + persisted. This is useful if you want to review other users' states by + setting `asUserId` and don't want to change their state. Note that the + H5P doesn't support this behavior and we use a workaround to implement + it. The workaround includes setting the query parameter `ignorePost=yes` + in the URL of the content state Ajax call. The h5p-express adapter + ignores posts that have this query parameter. You should, however, still + prevent malicious users from writing other users' states in the + permission system! * @returns a HTML string that you can insert into your page */ public async render( contentId: ContentId, - user: IUser, + actingUser: IUser, language: string = 'en', options?: { ignoreUserPermissions?: boolean; @@ -142,16 +167,21 @@ export default class H5PPlayer { showH5PIcon?: boolean; showLicenseButton?: boolean; contextId?: string; + asUserId?: string; // the user for which the content state should be displayed; + readOnlyState?: boolean; } ): Promise { log.debug(`rendering page for ${contentId} in language ${language}`); + if (options?.asUserId) { + log.debug(`Personifying ${options.asUserId}`); + } let parameters: ContentParameters; if (!options?.parametersOverride) { try { - parameters = await this.contentStorage.getParameters( + parameters = await this.contentManager.getContentParameters( contentId, - options?.ignoreUserPermissions ? undefined : user + options?.ignoreUserPermissions ? undefined : actingUser ); } catch (error) { throw new H5pError('h5p-player:content-missing', {}, 404); @@ -163,9 +193,9 @@ export default class H5PPlayer { let metadata: ContentMetadata; if (!options?.metadataOverride) { try { - metadata = await this.contentStorage.getMetadata( + metadata = await this.contentManager.getContentMetadata( contentId, - options?.ignoreUserPermissions ? undefined : user + options?.ignoreUserPermissions ? undefined : actingUser ); } catch (error) { throw new H5pError('h5p-player:content-missing', {}, 404); @@ -223,7 +253,7 @@ export default class H5PPlayer { metadata, assets, mainLibrarySupportsFullscreen, - user, + actingUser, language, { showCopyButton: options?.showCopyButton ?? false, @@ -233,13 +263,15 @@ export default class H5PPlayer { showH5PIcon: options?.showH5PIcon ?? false, showLicenseButton: options?.showLicenseButton ?? false }, - options?.contextId + options?.contextId, + options?.asUserId, + options?.readOnlyState ), scripts: this.listCoreScripts().concat(assets.scripts), styles: this.listCoreStyles().concat(assets.styles), translations: {}, embedTypes: metadata.embedTypes, // TODO: check if the library supports the embed type! - user + user: actingUser }; return this.renderer(model); @@ -359,7 +391,7 @@ export default class H5PPlayer { metadata: IContentMetadata, assets: IAssets, supportsFullscreen: boolean, - user: IUser, + actingUser: IUser, language: string, displayOptions: { showCopyButton: boolean; @@ -369,7 +401,9 @@ export default class H5PPlayer { showH5PIcon: boolean; showLicenseButton: boolean; }, - contextId: string + contextId: string, + asUserId?: string, + readOnlyState?: boolean ): Promise { // see https://h5p.org/creating-your-own-h5p-plugin log.info(`generating integration for ${contentId}`); @@ -377,12 +411,14 @@ export default class H5PPlayer { return { ajax: { contentUserData: this.urlGenerator.contentUserData( - user, - contextId + actingUser, + contextId, + asUserId, + { readonly: readOnlyState } ), - setFinished: this.urlGenerator.setFinished(user) + setFinished: this.urlGenerator.setFinished(actingUser) }, - ajaxPath: this.urlGenerator.ajaxEndpoint(user), + ajaxPath: this.urlGenerator.ajaxEndpoint(actingUser), contents: { [`cid-${contentId}`]: { displayOptions: { @@ -400,8 +436,9 @@ export default class H5PPlayer { contentUserData: await this.contentUserDataManager.generateContentUserDataIntegration( contentId, - user, - contextId + actingUser, + contextId, + asUserId ), metadata: { license: metadata.license || 'U', @@ -437,25 +474,33 @@ export default class H5PPlayer { }, libraryConfig: this.config.libraryConfig, postUserStatistics: this.config.setFinishedEnabled, - saveFreq: - this.config.contentUserStateSaveInterval !== false - ? Math.round( - Number(this.config.contentUserStateSaveInterval) / - 1000 - ) || 1 - : false, + saveFreq: this.getSaveFreq(readOnlyState), url: this.urlGenerator.baseUrl(), hubIsEnabled: true, fullscreenDisabled: this.config.disableFullscreen ? 1 : 0, ...this.integrationObjectDefaults, user: { - name: user.name, - mail: user.email, - id: user.id + name: actingUser.name, + mail: actingUser.email, + id: actingUser.id } }; } + private getSaveFreq(readOnlyState: boolean): number | boolean { + if (readOnlyState) { + return Number.MAX_SAFE_INTEGER; + } + if (this.config.contentUserStateSaveInterval !== false) { + return ( + Math.round( + Number(this.config.contentUserStateSaveInterval) / 1000 + ) || 1 + ); + } + return false; + } + /** * Finds out which adds should be added to the library due to the settings * in the global configuration or in the library metadata. diff --git a/packages/h5p-server/src/PackageExporter.ts b/packages/h5p-server/src/PackageExporter.ts index 4cafb31b2..43e2b7931 100644 --- a/packages/h5p-server/src/PackageExporter.ts +++ b/packages/h5p-server/src/PackageExporter.ts @@ -10,13 +10,15 @@ import { ContentId, IContentMetadata, IUser, - Permission + ContentPermission, + IPermissionSystem } from './types'; import { ContentFileScanner } from './ContentFileScanner'; import Logger from './helpers/Logger'; import LibraryManager from './LibraryManager'; import generateFilename from './helpers/FilenameGenerator'; import { generalizedSanitizeFilename } from './implementation/utils'; +import { LaissezFairePermissionSystem } from './implementation/LaissezFairePermissionSystem'; const log = new Logger('PackageExporter'); @@ -37,14 +39,27 @@ export default class PackageExporter { private libraryManager: LibraryManager, // eslint-disable-next-line @typescript-eslint/default-param-last private contentStorage: IContentStorage = null, - { exportMaxContentPathLength }: { exportMaxContentPathLength: number } + { + exportMaxContentPathLength, + permissionSystem + }: { + exportMaxContentPathLength: number; + permissionSystem?: IPermissionSystem; + } ) { log.info(`initialize`); this.maxContentPathLength = exportMaxContentPathLength ?? 255; + if (permissionSystem) { + this.permissionSystem = permissionSystem; + } else { + this.permissionSystem = new LaissezFairePermissionSystem(); + } } private maxContentPathLength: number; + private permissionSystem: IPermissionSystem; + /** * Creates a .h5p-package for the specified content file and pipes it to the * stream. Throws H5pErrors if something goes wrong. The contents of the @@ -66,7 +81,7 @@ export default class PackageExporter { user: IUser ): Promise { log.info(`creating package for ${contentId}`); - await this.checkAccess(contentId, user); + await this.checkPermission(contentId, user); // create zip files const outputZipFile = new yazl.ZipFile(); @@ -186,7 +201,7 @@ export default class PackageExporter { * permissions for it. Throws an exception with the respective error message * if this is not the case. */ - private async checkAccess( + private async checkPermission( contentId: ContentId, user: IUser ): Promise { @@ -198,9 +213,11 @@ export default class PackageExporter { ); } if ( - !( - await this.contentStorage.getUserPermissions(contentId, user) - ).some((p) => p === Permission.Download) + !(await this.permissionSystem.checkForContent( + user, + ContentPermission.Download, + contentId + )) ) { throw new H5pError( 'download-content-forbidden', diff --git a/packages/h5p-server/src/PackageImporter.ts b/packages/h5p-server/src/PackageImporter.ts index e13508273..c36ce5d78 100644 --- a/packages/h5p-server/src/PackageImporter.ts +++ b/packages/h5p-server/src/PackageImporter.ts @@ -15,7 +15,9 @@ import { IH5PConfig, ILibraryInstallResult, ILibraryName, - IUser + IPermissionSystem, + IUser, + GeneralPermission } from './types'; import Logger from './helpers/Logger'; import LibraryName from './LibraryName'; @@ -55,6 +57,7 @@ export default class PackageImporter { constructor( private libraryManager: LibraryManager, private config: IH5PConfig, + private permissionSystem: IPermissionSystem, private contentManager: ContentManager = null, private contentStorer: ContentStorer = null ) { @@ -148,7 +151,10 @@ export default class PackageImporter { { copyMode: ContentCopyModes.Install, installLibraries: - user?.canUpdateAndInstallLibraries === true + await this.permissionSystem.checkForGeneralAction( + user, + GeneralPermission.UpdateAndInstallLibraries + ) }, user, contentId @@ -184,7 +190,11 @@ export default class PackageImporter { packagePath, { copyMode: ContentCopyModes.Temporary, - installLibraries: user.canUpdateAndInstallLibraries + installLibraries: + await this.permissionSystem.checkForGeneralAction( + user, + GeneralPermission.UpdateAndInstallLibraries + ) }, user ); diff --git a/packages/h5p-server/src/TemporaryFileManager.ts b/packages/h5p-server/src/TemporaryFileManager.ts index 2a1205792..864429b38 100644 --- a/packages/h5p-server/src/TemporaryFileManager.ts +++ b/packages/h5p-server/src/TemporaryFileManager.ts @@ -2,8 +2,16 @@ import { ReadStream } from 'fs'; import { Readable } from 'stream'; import Logger from './helpers/Logger'; -import { IH5PConfig, ITemporaryFileStorage, IUser } from './types'; +import { + IFileStats, + IH5PConfig, + IPermissionSystem, + ITemporaryFileStorage, + IUser, + TemporaryFilePermission +} from './types'; import FilenameGenerator from './helpers/FilenameGenerator'; +import H5pError from './helpers/H5pError'; const log = new Logger('TemporaryFileManager'); @@ -16,7 +24,8 @@ export default class TemporaryFileManager { */ constructor( private storage: ITemporaryFileStorage, - private config: IH5PConfig + private config: IH5PConfig, + private permissionSystem: IPermissionSystem ) { log.info('initialize'); } @@ -35,6 +44,23 @@ export default class TemporaryFileManager { dataStream: ReadStream, user: IUser ): Promise { + if ( + !(await this.permissionSystem.checkForTemporaryFile( + user, + TemporaryFilePermission.Create, + undefined + )) + ) { + log.error( + `User tried upload file to temporary storage without proper permissions.` + ); + throw new H5pError( + 'h5p-server:temporary-file-missing-write-permission', + {}, + 403 + ); + } + log.info(`Storing temporary file ${filename}`); const uniqueFilename = await this.generateUniqueName(filename, user); log.debug(`Assigned unique filename ${uniqueFilename}`); @@ -86,6 +112,24 @@ export default class TemporaryFileManager { */ public async deleteFile(filename: string, user: IUser): Promise { if (await this.storage.fileExists(filename, user)) { + if ( + user !== null && + !(await this.permissionSystem.checkForTemporaryFile( + user, + TemporaryFilePermission.Delete, + filename + )) + ) { + log.error( + `User tried to delete a file from a temporary storage without proper permissions.` + ); + throw new H5pError( + 'h5p-server:temporary-file-missing-delete-permission', + {}, + 403 + ); + } + await this.storage.deleteFile(filename, user.id); } } @@ -116,10 +160,59 @@ export default class TemporaryFileManager { rangeStart?: number, rangeEnd?: number ): Promise { + if ( + !(await this.permissionSystem.checkForTemporaryFile( + user, + TemporaryFilePermission.View, + filename + )) + ) { + log.error( + `User tried to display a file from a content object without proper permissions.` + ); + throw new H5pError( + 'h5p-server:temporary-file-missing-delete-permission', + {}, + 403 + ); + } + log.info(`Getting temporary file ${filename}`); + return this.storage.getFileStream(filename, user, rangeStart, rangeEnd); } + /** + * Returns a information about a temporary file. + * Throws an exception if the file does not exist. + * @param filename the relative path inside the library + * @param user the user who wants to access the file + * @returns the file stats + */ + public async getFileStats( + filename: string, + user: IUser + ): Promise { + if ( + !(await this.permissionSystem.checkForTemporaryFile( + user, + TemporaryFilePermission.View, + filename + )) + ) { + log.error( + `User tried to get stats of a content object without proper permissions.` + ); + throw new H5pError( + 'h5p-server:temporary-file-missing-view-permission', + {}, + 403 + ); + } + + return this.storage.getFileStats(filename, user); + } + /** * Tries generating a unique filename for the file by appending a * id to it. Checks in storage if the filename already exists and @@ -129,6 +222,7 @@ export default class TemporaryFileManager { * @param user the user who is saving the file * @returns the unique filename */ + protected async generateUniqueName( filename: string, user: IUser diff --git a/packages/h5p-server/src/UrlGenerator.ts b/packages/h5p-server/src/UrlGenerator.ts index ea1ca7713..c0a0b0962 100644 --- a/packages/h5p-server/src/UrlGenerator.ts +++ b/packages/h5p-server/src/UrlGenerator.ts @@ -1,3 +1,5 @@ +import { encode } from 'node:querystring'; + import { ContentId, IFullLibraryName, @@ -85,22 +87,44 @@ export default class UrlGenerator implements IUrlGenerator { ); } - public contentUserData = (user: IUser, contextId?: string): string => { + public contentUserData = ( + user: IUser, + contextId?: string, + asUserId?: string, + options?: { readonly?: boolean } + ): string => { + const queries: any = {}; + if (contextId) { + queries.contextId = contextId; + } + if (asUserId) { + queries.asUserId = asUserId; + } + if (options?.readonly) { + queries.ignorePost = 'yes'; + } + if ( this.csrfProtection?.queryParamGenerator && this.csrfProtection?.protectContentUserData ) { const qs = this.csrfProtection.queryParamGenerator(user); + if (qs.name) { + queries[qs.name] = qs.value; + } + const queryString = encode(queries); return `${this.config.baseUrl}${ this.config.contentUserDataUrl - }/:contentId/:dataType/:subContentId?${qs.name}=${qs.value}${ - contextId ? `&contextId=${contextId}` : '' + }/:contentId/:dataType/:subContentId${ + queryString ? `?${queryString}` : '' }`; } + + const queryString = encode(queries); return `${this.config.baseUrl}${ this.config.contentUserDataUrl }/:contentId/:dataType/:subContentId${ - contextId ? `?contextId=${contextId}` : '' + queryString ? `?${queryString}` : '' }`; }; diff --git a/packages/h5p-server/src/implementation/LaissezFairePermissionSystem.ts b/packages/h5p-server/src/implementation/LaissezFairePermissionSystem.ts new file mode 100644 index 000000000..a6ab1322f --- /dev/null +++ b/packages/h5p-server/src/implementation/LaissezFairePermissionSystem.ts @@ -0,0 +1,42 @@ +import { + IPermissionSystem, + IUser, + ContentPermission, + GeneralPermission, + TemporaryFilePermission, + UserDataPermission +} from '../types'; + +/** + * A permission system that allows everything to every user. + */ +export class LaissezFairePermissionSystem implements IPermissionSystem { + async checkForUserData( + _actingUser: IUser, + _permission: UserDataPermission, + _contentId: string, + _affectedUserId?: string + ): Promise { + return true; + } + async checkForGeneralAction( + _actingUser: IUser, + _permission: GeneralPermission + ): Promise { + return true; + } + async checkForContent( + _user: IUser, + _permission: ContentPermission, + _contentId?: string + ): Promise { + return true; + } + async checkForTemporaryFile( + _user: IUser, + _permission: TemporaryFilePermission, + _filename?: string + ): Promise { + return true; + } +} diff --git a/packages/h5p-server/src/implementation/fs/DirectoryTemporaryFileStorage.ts b/packages/h5p-server/src/implementation/fs/DirectoryTemporaryFileStorage.ts index 6b626664e..f21f15dbb 100644 --- a/packages/h5p-server/src/implementation/fs/DirectoryTemporaryFileStorage.ts +++ b/packages/h5p-server/src/implementation/fs/DirectoryTemporaryFileStorage.ts @@ -58,14 +58,19 @@ export default class DirectoryTemporaryFileStorage private maxFileLength: number; - public async deleteFile(filename: string, userId: string): Promise { + public async deleteFile(filename: string, ownerId: string): Promise { checkFilename(filename); - checkFilename(userId); - const filePath = this.getAbsoluteFilePath(userId, filename); + if (!ownerId) { + throw new Error( + 'Invalid arguments for DirectoryTemporaryFileStorage.deleteFile: you must specify an ownerId' + ); + } + checkFilename(ownerId); + const filePath = this.getAbsoluteFilePath(ownerId, filename); await fsExtra.remove(filePath); await fsExtra.remove(`${filePath}.metadata`); - const userDirectoryPath = this.getAbsoluteUserDirectoryPath(userId); + const userDirectoryPath = this.getAbsoluteUserDirectoryPath(ownerId); const fileDirectoryPath = path.dirname(filePath); if (userDirectoryPath !== fileDirectoryPath) { await this.deleteEmptyDirectory(fileDirectoryPath); diff --git a/packages/h5p-server/src/implementation/fs/FileContentStorage.ts b/packages/h5p-server/src/implementation/fs/FileContentStorage.ts index 7be63f4f7..76ba5a73c 100644 --- a/packages/h5p-server/src/implementation/fs/FileContentStorage.ts +++ b/packages/h5p-server/src/implementation/fs/FileContentStorage.ts @@ -11,7 +11,6 @@ import { IContentMetadata, IContentStorage, IUser, - Permission, ContentParameters, IFileStats, ILibraryName @@ -392,26 +391,6 @@ export default class FileContentStorage implements IContentStorage { return { asDependency, asMainLibrary }; } - /** - * Returns an array of permissions that the user has on the piece of content - * @param contentId the content id to check - * @param user the user who wants to access the piece of content - * @returns the permissions the user has for this content (e.g. download it, - * delete it etc.) - */ - public async getUserPermissions( - contentId: ContentId, - user: IUser - ): Promise { - return [ - Permission.Delete, - Permission.Download, - Permission.Edit, - Permission.Embed, - Permission.View - ]; - } - /** * Lists the content objects in the system (if no user is specified) or * owned by the user. diff --git a/packages/h5p-server/src/implementation/fs/FileContentUserDataStorage.ts b/packages/h5p-server/src/implementation/fs/FileContentUserDataStorage.ts index 9f251a8e9..d5380d97a 100644 --- a/packages/h5p-server/src/implementation/fs/FileContentUserDataStorage.ts +++ b/packages/h5p-server/src/implementation/fs/FileContentUserDataStorage.ts @@ -33,7 +33,7 @@ export default class FileContentUserDataStorage contentId: ContentId, dataType: string, subContentId: string, - user: IUser, + userId: string, contextId?: string ): Promise { const file = this.getUserDataFilePath(contentId); @@ -56,7 +56,7 @@ export default class FileContentUserDataStorage (data) => data.dataType === dataType && data.subContentId === subContentId && - data.userId === user.id && + data.userId === userId && data.contextId === contextId ) || null ); @@ -260,7 +260,7 @@ export default class FileContentUserDataStorage public async getContentUserDataByContentIdAndUser( contentId: ContentId, - user: IUser, + userId: string, contextId?: string ): Promise { const file = this.getUserDataFilePath(contentId); @@ -280,7 +280,7 @@ export default class FileContentUserDataStorage try { return dataList.filter( - (data) => data.userId === user.id && data.contextId == contextId + (data) => data.userId === userId && data.contextId == contextId ); } catch (error) { log.error( diff --git a/packages/h5p-server/src/index.ts b/packages/h5p-server/src/index.ts index d04947eb2..df5cc0fde 100644 --- a/packages/h5p-server/src/index.ts +++ b/packages/h5p-server/src/index.ts @@ -31,11 +31,14 @@ import LibraryManager from './LibraryManager'; import ContentUserDataManager from './ContentUserDataManager'; import UrlGenerator from './UrlGenerator'; import SimpleLockProvider from './implementation/SimpleLockProvider'; +import { LaissezFairePermissionSystem } from './implementation/LaissezFairePermissionSystem'; // Interfaces import { ContentId, ContentParameters, + ContentPermission, + GeneralPermission, IAdditionalLibraryMetadata, IAjaxResponse, IContentMetadata, @@ -58,6 +61,7 @@ import { ILibraryStorage, ILicenseData, ILockProvider, + IPermissionSystem, IPlayerModel, IPostContentUserData, IPostUserFinishedData, @@ -66,7 +70,8 @@ import { ITranslationFunction, IUrlGenerator, IUser, - Permission + TemporaryFilePermission, + UserDataPermission } from './types'; // Adapters @@ -108,6 +113,8 @@ export { // interfaces ContentId, ContentParameters, + ContentPermission, + GeneralPermission, IAdditionalLibraryMetadata, IAjaxResponse, IContentMetadata, @@ -130,6 +137,7 @@ export { ILibraryStorage, ILicenseData, ILockProvider, + IPermissionSystem, IPlayerModel, IPostContentUserData, IPostUserFinishedData, @@ -138,9 +146,11 @@ export { ITranslationFunction, IUrlGenerator, IUser, - Permission, + TemporaryFilePermission, + UserDataPermission, // implementations H5PConfig, + LaissezFairePermissionSystem, fs, utils, fsImplementations, diff --git a/packages/h5p-server/src/types.ts b/packages/h5p-server/src/types.ts index 0a0625e0c..b402f9dfd 100644 --- a/packages/h5p-server/src/types.ts +++ b/packages/h5p-server/src/types.ts @@ -9,9 +9,10 @@ import { Readable, Stream } from 'stream'; export type ContentId = string; /** - * Permissions give rights to users to do certain actions with a piece of content. + * Give rights to users to perform certain actions with a piece of content. */ -export enum Permission { +export enum ContentPermission { + Create, Delete, Download, Edit, @@ -20,6 +21,52 @@ export enum Permission { View } +/** + * Give rights to users to perform certain actions with a user data. + */ +export enum UserDataPermission { + EditState, + DeleteState, + ViewState, + ListStates, + EditFinished, + ViewFinished, + DeleteFinished +} + +/** + * Give rights to users to perform certain actions with temporary files. + */ +export enum TemporaryFilePermission { + Create, + Delete, + List, + View +} + +/** + * Give rights to users to perform certain actions that are not associated with + * existing objects. + */ +export enum GeneralPermission { + /** + * If given, the user can create content of content types that are set to + * "restricted". + */ + CreateRestricted, + /** + * If given, the user can install content types from the hub that have the + * "recommended" flag in the Hub. + */ + InstallRecommended, + /** + * If given, the user can generally install and update libraries. This + * includes Hub content types that aren't set to "recommended" or uploading + * custom packages. + */ + UpdateAndInstallLibraries +} + /** * A response that is sent back to an AJAX call. */ @@ -512,20 +559,6 @@ export interface ILibraryOverviewForClient { * implementations. */ export interface IUser { - /** - * If true, the user can create content of content types that are set to "restricted". - */ - canCreateRestricted: boolean; - /** - * If true, the user can install content types from the hub that are set the "recommended" - * by the Hub. - */ - canInstallRecommended: boolean; - /** - * If true, the user can generally install and update libraries. This includes Hub - * content types that aren't set to "recommended" or uploading custom packages. - */ - canUpdateAndInstallLibraries: boolean; /** * E-Mail address. */ @@ -671,7 +704,7 @@ export interface IContentUserDataStorage { contentId: ContentId, dataType: string, subContentId: string, - user: IUser, + userId: string, contextId?: string ): Promise; @@ -686,7 +719,7 @@ export interface IContentUserDataStorage { */ getContentUserDataByContentIdAndUser( contentId: ContentId, - user: IUser, + userId: string, contextId?: string ): Promise; @@ -872,18 +905,6 @@ export interface IContentStorage { library: ILibraryName ): Promise<{ asDependency: number; asMainLibrary: number }>; - /** - * Returns an array of permissions that the user has on the piece of content - * @param contentId the content id to check - * @param user the user who wants to access the piece of content - * @returns the permissions the user has for this content (e.g. download it, - * delete it etc.) - */ - getUserPermissions( - contentId: ContentId, - user: IUser - ): Promise; - /** * Lists the content objects in the system (if no user is specified) or * owned by the user. @@ -1106,6 +1127,68 @@ export interface ILibraryStorage { ): Promise; } +/** + * Manages permissions of users. + */ +export interface IPermissionSystem { + /** + * Checks if a user has a certain permission on a content object + * @param actingUser the user who is currently active + * @param permission the permission to check + * @param contentId the content for which to check; if the permission if + * `ContentPermission.List` or `ContentPermission.Create` the id will be + * undefined + * @returns true if the user is allowed to do it + */ + checkForContent( + actingUser: TUser, + permission: ContentPermission, + contentId: ContentId | undefined + ): Promise; + + /** + * Checks if a user has a certain permission on a user data object. + * @param actingUser the user who is currently active + * @param permission the permission to check + * @param contentId the content id to which the user data belongs + * @param affectedUserId (optional) if the acting user tries to access user + * data that is not their own, the affected user will be specified here + * @returns true if the user is allowed to do it + */ + checkForUserData( + actingUser: TUser, + permission: UserDataPermission, + contentId: ContentId, + affectedUserId?: string + ): Promise; + + /** + * Checks if a user has a certain permission on a temporary file + * @param actingUser the currently active user + * @param permission the permission to check + * @param filename the file the user is trying to access; can be undefined + * if the the check is for TemporaryFilePermission.Create + * @returns true if the user is allowed to do it + */ + checkForTemporaryFile( + actingUser: TUser, + permission: TemporaryFilePermission, + filename: string | undefined + ): Promise; + + /** + * Checks if a user has a certain permission that is not associated with any + * object, but part of their general role. + * @param actingUser the currently active user + * @param permission the permission to check + * @return true if the user is allowed to do it + */ + checkForGeneralAction( + actingUser: TUser, + permission: GeneralPermission + ): Promise; +} + /** * This is the actual "content itself", meaning the object contained in * content.json. It is created by the JavaScript editor client and played out by @@ -1849,11 +1932,13 @@ export interface ITemporaryFile { export interface ITemporaryFileStorage { /** * Deletes the file from temporary storage (e.g. because it has expired) - * @param filename the filename; can be a path including subdirectories (e.g. 'images/xyz.png') - * @param userId the user id + * @param filename the filename; can be a path including subdirectories + * (e.g. 'images/xyz.png') + * @param ownerId (optional) when there is no user deleting, you must specify who the + * owner of the temporary file is; only needed when userId is null * @returns true if deletion was successful */ - deleteFile(filename: string, userId: string): Promise; + deleteFile(filename: string, ownerId: string): Promise; /** * Checks if a file exists in temporary storage. @@ -2025,7 +2110,12 @@ export interface IUrlGenerator { * @param contextId allows implementation to have multiple user data objects * for one h5p content object */ - contentUserData(user: IUser, contextId?: string): string; + contentUserData( + user: IUser, + contextId?: string, + asUserId?: string, + options?: { readonly?: boolean } + ): string; coreFile(file: string): string; coreFiles(): string; downloadPackage(contentId: ContentId): string; @@ -2210,6 +2300,8 @@ export interface IH5PEditorOptions { * override. */ getLocalIdOverride?: () => string; + + permissionSystem?: IPermissionSystem; } /** @@ -2252,11 +2344,7 @@ export interface IH5PPlayerOptions { * is used in a multi-process or cluster environment. */ lockProvider?: ILockProvider; - - getPermissions?: ( - contentId: ContentId, - user: IUser - ) => Promise; + permissionSystem?: IPermissionSystem; } /** diff --git a/packages/h5p-server/test/ContentFileScanner.test.ts b/packages/h5p-server/test/ContentFileScanner.test.ts index eef233dee..8bc219943 100644 --- a/packages/h5p-server/test/ContentFileScanner.test.ts +++ b/packages/h5p-server/test/ContentFileScanner.test.ts @@ -12,6 +12,7 @@ import LibraryManager from '../src/LibraryManager'; import PackageImporter from '../src/PackageImporter'; import { ContentId, IUser } from '../src/types'; import ContentStorer from '../src/ContentStorer'; +import { LaissezFairePermissionSystem } from '../src/implementation/LaissezFairePermissionSystem'; import User from './User'; import { getContentDetails } from './ContentScanner.test'; @@ -33,7 +34,8 @@ describe('ContentFileScanner', () => { await fsExtra.ensureDir(libraryDir); const contentManager = new ContentManager( - new FileContentStorage(contentDir) + new FileContentStorage(contentDir), + new LaissezFairePermissionSystem() ); const libraryManager = new LibraryManager( new FileLibraryStorage(libraryDir) @@ -43,6 +45,7 @@ describe('ContentFileScanner', () => { const packageImporter = new PackageImporter( libraryManager, new H5PConfig(null), + new LaissezFairePermissionSystem(), contentManager, new ContentStorer(contentManager, libraryManager, undefined) ); @@ -62,7 +65,6 @@ describe('ContentFileScanner', () => { await withDir( async ({ path: tmpDirPath }) => { const user = new User(); - user.canUpdateAndInstallLibraries = true; const { contentScanner, contentId, contentManager } = await createContentFileScanner( @@ -105,7 +107,6 @@ describe('ContentFileScanner', () => { await withDir( async ({ path: tmpDirPath }) => { const user = new User(); - user.canUpdateAndInstallLibraries = true; const { contentScanner, contentId, contentManager } = await createContentFileScanner( diff --git a/packages/h5p-server/test/ContentManager.test.ts b/packages/h5p-server/test/ContentManager.test.ts index 1c42908e3..b3f049ca5 100644 --- a/packages/h5p-server/test/ContentManager.test.ts +++ b/packages/h5p-server/test/ContentManager.test.ts @@ -6,6 +6,7 @@ import { withDir } from 'tmp-promise'; import ContentManager from '../src/ContentManager'; import FileContentStorage from '../src/implementation/fs/FileContentStorage'; +import { LaissezFairePermissionSystem } from '../src/index'; import { IContentMetadata } from '../src/types'; import User from './User'; @@ -43,7 +44,8 @@ describe('ContentManager', () => { await withDir( async ({ path: tempDirPath }) => { const contentManager = new ContentManager( - new FileContentStorage(tempDirPath) + new FileContentStorage(tempDirPath), + new LaissezFairePermissionSystem() ); const contentId = await contentManager.createOrUpdateContent( @@ -61,7 +63,8 @@ describe('ContentManager', () => { await withDir( async ({ path: tempDirPath }) => { const contentManager = new ContentManager( - new FileContentStorage(tempDirPath) + new FileContentStorage(tempDirPath), + new LaissezFairePermissionSystem() ); const contentId = await contentManager.createOrUpdateContent( @@ -80,7 +83,8 @@ describe('ContentManager', () => { await withDir( async ({ path: tempDirPath }) => { const contentManager = new ContentManager( - new FileContentStorage(tempDirPath) + new FileContentStorage(tempDirPath), + new LaissezFairePermissionSystem() ); const user = new User(); @@ -110,7 +114,8 @@ describe('ContentManager', () => { await withDir( async ({ path: tempDirPath }) => { const contentManager = new ContentManager( - new FileContentStorage(tempDirPath) + new FileContentStorage(tempDirPath), + new LaissezFairePermissionSystem() ); const user = new User(); @@ -137,6 +142,7 @@ describe('ContentManager', () => { async ({ path: tempDirPath }) => { const contentManager = new ContentManager( new FileContentStorage(tempDirPath), + new LaissezFairePermissionSystem(), mockContentUserDataStorage ); @@ -162,7 +168,8 @@ describe('ContentManager', () => { await withDir( async ({ path: tempDirPath }) => { const contentManager = new ContentManager( - new FileContentStorage(tempDirPath) + new FileContentStorage(tempDirPath), + new LaissezFairePermissionSystem() ); const user = new User(); @@ -235,7 +242,8 @@ describe('ContentManager', () => { await withDir( async ({ path: tempDirPath }) => { const contentManager = new ContentManager( - new FileContentStorage(tempDirPath) + new FileContentStorage(tempDirPath), + new LaissezFairePermissionSystem() ); const user1 = new User(); diff --git a/packages/h5p-server/test/ContentScanner.test.ts b/packages/h5p-server/test/ContentScanner.test.ts index d9f1d9b59..1035c0933 100644 --- a/packages/h5p-server/test/ContentScanner.test.ts +++ b/packages/h5p-server/test/ContentScanner.test.ts @@ -13,6 +13,7 @@ import { ContentId, ILibraryName, IUser } from '../src/types'; import ContentStorer from '../src/ContentStorer'; import User from './User'; +import { LaissezFairePermissionSystem } from '../src/implementation/LaissezFairePermissionSystem'; async function createContentScanner( file: string, @@ -30,7 +31,8 @@ async function createContentScanner( await fsExtra.ensureDir(libraryDir); const contentManager = new ContentManager( - new FileContentStorage(contentDir) + new FileContentStorage(contentDir), + new LaissezFairePermissionSystem() ); const libraryManager = new LibraryManager( new FileLibraryStorage(libraryDir) @@ -40,6 +42,7 @@ async function createContentScanner( const packageImporter = new PackageImporter( libraryManager, new H5PConfig(null), + new LaissezFairePermissionSystem(), contentManager, new ContentStorer(contentManager, libraryManager, undefined) ); @@ -82,7 +85,6 @@ describe('ContentScanner', () => { await withDir( async ({ path: tmpDirPath }) => { const user = new User(); - user.canUpdateAndInstallLibraries = true; // initialize content manager const { contentScanner, contentManager, contentId } = @@ -117,7 +119,6 @@ describe('ContentScanner', () => { await withDir( async ({ path: tmpDirPath }) => { const user = new User(); - user.canUpdateAndInstallLibraries = true; const { contentScanner, contentId, contentManager } = await createContentScanner( diff --git a/packages/h5p-server/test/ContentTypeInformationRepository.test.ts b/packages/h5p-server/test/ContentTypeInformationRepository.test.ts index 7587e6394..715667f3a 100644 --- a/packages/h5p-server/test/ContentTypeInformationRepository.test.ts +++ b/packages/h5p-server/test/ContentTypeInformationRepository.test.ts @@ -10,6 +10,8 @@ import H5PConfig from '../src/implementation/H5PConfig'; import FileLibraryStorage from '../src/implementation/fs/FileLibraryStorage'; import InMemoryStorage from '../src/implementation/InMemoryStorage'; import LibraryManager from '../src/LibraryManager'; +import { LaissezFairePermissionSystem } from '../src/implementation/LaissezFairePermissionSystem'; +import { IUser, GeneralPermission } from '../src/types'; import User from './User'; @@ -50,7 +52,8 @@ describe('Content type information repository (= connection to H5P Hub)', () => const repository = new ContentTypeInformationRepository( cache, libManager, - config + config, + new LaissezFairePermissionSystem() ); const content = await repository.get(new User()); expect(content.outdated).toBe(false); @@ -101,6 +104,7 @@ describe('Content type information repository (= connection to H5P Hub)', () => cache, libManager, config, + new LaissezFairePermissionSystem(), translationSpy ); const content = await repository.get(new User(), 'de'); @@ -148,6 +152,7 @@ describe('Content type information repository (= connection to H5P Hub)', () => cache, libManager, config, + new LaissezFairePermissionSystem(), translationSpy ); const content = await repository.get(new User(), 'de'); @@ -189,7 +194,8 @@ describe('Content type information repository (= connection to H5P Hub)', () => const repository = new ContentTypeInformationRepository( cache, libManager, - config + config, + new LaissezFairePermissionSystem() ); const content = await repository.get(new User()); expect(content.outdated).toBe(false); @@ -234,7 +240,8 @@ describe('Content type information repository (= connection to H5P Hub)', () => const repository = new ContentTypeInformationRepository( cache, libManager, - config + config, + new LaissezFairePermissionSystem() ); const content = await repository.get(new User()); expect(content.libraries.length).toEqual(2); @@ -272,7 +279,8 @@ describe('Content type information repository (= connection to H5P Hub)', () => const repository = new ContentTypeInformationRepository( cache, libManager, - config + config, + new LaissezFairePermissionSystem() ); const content = await repository.get(new User()); expect(content.libraries.length).toEqual(2); @@ -303,7 +311,8 @@ describe('Content type information repository (= connection to H5P Hub)', () => const repository = new ContentTypeInformationRepository( cache, libManager, - config + config, + new LaissezFairePermissionSystem() ); const content = await repository.get(new User()); expect(content.libraries.length).toEqual(2); @@ -320,7 +329,6 @@ describe('Content type information repository (= connection to H5P Hub)', () => config.enableLrsContentTypes = false; config.lrsContentTypes = ['H5P.Example1']; - user.canCreateRestricted = false; axiosMock .onPost(config.hubRegistrationEndpoint) @@ -337,7 +345,15 @@ describe('Content type information repository (= connection to H5P Hub)', () => const repository = new ContentTypeInformationRepository( cache, libManager, - config + config, + new (class extends LaissezFairePermissionSystem { + async checkForGeneralAction( + _actingUser: IUser, + permission: GeneralPermission + ): Promise { + return permission !== GeneralPermission.CreateRestricted; + } + })() ); const content = await repository.get(user); expect(content.libraries.length).toEqual(2); @@ -385,7 +401,8 @@ describe('Content type information repository (= connection to H5P Hub)', () => const repository = new ContentTypeInformationRepository( cache, libManager, - config + config, + new LaissezFairePermissionSystem() ); await expect( repository.installContentType(undefined, new User()) @@ -427,20 +444,44 @@ describe('Content type information repository (= connection to H5P Hub)', () => await cache.updateIfNecessary(); - const repository = new ContentTypeInformationRepository( + let repository = new ContentTypeInformationRepository( cache, libManager, - config + config, + new (class extends LaissezFairePermissionSystem { + async checkForGeneralAction( + _actingUser: IUser, + permission: GeneralPermission + ): Promise { + return !( + permission === GeneralPermission.InstallRecommended || + permission === + GeneralPermission.UpdateAndInstallLibraries + ); + } + })() ); - user.canInstallRecommended = false; - user.canUpdateAndInstallLibraries = false; await expect( repository.installContentType('H5P.Blanks', user) ).rejects.toThrow('hub-install-denied'); - user.canInstallRecommended = true; - user.canUpdateAndInstallLibraries = false; + repository = repository = new ContentTypeInformationRepository( + cache, + libManager, + config, + new (class extends LaissezFairePermissionSystem { + async checkForGeneralAction( + _actingUser: IUser, + permission: GeneralPermission + ): Promise { + return ( + permission !== + GeneralPermission.UpdateAndInstallLibraries + ); + } + })() + ); await expect( repository.installContentType('H5P.ImageHotspotQuestion', user) ).rejects.toThrow('hub-install-denied'); @@ -494,7 +535,8 @@ describe('Content type information repository (= connection to H5P Hub)', () => const repository = new ContentTypeInformationRepository( cache, libManager, - config + config, + new LaissezFairePermissionSystem() ); await expect( diff --git a/packages/h5p-server/test/ContentUserDataManager.test.ts b/packages/h5p-server/test/ContentUserDataManager.test.ts index 6d5659e56..fed1c7ef6 100644 --- a/packages/h5p-server/test/ContentUserDataManager.test.ts +++ b/packages/h5p-server/test/ContentUserDataManager.test.ts @@ -1,4 +1,5 @@ import ContentUserDataManager from '../src/ContentUserDataManager'; +import { LaissezFairePermissionSystem } from '../src/implementation/LaissezFairePermissionSystem'; import MockContentUserDataStorage from './__mocks__/ContentUserDataStorage'; @@ -8,12 +9,16 @@ describe('ContentUserDataManager', () => { describe('deleteAllContentUserDataByUser', () => { it('returns undefined if contentUserDataStorage is undefined', async () => { const contentUserDataManager = new ContentUserDataManager( - undefined + undefined, + new LaissezFairePermissionSystem() ); + const user = new User(); + expect( await contentUserDataManager.deleteAllContentUserDataByUser( - new User() + user.id, + user ) ).toBeUndefined(); }); @@ -22,11 +27,15 @@ describe('ContentUserDataManager', () => { const mockContentUserDataStorage = MockContentUserDataStorage(); const contentUserDataManager = new ContentUserDataManager( - mockContentUserDataStorage + mockContentUserDataStorage, + new LaissezFairePermissionSystem() ); const user = new User(); - await contentUserDataManager.deleteAllContentUserDataByUser(user); + await contentUserDataManager.deleteAllContentUserDataByUser( + user.id, + user + ); expect( mockContentUserDataStorage.deleteAllContentUserDataByUser @@ -37,7 +46,8 @@ describe('ContentUserDataManager', () => { describe('deleteInvalidatedContentUserData', () => { it('returns undefined if contentUserDataStorage is undefined', async () => { const contentUserDataManager = new ContentUserDataManager( - undefined + undefined, + new LaissezFairePermissionSystem() ); expect( @@ -51,7 +61,8 @@ describe('ContentUserDataManager', () => { const mockContentUserDataStorage = MockContentUserDataStorage(); const contentUserDataManager = new ContentUserDataManager( - mockContentUserDataStorage + mockContentUserDataStorage, + new LaissezFairePermissionSystem() ); const contentId = 'contentId'; @@ -68,7 +79,8 @@ describe('ContentUserDataManager', () => { describe('setFinished', () => { it('returns undefined if contentUserDataStorage is undefined', async () => { const contentUserDataManager = new ContentUserDataManager( - undefined + undefined, + new LaissezFairePermissionSystem() ); const contentId = 'contentId'; @@ -95,7 +107,8 @@ describe('ContentUserDataManager', () => { const mockContentUserDataStorage = MockContentUserDataStorage(); const contentUserDataManager = new ContentUserDataManager( - mockContentUserDataStorage + mockContentUserDataStorage, + new LaissezFairePermissionSystem() ); const contentId = 'contentId'; @@ -132,12 +145,14 @@ describe('ContentUserDataManager', () => { describe('deleteAllContentUserDataByContentId', () => { it('returns undefined if contentUserDataStorage is undefined', async () => { const contentUserDataManager = new ContentUserDataManager( - undefined + undefined, + new LaissezFairePermissionSystem() ); expect( await contentUserDataManager.deleteAllContentUserDataByContentId( - 'contentId' + 'contentId', + new User() ) ).toBeUndefined(); }); @@ -146,12 +161,14 @@ describe('ContentUserDataManager', () => { const mockContentUserDataStorage = MockContentUserDataStorage(); const contentUserDataManager = new ContentUserDataManager( - mockContentUserDataStorage + mockContentUserDataStorage, + new LaissezFairePermissionSystem() ); const contentId = 'contentId'; await contentUserDataManager.deleteAllContentUserDataByContentId( - contentId + contentId, + new User() ); expect( @@ -163,7 +180,8 @@ describe('ContentUserDataManager', () => { describe('getContentUserData', () => { it('returns undefined if contentUserDataStorage is undefined', async () => { const contentUserDataManager = new ContentUserDataManager( - undefined + undefined, + new LaissezFairePermissionSystem() ); expect( @@ -180,7 +198,8 @@ describe('ContentUserDataManager', () => { const mockContentUserDataStorage = MockContentUserDataStorage(); const contentUserDataManager = new ContentUserDataManager( - mockContentUserDataStorage + mockContentUserDataStorage, + new LaissezFairePermissionSystem() ); const contentId = 'contentId'; const dataType = 'string'; @@ -200,7 +219,7 @@ describe('ContentUserDataManager', () => { contentId, dataType, subContentId, - user, + user.id, undefined ); }); @@ -209,7 +228,8 @@ describe('ContentUserDataManager', () => { const mockContentUserDataStorage = MockContentUserDataStorage(); const contentUserDataManager = new ContentUserDataManager( - mockContentUserDataStorage + mockContentUserDataStorage, + new LaissezFairePermissionSystem() ); const contentId = 'contentId'; const dataType = 'string'; @@ -231,7 +251,7 @@ describe('ContentUserDataManager', () => { contentId, dataType, subContentId, - user, + user.id, contextId ); }); @@ -240,7 +260,8 @@ describe('ContentUserDataManager', () => { describe('createOrUpdateContentUserData', () => { it('returns undefined if contentUserDataStorage is undefined', async () => { const contentUserDataManager = new ContentUserDataManager( - undefined + undefined, + new LaissezFairePermissionSystem() ); expect( @@ -258,7 +279,8 @@ describe('ContentUserDataManager', () => { it('throws an error if invalid arguments are passed', async () => { const contentUserDataManager = new ContentUserDataManager( - undefined + undefined, + new LaissezFairePermissionSystem() ); await expect( @@ -282,7 +304,8 @@ describe('ContentUserDataManager', () => { const mockContentUserDataStorage = MockContentUserDataStorage(); const contentUserDataManager = new ContentUserDataManager( - mockContentUserDataStorage + mockContentUserDataStorage, + new LaissezFairePermissionSystem() ); const contentId = 'contentId'; const dataType = 'state'; @@ -318,7 +341,8 @@ describe('ContentUserDataManager', () => { const mockContentUserDataStorage = MockContentUserDataStorage(); const contentUserDataManager = new ContentUserDataManager( - mockContentUserDataStorage + mockContentUserDataStorage, + new LaissezFairePermissionSystem() ); const contentId = 'contentId'; const dataType = 'state'; @@ -356,7 +380,8 @@ describe('ContentUserDataManager', () => { describe('generateContentUserDataIntegration', () => { it('returns undefined if contentUserDataStorage is undefined', async () => { const contentUserDataManager = new ContentUserDataManager( - undefined + undefined, + new LaissezFairePermissionSystem() ); expect( @@ -371,7 +396,8 @@ describe('ContentUserDataManager', () => { const mockContentUserDataStorage = MockContentUserDataStorage(); const contentUserDataManager = new ContentUserDataManager( - mockContentUserDataStorage + mockContentUserDataStorage, + new LaissezFairePermissionSystem() ); const contentId = 'contentId'; const user = new User(); @@ -383,14 +409,15 @@ describe('ContentUserDataManager', () => { expect( mockContentUserDataStorage.getContentUserDataByContentIdAndUser - ).toHaveBeenCalledWith(contentId, user, undefined); + ).toHaveBeenCalledWith(contentId, user.id, undefined); }); it('calls the getContentUserDataByContentIdAndUser method of the contentUserDateStorage with the correct arguments using contextId', async () => { const mockContentUserDataStorage = MockContentUserDataStorage(); const contentUserDataManager = new ContentUserDataManager( - mockContentUserDataStorage + mockContentUserDataStorage, + new LaissezFairePermissionSystem() ); const contentId = 'contentId'; const user = new User(); @@ -404,14 +431,15 @@ describe('ContentUserDataManager', () => { expect( mockContentUserDataStorage.getContentUserDataByContentIdAndUser - ).toHaveBeenCalledWith(contentId, user, contextId); + ).toHaveBeenCalledWith(contentId, user.id, contextId); }); it('generates the contentUserDataIntegration as an array in the correct order', async () => { const mockContentUserDataStorage = MockContentUserDataStorage(); const contentUserDataManager = new ContentUserDataManager( - mockContentUserDataStorage + mockContentUserDataStorage, + new LaissezFairePermissionSystem() ); const contentId = 'contentId'; const user = new User(); @@ -457,7 +485,8 @@ describe('ContentUserDataManager', () => { const mockContentUserDataStorage = MockContentUserDataStorage(); const contentUserDataManager = new ContentUserDataManager( - mockContentUserDataStorage + mockContentUserDataStorage, + new LaissezFairePermissionSystem() ); const contentId = 'contentId'; const user = new User(); diff --git a/packages/h5p-server/test/H5PPlayer.render.test.ts b/packages/h5p-server/test/H5PPlayer.render.test.ts index e8916bea4..26567f01b 100644 --- a/packages/h5p-server/test/H5PPlayer.render.test.ts +++ b/packages/h5p-server/test/H5PPlayer.render.test.ts @@ -1,9 +1,10 @@ import UrlGenerator from '../src/UrlGenerator'; import H5PPlayer from '../src/H5PPlayer'; import H5PConfig from '../src/implementation/H5PConfig'; -import User from './User'; +import { LaissezFairePermissionSystem } from '../src'; import { IPlayerModel } from '../src/types'; +import User from './User'; import MockContentUserDataStorage from './__mocks__/ContentUserDataStorage'; describe('H5P.render()', () => { @@ -182,7 +183,7 @@ describe('H5P.render()', () => { ); }); - it('adds the contextId to the contentUserData POST URl with CSRF token if contextId is used', async () => { + it('adds the contextId to the contentUserData POST URL with CSRF token if contextId is used', async () => { const contentId = 'foo'; const contentObject = {}; const metadata: any = {}; @@ -226,7 +227,7 @@ describe('H5P.render()', () => { ); expect(playerModel.integration.ajax.contentUserData).toEqual( - '/h5p/contentUserData/:contentId/:dataType/:subContentId?_csrf=token&contextId=123' + '/h5p/contentUserData/:contentId/:dataType/:subContentId?contextId=123&_csrf=token' ); }); @@ -297,4 +298,176 @@ describe('H5P.render()', () => { playerModel.integration.contents[`cid-${contentId}`].contentUserData ).toBeUndefined(); }); + + it('returns your own content users data', async () => { + const contentId = 'foo'; + const contentObject = {}; + const metadata: any = {}; + + const config = new H5PConfig(undefined); + const user = new User(); + + const mockContentUserDataStorage = MockContentUserDataStorage( + contentId, + user.id + ); + mockContentUserDataStorage.setMockData([ + { + contentId, + userId: user.id, + dataType: 'state', + subContentId: '0', + userState: `{"mock": "data"}`, + preload: true + } + ]); + + const player = new H5PPlayer( + undefined, + undefined, + config, + undefined, + undefined, + undefined, + {}, + mockContentUserDataStorage + ); + player.setRenderer((model) => model); + const playerModel: IPlayerModel = await player.render( + contentId, + user, + 'en', + { + parametersOverride: contentObject, + metadataOverride: metadata as any + } + ); + + expect( + playerModel.integration.contents['cid-foo'].contentUserData + ).toMatchObject([ + { + state: `{"mock": "data"}` + } + ]); + }); + + it('returns others content users data', async () => { + const contentId = 'foo'; + const contentObject = {}; + const metadata: any = {}; + + const config = new H5PConfig(undefined); + const user = new User(); + + const mockContentUserDataStorage = MockContentUserDataStorage( + contentId, + user.id + ); + mockContentUserDataStorage.getContentUserDataByContentIdAndUser = + async (cid: string, userId: string) => { + if (userId == '2') { + return [ + { + contentId, + userId: userId, + dataType: 'state', + subContentId: '0', + userState: `user2-state`, + preload: true + } + ]; + } + if (userId == '1') { + return []; + } + }; + + const player = new H5PPlayer( + undefined, + undefined, + config, + undefined, + undefined, + undefined, + {}, + mockContentUserDataStorage + ); + player.setRenderer((model) => model); + const playerModel: IPlayerModel = await player.render( + contentId, + user, + 'en', + { + parametersOverride: contentObject, + metadataOverride: metadata as any, + asUserId: '2' + } + ); + + expect( + playerModel.integration.contents['cid-foo'].contentUserData + ).toMatchObject([ + { + state: `user2-state` + } + ]); + }); + + it("rejects viewing others content users data if you don't have the permission", async () => { + const contentId = 'foo'; + const contentObject = {}; + const metadata: any = {}; + + const config = new H5PConfig(undefined); + const user = new User(); + + const mockContentUserDataStorage = MockContentUserDataStorage( + contentId, + user.id + ); + + const permissionSystem = new LaissezFairePermissionSystem(); + permissionSystem.checkForUserData = async ( + actingUser, + _permission, + _cid, + affectedUserId + ) => { + if (actingUser.id !== affectedUserId) { + return false; + } + return true; + }; + + const player = new H5PPlayer( + undefined, + undefined, + config, + undefined, + undefined, + undefined, + { + permissionSystem + }, + mockContentUserDataStorage + ); + player.setRenderer((model) => model); + + await expect( + player.render(contentId, user, 'en', { + parametersOverride: contentObject, + metadataOverride: metadata as any, + asUserId: user.id + }) + ).resolves.toBeDefined(); + + await expect( + player.render(contentId, user, 'en', { + parametersOverride: contentObject, + metadataOverride: metadata as any, + asUserId: '2' + }) + ).rejects.toThrowError('h5p-server:user-state-missing-view-permission'); + }); }); diff --git a/packages/h5p-server/test/PackageExporter.test.ts b/packages/h5p-server/test/PackageExporter.test.ts index 988f670ba..0ba3c0656 100644 --- a/packages/h5p-server/test/PackageExporter.test.ts +++ b/packages/h5p-server/test/PackageExporter.test.ts @@ -13,6 +13,7 @@ import PackageImporter from '../src/PackageImporter'; import ContentStorer from '../src/ContentStorer'; import User from './User'; +import { LaissezFairePermissionSystem } from '../src/implementation/LaissezFairePermissionSystem'; export function importAndExportPackage( packagePath: string, @@ -26,10 +27,12 @@ export function importAndExportPackage( await fsExtra.ensureDir(libraryDir); const user = new User(); - user.canUpdateAndInstallLibraries = true; const contentStorage = new FileContentStorage(contentDir); - const contentManager = new ContentManager(contentStorage); + const contentManager = new ContentManager( + contentStorage, + new LaissezFairePermissionSystem() + ); const libraryStorage = new FileLibraryStorage(libraryDir); const libraryManager = new LibraryManager(libraryStorage); const config = new H5PConfig(null); @@ -37,6 +40,7 @@ export function importAndExportPackage( const packageImporter = new PackageImporter( libraryManager, config, + new LaissezFairePermissionSystem(), contentManager, new ContentStorer(contentManager, libraryManager, undefined) ); diff --git a/packages/h5p-server/test/PackageImporter.test.ts b/packages/h5p-server/test/PackageImporter.test.ts index a6aa7d156..153a6836f 100644 --- a/packages/h5p-server/test/PackageImporter.test.ts +++ b/packages/h5p-server/test/PackageImporter.test.ts @@ -11,6 +11,8 @@ import FileLibraryStorage from '../src/implementation/fs/FileLibraryStorage'; import LibraryManager from '../src/LibraryManager'; import PackageImporter from '../src/PackageImporter'; import ContentStorer from '../src/ContentStorer'; +import { LaissezFairePermissionSystem } from '../src/implementation/LaissezFairePermissionSystem'; +import { IUser, ContentPermission, GeneralPermission } from '../src/types'; import User from './User'; @@ -26,7 +28,8 @@ describe('package importer', () => { ); const packageImporter = new PackageImporter( libraryManager, - new H5PConfig(null) + new H5PConfig(null), + new LaissezFairePermissionSystem() ); const installedLibraryNames = await packageImporter.installLibrariesFromPackage( @@ -70,10 +73,10 @@ describe('package importer', () => { await fsExtra.ensureDir(libraryDir); const user = new User(); - user.canUpdateAndInstallLibraries = true; const contentManager = new ContentManager( - new FileContentStorage(contentDir) + new FileContentStorage(contentDir), + new LaissezFairePermissionSystem() ); const libraryManager = new LibraryManager( new FileLibraryStorage(libraryDir) @@ -81,6 +84,7 @@ describe('package importer', () => { const packageImporter = new PackageImporter( libraryManager, new H5PConfig(null), + new LaissezFairePermissionSystem(), contentManager, new ContentStorer(contentManager, libraryManager, undefined) ); @@ -142,10 +146,23 @@ describe('package importer', () => { await fsExtra.ensureDir(libraryDir); const user = new User(); - user.canUpdateAndInstallLibraries = false; + + const permissionSystem = + new (class extends LaissezFairePermissionSystem { + async checkForGeneralAction( + _actingUser: IUser, + permission: GeneralPermission + ): Promise { + return ( + permission !== + GeneralPermission.UpdateAndInstallLibraries + ); + } + })(); const contentManager = new ContentManager( - new FileContentStorage(contentDir) + new FileContentStorage(contentDir), + permissionSystem ); const libraryManager = new LibraryManager( new FileLibraryStorage(libraryDir) @@ -153,6 +170,7 @@ describe('package importer', () => { const packageImporter = new PackageImporter( libraryManager, new H5PConfig(null), + permissionSystem, contentManager, new ContentStorer(contentManager, libraryManager, undefined) ); diff --git a/packages/h5p-server/test/TemporaryFileManager.test.ts b/packages/h5p-server/test/TemporaryFileManager.test.ts index b453e5a8a..15c1fe31c 100644 --- a/packages/h5p-server/test/TemporaryFileManager.test.ts +++ b/packages/h5p-server/test/TemporaryFileManager.test.ts @@ -8,6 +8,7 @@ import { withDir } from 'tmp-promise'; import H5PConfig from '../src/implementation/H5PConfig'; import DirectoryTemporaryFileStorage from '../src/implementation/fs/DirectoryTemporaryFileStorage'; import TemporaryFileManager from '../src/TemporaryFileManager'; +import { LaissezFairePermissionSystem } from '../src'; import User from './User'; @@ -21,7 +22,8 @@ describe('TemporaryFileManager', () => { async ({ path: tempDirPath }) => { const tmpManager = new TemporaryFileManager( new DirectoryTemporaryFileStorage(tempDirPath), - config + config, + new LaissezFairePermissionSystem() ); const newFilename = await tmpManager.addFile( 'real-content-types.json', @@ -61,7 +63,8 @@ describe('TemporaryFileManager', () => { async ({ path: tempDirPath }) => { const tmpManager = new TemporaryFileManager( new DirectoryTemporaryFileStorage(tempDirPath), - config + config, + new LaissezFairePermissionSystem() ); const newFilename1 = await tmpManager.addFile( 'real-content-types.json', @@ -96,7 +99,8 @@ describe('TemporaryFileManager', () => { async ({ path: tempDirPath }) => { const tmpManager = new TemporaryFileManager( new DirectoryTemporaryFileStorage(tempDirPath), - config + config, + new LaissezFairePermissionSystem() ); // add files that will expire @@ -173,7 +177,8 @@ describe('TemporaryFileManager', () => { async ({ path: tempDirPath }) => { const tmpManager = new TemporaryFileManager( new DirectoryTemporaryFileStorage(tempDirPath), - config + config, + new LaissezFairePermissionSystem() ); const newFilename1 = await tmpManager.addFile( 'real-content-types.json', diff --git a/packages/h5p-server/test/User.ts b/packages/h5p-server/test/User.ts index 264f3c336..a417eea05 100644 --- a/packages/h5p-server/test/User.ts +++ b/packages/h5p-server/test/User.ts @@ -7,16 +7,10 @@ export default class User implements IUser { constructor() { this.id = '1'; this.name = 'Firstname Surname'; - this.canInstallRecommended = true; - this.canUpdateAndInstallLibraries = true; - this.canCreateRestricted = true; this.type = 'local'; this.email = 'test@example.com'; } - public canCreateRestricted: boolean; - public canInstallRecommended: boolean; - public canUpdateAndInstallLibraries: boolean; public email: string; public id: string; public name: string; diff --git a/packages/h5p-server/test/__mocks__/ContentUserDataStorage.ts b/packages/h5p-server/test/__mocks__/ContentUserDataStorage.ts index 2e79c48a0..5525e0c07 100644 --- a/packages/h5p-server/test/__mocks__/ContentUserDataStorage.ts +++ b/packages/h5p-server/test/__mocks__/ContentUserDataStorage.ts @@ -1,4 +1,4 @@ -import { ContentId, IUser } from '../../src/types'; +import { ContentId } from '../../src/types'; let mockData; const mock = jest.fn().mockImplementation(() => { @@ -26,15 +26,15 @@ const mock = jest.fn().mockImplementation(() => { setMockData: (m) => (mockData = m), getContentUserDataByContentIdAndUser: jest .fn() - .mockImplementation((contentId: ContentId, user: IUser) => { + .mockImplementation((contentId: ContentId, userId: string) => { return ( mockData || [ { contentId, - userId: user.id, + userId: userId, dataType: 'state', subContentId: '0', - userState: `${contentId}-${user.id}`, + userState: `${contentId}-${userId}`, preload: true } ] diff --git a/packages/h5p-server/test/implementation/ContentUserDataStorage.ts b/packages/h5p-server/test/implementation/ContentUserDataStorage.ts index 332e0679b..21add3607 100644 --- a/packages/h5p-server/test/implementation/ContentUserDataStorage.ts +++ b/packages/h5p-server/test/implementation/ContentUserDataStorage.ts @@ -21,11 +21,11 @@ export default (getStorage: () => IContentUserDataStorage): void => { storage.createOrUpdateContentUserData(dataTemplate) ).resolves.not.toThrow(); await expect( - storage.getContentUserData('1', 'dataType', '0', user) + storage.getContentUserData('1', 'dataType', '0', user.id) ).resolves.toMatchObject(dataTemplate); const res = await storage.getContentUserDataByContentIdAndUser( '1', - user + user.id ); expect(res.length).toEqual(1); }); @@ -43,11 +43,11 @@ export default (getStorage: () => IContentUserDataStorage): void => { ).resolves.not.toThrow(); await expect( - storage.getContentUserData('1', 'dataType', '0', user) + storage.getContentUserData('1', 'dataType', '0', user.id) ).resolves.toMatchObject(dataTemplate); await expect( - storage.getContentUserData('1', 'dataType', '0', user, '123') + storage.getContentUserData('1', 'dataType', '0', user.id, '123') ).resolves.toMatchObject({ ...dataTemplate, contextId: '123' @@ -61,7 +61,7 @@ export default (getStorage: () => IContentUserDataStorage): void => { ).resolves.not.toThrow(); await expect( - storage.getContentUserData('1', 'dataType', '0', user, '123') + storage.getContentUserData('1', 'dataType', '0', user.id, '123') ).resolves.toEqual(null); }); @@ -84,26 +84,20 @@ export default (getStorage: () => IContentUserDataStorage): void => { ).resolves.not.toThrow(); await expect( - storage.getContentUserData('1', 'dataType', '0', user) + storage.getContentUserData('1', 'dataType', '0', user.id) ).resolves.toMatchObject(dataTemplate); await expect( - storage.getContentUserData('1', 'dataType', '0', { - ...user, - id: '2' - }) + storage.getContentUserData('1', 'dataType', '0', '2') ).resolves.toMatchObject({ ...dataTemplate, userId: '2' }); await expect( - storage.getContentUserData('1', 'dataType', '0', { - ...user, - id: '3' - }) + storage.getContentUserData('1', 'dataType', '0', '3') ).resolves.toMatchObject({ ...dataTemplate, userId: '3' }); }); it("returns null if user data doesn't exist", async () => { const storage = getStorage(); await expect( - storage.getContentUserData('1', 'dataType', '0', user) + storage.getContentUserData('1', 'dataType', '0', user.id) ).resolves.toEqual(null); }); @@ -111,7 +105,7 @@ export default (getStorage: () => IContentUserDataStorage): void => { const storage = getStorage(); const res = await storage.getContentUserDataByContentIdAndUser( '1', - user + user.id ); expect(res.length).toEqual(0); }); @@ -125,7 +119,7 @@ export default (getStorage: () => IContentUserDataStorage): void => { await storage.createOrUpdateContentUserData(data2); await expect( - storage.getContentUserData('1', 'dataType', '0', user) + storage.getContentUserData('1', 'dataType', '0', user.id) ).resolves.toMatchObject(data2); const allUserData = await storage.getContentUserDataByUser(user); @@ -145,7 +139,7 @@ export default (getStorage: () => IContentUserDataStorage): void => { await storage.createOrUpdateContentUserData(data2); await expect( - storage.getContentUserData('1', 'dataType', '0', user, '123') + storage.getContentUserData('1', 'dataType', '0', user.id, '123') ).resolves.toMatchObject({ ...dataTemplate, contextId: '123' }); }); @@ -166,7 +160,7 @@ export default (getStorage: () => IContentUserDataStorage): void => { await storage.createOrUpdateContentUserData(data2); await expect( - storage.getContentUserData('1', 'dataType', '0', user) + storage.getContentUserData('1', 'dataType', '0', user.id) ).resolves.toMatchObject(dataTemplate); }); @@ -231,20 +225,20 @@ export default (getStorage: () => IContentUserDataStorage): void => { const notFound1 = await storage.getContentUserDataByContentIdAndUser( dataTemplate.contentId, - user + user.id ); expect(notFound1.length).toEqual(0); const notFound2 = await storage.getContentUserDataByContentIdAndUser( dataTemplate.contentId, - { ...user, id: '2' } + '2' ); expect(notFound2.length).toEqual(0); const found = await storage.getContentUserDataByContentIdAndUser( dataTemplate.contentId, - { ...user, id: '3' } + '3' ); expect(found.length).toEqual(1); }); @@ -267,19 +261,22 @@ export default (getStorage: () => IContentUserDataStorage): void => { await storage.deleteAllContentUserDataByUser(user); const notFound1 = - await storage.getContentUserDataByContentIdAndUser('1', user); + await storage.getContentUserDataByContentIdAndUser( + '1', + user.id + ); expect(notFound1.length).toEqual(0); const notFound2 = - await storage.getContentUserDataByContentIdAndUser('2', user); + await storage.getContentUserDataByContentIdAndUser( + '2', + user.id + ); expect(notFound2.length).toEqual(0); const found = await storage.getContentUserDataByContentIdAndUser( '1', - { - ...user, - id: '2' - } + '2' ); expect(found.length).toEqual(1); }); diff --git a/packages/h5p-server/test/integration/ContentFileScanner.test.ts b/packages/h5p-server/test/integration/ContentFileScanner.test.ts index 501ebcc1b..709f68c4c 100644 --- a/packages/h5p-server/test/integration/ContentFileScanner.test.ts +++ b/packages/h5p-server/test/integration/ContentFileScanner.test.ts @@ -12,6 +12,7 @@ import LibraryManager from '../../src/LibraryManager'; import PackageImporter from '../../src/PackageImporter'; import { ContentId } from '../../src/types'; import ContentStorer from '../../src/ContentStorer'; +import { LaissezFairePermissionSystem } from '../../src/implementation/LaissezFairePermissionSystem'; import User from '../User'; @@ -39,7 +40,6 @@ describe('ContentFileScanner (integration test with H5P Hub examples)', () => { let contentScanner: ContentFileScanner = null; let packageIdMap: Map; const user = new User(); - user.canUpdateAndInstallLibraries = true; // We have to use beforeAll as describe(...) doesn't accept async functions beforeAll(async () => { @@ -52,7 +52,10 @@ describe('ContentFileScanner (integration test with H5P Hub examples)', () => { await fsExtra.ensureDir(contentDir); await fsExtra.ensureDir(libraryDir); - contentManager = new ContentManager(new FileContentStorage(contentDir)); + contentManager = new ContentManager( + new FileContentStorage(contentDir), + new LaissezFairePermissionSystem() + ); const libraryManager = new LibraryManager( new FileLibraryStorage(libraryDir) ); @@ -61,6 +64,7 @@ describe('ContentFileScanner (integration test with H5P Hub examples)', () => { const packageImporter = new PackageImporter( libraryManager, new H5PConfig(null), + new LaissezFairePermissionSystem(), contentManager, new ContentStorer(contentManager, libraryManager, undefined) ); diff --git a/packages/h5p-webcomponents/src/h5p-player.ts b/packages/h5p-webcomponents/src/h5p-player.ts index c465d9075..724f21552 100644 --- a/packages/h5p-webcomponents/src/h5p-player.ts +++ b/packages/h5p-webcomponents/src/h5p-player.ts @@ -34,6 +34,22 @@ export class H5PPlayerComponent extends HTMLElement { this.setAttribute('context-id', contextId); } + get asUserId(): string { + return this.getAttribute('as-user-id'); + } + + set asUserId(asUserId: string) { + this.setAttribute('as-user-id', asUserId); + } + + get readOnlyState(): string { + return this.getAttribute('read-only-state'); + } + + set readOnlyState(readOnlyState: string) { + this.setAttribute('read-only-state', readOnlyState); + } + /** * The internal H5P instance object of the H5P content. * @@ -87,7 +103,9 @@ export class H5PPlayerComponent extends HTMLElement { */ public get loadContentCallback(): ( contentId: string, - contextId?: string + contextId?: string, + asUserId?: string, + readOnlyState?: boolean ) => Promise { return this.privateLoadContentCallback; } @@ -95,13 +113,20 @@ export class H5PPlayerComponent extends HTMLElement { public set loadContentCallback( callback: ( contentId: string, - contextId?: string + contextId?: string, + asUserId?: string, + readOnlyState?: boolean ) => Promise ) { const mustRender = this.privateLoadContentCallback !== callback; this.privateLoadContentCallback = callback; if (mustRender) { - this.render(this.contentId, this.contextId); + this.render( + this.contentId, + this.contextId, + this.asUserId, + this.readOnlyState + ); } } @@ -111,7 +136,7 @@ export class H5PPlayerComponent extends HTMLElement { * @memberof H5PPlayerComponent */ static get observedAttributes(): string[] { - return ['content-id', 'context-id']; + return ['content-id', 'context-id', 'as-user-id', 'read-only-state']; } constructor() { super(); @@ -123,7 +148,9 @@ export class H5PPlayerComponent extends HTMLElement { private playerModel: IPlayerModel; private privateLoadContentCallback: ( contentId: string, - contextId?: string + contextId?: string, + asUserId?: string, + readOnlyState?: boolean ) => Promise; private resizeObserver: ResizeObserver; private root: HTMLElement; @@ -175,13 +202,42 @@ export class H5PPlayerComponent extends HTMLElement { if (oldVal) { removeUnusedContent(oldVal); } - await this.render(newVal, this.contextId); - } - if (name === 'context-id') { + await this.render( + newVal, + this.contextId, + this.asUserId, + this.readOnlyState + ); + } else if (name === 'context-id') { if (oldVal) { removeUnusedContent(this.contentId); } - await this.render(this.contentId, newVal); + await this.render( + this.contentId, + newVal, + this.asUserId, + this.readOnlyState + ); + } else if (name === 'as-user-id') { + if (oldVal) { + removeUnusedContent(this.contentId); + } + await this.render( + this.contentId, + this.contextId, + newVal, + this.readOnlyState + ); + } else if (name === 'read-only-state') { + if (oldVal) { + removeUnusedContent(this.contentId); + } + await this.render( + this.contentId, + this.contextId, + this.asUserId, + newVal + ); } } @@ -380,15 +436,23 @@ export class H5PPlayerComponent extends HTMLElement { * Displays content. * @param {string} contentId */ - private async render(contentId: string, contextId?: string): Promise { + private async render( + contentId: string, + contextId?: string, + asUserId?: string, + readOnlyState?: string + ): Promise { if (!this.loadContentCallback) { return; } // Get data from H5P server try { + console.log('readOnlyState', readOnlyState); this.playerModel = await this.loadContentCallback( contentId, - contextId + contextId, + asUserId, + readOnlyState === 'true' ); } catch (error) { this.root.innerHTML = `

Error loading H5P content from server: ${error.message}

`;