From 5d7d72875e145c5feabf300b5321b74e4677288b Mon Sep 17 00:00:00 2001 From: Angel Fernando Quiroz Campos <1697880+AngelFQC@users.noreply.github.com> Date: Wed, 3 Jul 2024 12:10:46 -0500 Subject: [PATCH 1/4] Plugin: XAPI: Restore plugin folder from 1.11.x See #3943 --- public/plugin/xapi/.htaccess | 1 + public/plugin/xapi/README.md | 88 +++ public/plugin/xapi/admin.php | 196 +++++ public/plugin/xapi/assets/css/cmi5_launch.css | 40 ++ public/plugin/xapi/assets/js/cmi5_launch.js | 2 + public/plugin/xapi/cmi5/launch.php | 188 +++++ public/plugin/xapi/cmi5/token.php | 22 + public/plugin/xapi/cmi5/view.php | 84 +++ public/plugin/xapi/cron/send_statements.php | 79 +++ public/plugin/xapi/install.php | 5 + public/plugin/xapi/lang/english.php | 56 ++ public/plugin/xapi/lang/french.php | 49 ++ public/plugin/xapi/lang/spanish.php | 56 ++ public/plugin/xapi/lrs.php | 12 + .../plugin/xapi/php-xapi/lrs-bundle/LICENSE | 19 + .../plugin/xapi/php-xapi/lrs-bundle/README.md | 0 .../xapi/php-xapi/lrs-bundle/composer.json | 51 ++ .../src/Controller/StatementGetController.php | 209 ++++++ .../Controller/StatementHeadController.php | 27 + .../Controller/StatementPostController.php | 67 ++ .../src/Controller/StatementPutController.php | 65 ++ .../src/DependencyInjection/Configuration.php | 35 + .../DependencyInjection/XApiLrsExtension.php | 60 ++ .../AlternateRequestSyntaxListener.php | 73 ++ .../src/EventListener/ExceptionListener.php | 17 + .../src/EventListener/SerializerListener.php | 40 ++ .../src/EventListener/VersionListener.php | 66 ++ .../src/Model/StatementsFilterFactory.php | 86 +++ .../src/Resources/config/controller.xml | 21 + .../src/Resources/config/doctrine.xml | 12 + .../src/Resources/config/event_listener.xml | 25 + .../src/Resources/config/factory.xml | 12 + .../lrs-bundle/src/Resources/config/orm.xml | 18 + .../src/Resources/config/routing.xml | 29 + .../src/Resources/config/serializer.xml | 32 + .../src/Response/AttachmentResponse.php | 76 ++ .../src/Response/MultipartResponse.php | 135 ++++ .../php-xapi/lrs-bundle/src/XApiLrsBundle.php | 26 + .../php-xapi/repository-doctrine-orm/LICENSE | 19 + .../repository-doctrine-orm/README.md | 0 .../repository-doctrine-orm/composer.json | 41 ++ .../metadata/Actor.orm.xml | 19 + .../metadata/Attachment.orm.xml | 23 + .../metadata/Context.orm.xml | 59 ++ .../metadata/Extensions.orm.xml | 13 + .../metadata/Result.orm.xml | 28 + .../metadata/Statement.orm.xml | 62 ++ .../metadata/StatementObject.orm.xml | 83 +++ .../metadata/Verb.orm.xml | 14 + .../src/StatementRepository.php | 67 ++ public/plugin/xapi/plugin.php | 5 + .../xapi/src/Entity/ActivityProfile.php | 93 +++ .../plugin/xapi/src/Entity/ActivityState.php | 111 +++ public/plugin/xapi/src/Entity/Cmi5Item.php | 372 ++++++++++ public/plugin/xapi/src/Entity/InternalLog.php | 227 ++++++ public/plugin/xapi/src/Entity/LrsAuth.php | 104 +++ .../Repository/ToolLaunchRepository.php | 85 +++ .../xapi/src/Entity/SharedStatement.php | 105 +++ public/plugin/xapi/src/Entity/ToolLaunch.php | 295 ++++++++ .../src/Hook/XApiActivityHookObserver.php | 66 ++ .../src/Hook/XApiCreateCourseHookObserver.php | 37 + .../Hook/XApiLearningPathEndHookObserver.php | 26 + ...XApiLearningPathItemViewedHookObserver.php | 35 + ...XApiPortfolioCommentEditedHookObserver.php | 19 + ...XApiPortfolioCommentScoredHookObserver.php | 19 + .../XApiPortfolioDownloadedHookObserver.php | 17 + .../XApiPortfolioItemAddedHookObserver.php | 25 + ...XApiPortfolioItemCommentedHookObserver.php | 25 + .../XApiPortfolioItemEditedHookObserver.php | 19 + ...piPortfolioItemHighlightedHookObserver.php | 22 + .../XApiPortfolioItemScoredHookObserver.php | 19 + .../XApiPortfolioItemViewedHookObserver.php | 25 + .../xapi/src/Hook/XApiQuizEndHookObserver.php | 29 + .../XApiQuizQuestionAnsweredHookObserver.php | 42 ++ .../xapi/src/Importer/PackageImporter.php | 67 ++ .../xapi/src/Importer/XmlPackageImporter.php | 29 + .../xapi/src/Importer/ZipPackageImporter.php | 88 +++ .../plugin/xapi/src/Lrs/AboutController.php | 30 + .../src/Lrs/ActivitiesProfileController.php | 78 ++ .../src/Lrs/ActivitiesStateController.php | 120 ++++ public/plugin/xapi/src/Lrs/BaseController.php | 28 + public/plugin/xapi/src/Lrs/LrsRequest.php | 221 ++++++ .../xapi/src/Lrs/StatementsController.php | 147 ++++ .../xapi/src/Lrs/Utils/InternalLogUtil.php | 115 +++ public/plugin/xapi/src/Parser/Cmi5Parser.php | 178 +++++ .../plugin/xapi/src/Parser/PackageParser.php | 60 ++ .../plugin/xapi/src/Parser/TinCanParser.php | 69 ++ .../ToolExperience/Activity/BaseActivity.php | 49 ++ .../src/ToolExperience/Activity/Course.php | 40 ++ .../ToolExperience/Activity/LearningPath.php | 53 ++ .../Activity/LearningPathItem.php | 54 ++ .../src/ToolExperience/Activity/Portfolio.php | 50 ++ .../Activity/PortfolioCategory.php | 58 ++ .../Activity/PortfolioComment.php | 50 ++ .../ToolExperience/Activity/PortfolioItem.php | 49 ++ .../xapi/src/ToolExperience/Activity/Quiz.php | 57 ++ .../ToolExperience/Activity/QuizQuestion.php | 170 +++++ .../xapi/src/ToolExperience/Activity/Site.php | 33 + .../src/ToolExperience/Actor/BaseActor.php | 17 + .../xapi/src/ToolExperience/Actor/User.php | 35 + .../Statement/BaseStatement.php | 53 ++ .../Statement/LearningPathCompleted.php | 61 ++ .../Statement/LearningPathItemViewed.php | 79 +++ .../Statement/PortfolioAttachmentsTrait.php | 89 +++ .../Statement/PortfolioComment.php | 25 + .../Statement/PortfolioCommentEdited.php | 39 + .../Statement/PortfolioCommentScored.php | 47 ++ .../Statement/PortfolioDownloaded.php | 42 ++ .../Statement/PortfolioItem.php | 41 ++ .../Statement/PortfolioItemCommented.php | 74 ++ .../Statement/PortfolioItemEdited.php | 39 + .../Statement/PortfolioItemHighlighted.php | 39 + .../Statement/PortfolioItemScored.php | 47 ++ .../Statement/PortfolioItemShared.php | 47 ++ .../Statement/PortfolioItemViewed.php | 40 ++ .../Statement/QuizCompleted.php | 70 ++ .../Statement/QuizQuestionAnswered.php | 79 +++ .../xapi/src/ToolExperience/Verb/Answered.php | 21 + .../xapi/src/ToolExperience/Verb/BaseVerb.php | 46 ++ .../src/ToolExperience/Verb/Commented.php | 21 + .../src/ToolExperience/Verb/Completed.php | 21 + .../src/ToolExperience/Verb/Downloaded.php | 14 + .../xapi/src/ToolExperience/Verb/Edited.php | 16 + .../src/ToolExperience/Verb/Highlighted.php | 16 + .../xapi/src/ToolExperience/Verb/Replied.php | 21 + .../xapi/src/ToolExperience/Verb/Scored.php | 16 + .../xapi/src/ToolExperience/Verb/Shared.php | 21 + .../xapi/src/ToolExperience/Verb/Viewed.php | 21 + public/plugin/xapi/src/XApiPlugin.php | 671 ++++++++++++++++++ public/plugin/xapi/start.php | 151 ++++ public/plugin/xapi/tincan/launch.php | 142 ++++ public/plugin/xapi/tincan/stats.php | 163 +++++ .../xapi/tincan/stats_attempts.ajax.php | 131 ++++ .../xapi/tincan/stats_statements.ajax.php | 113 +++ public/plugin/xapi/tincan/view.php | 192 +++++ public/plugin/xapi/tool_delete.php | 39 + public/plugin/xapi/tool_edit.php | 145 ++++ public/plugin/xapi/tool_import.php | 177 +++++ public/plugin/xapi/uninstall.php | 5 + public/plugin/xapi/views/cmi5_launch.twig | 15 + 140 files changed, 9363 insertions(+) create mode 100644 public/plugin/xapi/.htaccess create mode 100644 public/plugin/xapi/README.md create mode 100644 public/plugin/xapi/admin.php create mode 100644 public/plugin/xapi/assets/css/cmi5_launch.css create mode 100644 public/plugin/xapi/assets/js/cmi5_launch.js create mode 100644 public/plugin/xapi/cmi5/launch.php create mode 100644 public/plugin/xapi/cmi5/token.php create mode 100644 public/plugin/xapi/cmi5/view.php create mode 100644 public/plugin/xapi/cron/send_statements.php create mode 100644 public/plugin/xapi/install.php create mode 100644 public/plugin/xapi/lang/english.php create mode 100644 public/plugin/xapi/lang/french.php create mode 100644 public/plugin/xapi/lang/spanish.php create mode 100644 public/plugin/xapi/lrs.php create mode 100644 public/plugin/xapi/php-xapi/lrs-bundle/LICENSE create mode 100644 public/plugin/xapi/php-xapi/lrs-bundle/README.md create mode 100644 public/plugin/xapi/php-xapi/lrs-bundle/composer.json create mode 100644 public/plugin/xapi/php-xapi/lrs-bundle/src/Controller/StatementGetController.php create mode 100644 public/plugin/xapi/php-xapi/lrs-bundle/src/Controller/StatementHeadController.php create mode 100644 public/plugin/xapi/php-xapi/lrs-bundle/src/Controller/StatementPostController.php create mode 100644 public/plugin/xapi/php-xapi/lrs-bundle/src/Controller/StatementPutController.php create mode 100644 public/plugin/xapi/php-xapi/lrs-bundle/src/DependencyInjection/Configuration.php create mode 100644 public/plugin/xapi/php-xapi/lrs-bundle/src/DependencyInjection/XApiLrsExtension.php create mode 100644 public/plugin/xapi/php-xapi/lrs-bundle/src/EventListener/AlternateRequestSyntaxListener.php create mode 100644 public/plugin/xapi/php-xapi/lrs-bundle/src/EventListener/ExceptionListener.php create mode 100644 public/plugin/xapi/php-xapi/lrs-bundle/src/EventListener/SerializerListener.php create mode 100644 public/plugin/xapi/php-xapi/lrs-bundle/src/EventListener/VersionListener.php create mode 100644 public/plugin/xapi/php-xapi/lrs-bundle/src/Model/StatementsFilterFactory.php create mode 100644 public/plugin/xapi/php-xapi/lrs-bundle/src/Resources/config/controller.xml create mode 100644 public/plugin/xapi/php-xapi/lrs-bundle/src/Resources/config/doctrine.xml create mode 100644 public/plugin/xapi/php-xapi/lrs-bundle/src/Resources/config/event_listener.xml create mode 100644 public/plugin/xapi/php-xapi/lrs-bundle/src/Resources/config/factory.xml create mode 100644 public/plugin/xapi/php-xapi/lrs-bundle/src/Resources/config/orm.xml create mode 100644 public/plugin/xapi/php-xapi/lrs-bundle/src/Resources/config/routing.xml create mode 100644 public/plugin/xapi/php-xapi/lrs-bundle/src/Resources/config/serializer.xml create mode 100644 public/plugin/xapi/php-xapi/lrs-bundle/src/Response/AttachmentResponse.php create mode 100644 public/plugin/xapi/php-xapi/lrs-bundle/src/Response/MultipartResponse.php create mode 100644 public/plugin/xapi/php-xapi/lrs-bundle/src/XApiLrsBundle.php create mode 100644 public/plugin/xapi/php-xapi/repository-doctrine-orm/LICENSE create mode 100644 public/plugin/xapi/php-xapi/repository-doctrine-orm/README.md create mode 100644 public/plugin/xapi/php-xapi/repository-doctrine-orm/composer.json create mode 100644 public/plugin/xapi/php-xapi/repository-doctrine-orm/metadata/Actor.orm.xml create mode 100644 public/plugin/xapi/php-xapi/repository-doctrine-orm/metadata/Attachment.orm.xml create mode 100644 public/plugin/xapi/php-xapi/repository-doctrine-orm/metadata/Context.orm.xml create mode 100644 public/plugin/xapi/php-xapi/repository-doctrine-orm/metadata/Extensions.orm.xml create mode 100644 public/plugin/xapi/php-xapi/repository-doctrine-orm/metadata/Result.orm.xml create mode 100644 public/plugin/xapi/php-xapi/repository-doctrine-orm/metadata/Statement.orm.xml create mode 100644 public/plugin/xapi/php-xapi/repository-doctrine-orm/metadata/StatementObject.orm.xml create mode 100644 public/plugin/xapi/php-xapi/repository-doctrine-orm/metadata/Verb.orm.xml create mode 100644 public/plugin/xapi/php-xapi/repository-doctrine-orm/src/StatementRepository.php create mode 100644 public/plugin/xapi/plugin.php create mode 100644 public/plugin/xapi/src/Entity/ActivityProfile.php create mode 100644 public/plugin/xapi/src/Entity/ActivityState.php create mode 100644 public/plugin/xapi/src/Entity/Cmi5Item.php create mode 100644 public/plugin/xapi/src/Entity/InternalLog.php create mode 100644 public/plugin/xapi/src/Entity/LrsAuth.php create mode 100644 public/plugin/xapi/src/Entity/Repository/ToolLaunchRepository.php create mode 100644 public/plugin/xapi/src/Entity/SharedStatement.php create mode 100644 public/plugin/xapi/src/Entity/ToolLaunch.php create mode 100644 public/plugin/xapi/src/Hook/XApiActivityHookObserver.php create mode 100644 public/plugin/xapi/src/Hook/XApiCreateCourseHookObserver.php create mode 100644 public/plugin/xapi/src/Hook/XApiLearningPathEndHookObserver.php create mode 100644 public/plugin/xapi/src/Hook/XApiLearningPathItemViewedHookObserver.php create mode 100644 public/plugin/xapi/src/Hook/XApiPortfolioCommentEditedHookObserver.php create mode 100644 public/plugin/xapi/src/Hook/XApiPortfolioCommentScoredHookObserver.php create mode 100644 public/plugin/xapi/src/Hook/XApiPortfolioDownloadedHookObserver.php create mode 100644 public/plugin/xapi/src/Hook/XApiPortfolioItemAddedHookObserver.php create mode 100644 public/plugin/xapi/src/Hook/XApiPortfolioItemCommentedHookObserver.php create mode 100644 public/plugin/xapi/src/Hook/XApiPortfolioItemEditedHookObserver.php create mode 100644 public/plugin/xapi/src/Hook/XApiPortfolioItemHighlightedHookObserver.php create mode 100644 public/plugin/xapi/src/Hook/XApiPortfolioItemScoredHookObserver.php create mode 100644 public/plugin/xapi/src/Hook/XApiPortfolioItemViewedHookObserver.php create mode 100644 public/plugin/xapi/src/Hook/XApiQuizEndHookObserver.php create mode 100644 public/plugin/xapi/src/Hook/XApiQuizQuestionAnsweredHookObserver.php create mode 100644 public/plugin/xapi/src/Importer/PackageImporter.php create mode 100644 public/plugin/xapi/src/Importer/XmlPackageImporter.php create mode 100644 public/plugin/xapi/src/Importer/ZipPackageImporter.php create mode 100644 public/plugin/xapi/src/Lrs/AboutController.php create mode 100644 public/plugin/xapi/src/Lrs/ActivitiesProfileController.php create mode 100644 public/plugin/xapi/src/Lrs/ActivitiesStateController.php create mode 100644 public/plugin/xapi/src/Lrs/BaseController.php create mode 100644 public/plugin/xapi/src/Lrs/LrsRequest.php create mode 100644 public/plugin/xapi/src/Lrs/StatementsController.php create mode 100644 public/plugin/xapi/src/Lrs/Utils/InternalLogUtil.php create mode 100644 public/plugin/xapi/src/Parser/Cmi5Parser.php create mode 100644 public/plugin/xapi/src/Parser/PackageParser.php create mode 100644 public/plugin/xapi/src/Parser/TinCanParser.php create mode 100644 public/plugin/xapi/src/ToolExperience/Activity/BaseActivity.php create mode 100644 public/plugin/xapi/src/ToolExperience/Activity/Course.php create mode 100644 public/plugin/xapi/src/ToolExperience/Activity/LearningPath.php create mode 100644 public/plugin/xapi/src/ToolExperience/Activity/LearningPathItem.php create mode 100644 public/plugin/xapi/src/ToolExperience/Activity/Portfolio.php create mode 100644 public/plugin/xapi/src/ToolExperience/Activity/PortfolioCategory.php create mode 100644 public/plugin/xapi/src/ToolExperience/Activity/PortfolioComment.php create mode 100644 public/plugin/xapi/src/ToolExperience/Activity/PortfolioItem.php create mode 100644 public/plugin/xapi/src/ToolExperience/Activity/Quiz.php create mode 100644 public/plugin/xapi/src/ToolExperience/Activity/QuizQuestion.php create mode 100644 public/plugin/xapi/src/ToolExperience/Activity/Site.php create mode 100644 public/plugin/xapi/src/ToolExperience/Actor/BaseActor.php create mode 100644 public/plugin/xapi/src/ToolExperience/Actor/User.php create mode 100644 public/plugin/xapi/src/ToolExperience/Statement/BaseStatement.php create mode 100644 public/plugin/xapi/src/ToolExperience/Statement/LearningPathCompleted.php create mode 100644 public/plugin/xapi/src/ToolExperience/Statement/LearningPathItemViewed.php create mode 100644 public/plugin/xapi/src/ToolExperience/Statement/PortfolioAttachmentsTrait.php create mode 100644 public/plugin/xapi/src/ToolExperience/Statement/PortfolioComment.php create mode 100644 public/plugin/xapi/src/ToolExperience/Statement/PortfolioCommentEdited.php create mode 100644 public/plugin/xapi/src/ToolExperience/Statement/PortfolioCommentScored.php create mode 100644 public/plugin/xapi/src/ToolExperience/Statement/PortfolioDownloaded.php create mode 100644 public/plugin/xapi/src/ToolExperience/Statement/PortfolioItem.php create mode 100644 public/plugin/xapi/src/ToolExperience/Statement/PortfolioItemCommented.php create mode 100644 public/plugin/xapi/src/ToolExperience/Statement/PortfolioItemEdited.php create mode 100644 public/plugin/xapi/src/ToolExperience/Statement/PortfolioItemHighlighted.php create mode 100644 public/plugin/xapi/src/ToolExperience/Statement/PortfolioItemScored.php create mode 100644 public/plugin/xapi/src/ToolExperience/Statement/PortfolioItemShared.php create mode 100644 public/plugin/xapi/src/ToolExperience/Statement/PortfolioItemViewed.php create mode 100644 public/plugin/xapi/src/ToolExperience/Statement/QuizCompleted.php create mode 100644 public/plugin/xapi/src/ToolExperience/Statement/QuizQuestionAnswered.php create mode 100644 public/plugin/xapi/src/ToolExperience/Verb/Answered.php create mode 100644 public/plugin/xapi/src/ToolExperience/Verb/BaseVerb.php create mode 100644 public/plugin/xapi/src/ToolExperience/Verb/Commented.php create mode 100644 public/plugin/xapi/src/ToolExperience/Verb/Completed.php create mode 100644 public/plugin/xapi/src/ToolExperience/Verb/Downloaded.php create mode 100644 public/plugin/xapi/src/ToolExperience/Verb/Edited.php create mode 100644 public/plugin/xapi/src/ToolExperience/Verb/Highlighted.php create mode 100644 public/plugin/xapi/src/ToolExperience/Verb/Replied.php create mode 100644 public/plugin/xapi/src/ToolExperience/Verb/Scored.php create mode 100644 public/plugin/xapi/src/ToolExperience/Verb/Shared.php create mode 100644 public/plugin/xapi/src/ToolExperience/Verb/Viewed.php create mode 100644 public/plugin/xapi/src/XApiPlugin.php create mode 100644 public/plugin/xapi/start.php create mode 100644 public/plugin/xapi/tincan/launch.php create mode 100644 public/plugin/xapi/tincan/stats.php create mode 100644 public/plugin/xapi/tincan/stats_attempts.ajax.php create mode 100644 public/plugin/xapi/tincan/stats_statements.ajax.php create mode 100644 public/plugin/xapi/tincan/view.php create mode 100644 public/plugin/xapi/tool_delete.php create mode 100644 public/plugin/xapi/tool_edit.php create mode 100644 public/plugin/xapi/tool_import.php create mode 100644 public/plugin/xapi/uninstall.php create mode 100644 public/plugin/xapi/views/cmi5_launch.twig diff --git a/public/plugin/xapi/.htaccess b/public/plugin/xapi/.htaccess new file mode 100644 index 00000000000..a3851a941ea --- /dev/null +++ b/public/plugin/xapi/.htaccess @@ -0,0 +1 @@ +AcceptPathInfo On \ No newline at end of file diff --git a/public/plugin/xapi/README.md b/public/plugin/xapi/README.md new file mode 100644 index 00000000000..64a7c88c2f8 --- /dev/null +++ b/public/plugin/xapi/README.md @@ -0,0 +1,88 @@ +# Experience API (xAPI) + +Allows you to connect to an external Learning Record Store and use activities with the xAPI standard. + +> You can import and use TinCan packages. +> Import CMI5 packages is to be considered a Beta state and still in development. + +**Configuration** + +Set LRS endpoint, username and password to integrate an external LRS in Chamilo LMS. + +The fields "Learning path item viewed", "Learning path ended", "Quiz question answered" and "Quiz ended" allow enabling +hooks when the user views an item in learning path, completes a learning path, answers a quiz question and ends the exam. + +The statements generated with these hooks are logged in Chamilo database, waiting to be sent to the LRS by a cron job. +The cron job to configure on your server is located in `CHAMILO_PATH/plugin/xapi/cron/send_statements.php`. + +**Use the Statement API from Chamilo LMS** + +You can use xAPI's "Statement API" to save some statements from another service. +You need to create credentials (username/password) to do this. First you need to enable the "menu_administrator" region +in the plugin configuration. You will then be able to create the credentials with the new page "Experience API (xAPI)" +inside de Plugins block in the Administration panel. +The endpoint for the statements API is "https://CHAMILO_DOMAIN/plugin/xapi/lrs.php/"; + +```mysql +CREATE TABLE xapi_attachment (identifier INT AUTO_INCREMENT NOT NULL, statement_id VARCHAR(255) DEFAULT NULL, usageType VARCHAR(255) NOT NULL, contentType VARCHAR(255) NOT NULL, length INT NOT NULL, sha2 VARCHAR(255) NOT NULL, display LONGTEXT NOT NULL COMMENT '(DC2Type:json)', hasDescription TINYINT(1) NOT NULL, description LONGTEXT DEFAULT NULL COMMENT '(DC2Type:json)', fileUrl VARCHAR(255) DEFAULT NULL, content LONGTEXT DEFAULT NULL, INDEX IDX_7148C9A1849CB65B (statement_id), PRIMARY KEY(identifier)) DEFAULT CHARACTER SET utf8 COLLATE `utf8_unicode_ci` ENGINE = InnoDB; +CREATE TABLE xapi_object (identifier INT AUTO_INCREMENT NOT NULL, group_id INT DEFAULT NULL, actor_id INT DEFAULT NULL, verb_id INT DEFAULT NULL, object_id INT DEFAULT NULL, type VARCHAR(255) DEFAULT NULL, activityId VARCHAR(255) DEFAULT NULL, hasActivityDefinition TINYINT(1) DEFAULT NULL, hasActivityName TINYINT(1) DEFAULT NULL, activityName LONGTEXT DEFAULT NULL COMMENT '(DC2Type:json)', hasActivityDescription TINYINT(1) DEFAULT NULL, activityDescription LONGTEXT DEFAULT NULL COMMENT '(DC2Type:json)', activityType VARCHAR(255) DEFAULT NULL, activityMoreInfo VARCHAR(255) DEFAULT NULL, mbox VARCHAR(255) DEFAULT NULL, mboxSha1Sum VARCHAR(255) DEFAULT NULL, openId VARCHAR(255) DEFAULT NULL, accountName VARCHAR(255) DEFAULT NULL, accountHomePage VARCHAR(255) DEFAULT NULL, name VARCHAR(255) DEFAULT NULL, referenced_statement_id VARCHAR(255) DEFAULT NULL, activityExtensions_id INT DEFAULT NULL, parentContext_id INT DEFAULT NULL, groupingContext_id INT DEFAULT NULL, categoryContext_id INT DEFAULT NULL, otherContext_id INT DEFAULT NULL, UNIQUE INDEX UNIQ_E2B68640303C7F1D (activityExtensions_id), INDEX IDX_E2B68640FE54D947 (group_id), UNIQUE INDEX UNIQ_E2B6864010DAF24A (actor_id), UNIQUE INDEX UNIQ_E2B68640C1D03483 (verb_id), UNIQUE INDEX UNIQ_E2B68640232D562B (object_id), INDEX IDX_E2B68640988A4CEC (parentContext_id), INDEX IDX_E2B686404F542860 (groupingContext_id), INDEX IDX_E2B68640AEA1B132 (categoryContext_id), INDEX IDX_E2B68640B73EEAB7 (otherContext_id), PRIMARY KEY(identifier)) DEFAULT CHARACTER SET utf8 COLLATE `utf8_unicode_ci` ENGINE = InnoDB; +CREATE TABLE xapi_result (identifier INT AUTO_INCREMENT NOT NULL, extensions_id INT DEFAULT NULL, hasScore TINYINT(1) NOT NULL, scaled DOUBLE PRECISION DEFAULT NULL, raw DOUBLE PRECISION DEFAULT NULL, min DOUBLE PRECISION DEFAULT NULL, max DOUBLE PRECISION DEFAULT NULL, success TINYINT(1) DEFAULT NULL, completion TINYINT(1) DEFAULT NULL, response VARCHAR(255) DEFAULT NULL, duration VARCHAR(255) DEFAULT NULL, UNIQUE INDEX UNIQ_5971ECBFD0A19400 (extensions_id), PRIMARY KEY(identifier)) DEFAULT CHARACTER SET utf8 COLLATE `utf8_unicode_ci` ENGINE = InnoDB; +CREATE TABLE xapi_verb (identifier INT AUTO_INCREMENT NOT NULL, id VARCHAR(255) NOT NULL, display LONGTEXT NOT NULL COMMENT '(DC2Type:json)', PRIMARY KEY(identifier)) DEFAULT CHARACTER SET utf8 COLLATE `utf8_unicode_ci` ENGINE = InnoDB; +CREATE TABLE xapi_extensions (identifier INT AUTO_INCREMENT NOT NULL, extensions LONGTEXT NOT NULL COMMENT '(DC2Type:json)', PRIMARY KEY(identifier)) DEFAULT CHARACTER SET utf8 COLLATE `utf8_unicode_ci` ENGINE = InnoDB; +CREATE TABLE xapi_context (identifier INT AUTO_INCREMENT NOT NULL, instructor_id INT DEFAULT NULL, team_id INT DEFAULT NULL, extensions_id INT DEFAULT NULL, registration VARCHAR(255) DEFAULT NULL, hasContextActivities TINYINT(1) DEFAULT NULL, revision VARCHAR(255) DEFAULT NULL, platform VARCHAR(255) DEFAULT NULL, language VARCHAR(255) DEFAULT NULL, statement VARCHAR(255) DEFAULT NULL, UNIQUE INDEX UNIQ_3D7771908C4FC193 (instructor_id), UNIQUE INDEX UNIQ_3D777190296CD8AE (team_id), UNIQUE INDEX UNIQ_3D777190D0A19400 (extensions_id), PRIMARY KEY(identifier)) DEFAULT CHARACTER SET utf8 COLLATE `utf8_unicode_ci` ENGINE = InnoDB; +CREATE TABLE xapi_actor (identifier INT AUTO_INCREMENT NOT NULL, type VARCHAR(255) DEFAULT NULL, mbox VARCHAR(255) DEFAULT NULL, mboxSha1Sum VARCHAR(255) DEFAULT NULL, openId VARCHAR(255) DEFAULT NULL, accountName VARCHAR(255) DEFAULT NULL, accountHomePage VARCHAR(255) DEFAULT NULL, name VARCHAR(255) DEFAULT NULL, members VARCHAR(255) NOT NULL, PRIMARY KEY(identifier)) DEFAULT CHARACTER SET utf8 COLLATE `utf8_unicode_ci` ENGINE = InnoDB; +CREATE TABLE xapi_statement (id VARCHAR(255) NOT NULL, actor_id INT DEFAULT NULL, verb_id INT DEFAULT NULL, object_id INT DEFAULT NULL, result_id INT DEFAULT NULL, authority_id INT DEFAULT NULL, context_id INT DEFAULT NULL, created BIGINT DEFAULT NULL, `stored` BIGINT DEFAULT NULL, hasAttachments TINYINT(1) DEFAULT NULL, UNIQUE INDEX UNIQ_BAF6663B10DAF24A (actor_id), UNIQUE INDEX UNIQ_BAF6663BC1D03483 (verb_id), UNIQUE INDEX UNIQ_BAF6663B232D562B (object_id), UNIQUE INDEX UNIQ_BAF6663B7A7B643 (result_id), UNIQUE INDEX UNIQ_BAF6663B81EC865B (authority_id), UNIQUE INDEX UNIQ_BAF6663B6B00C1CF (context_id), PRIMARY KEY(id)) DEFAULT CHARACTER SET utf8 COLLATE `utf8_unicode_ci` ENGINE = InnoDB; +ALTER TABLE xapi_attachment ADD CONSTRAINT FK_7148C9A1849CB65B FOREIGN KEY (statement_id) REFERENCES xapi_statement (id); +ALTER TABLE xapi_object ADD CONSTRAINT FK_E2B68640303C7F1D FOREIGN KEY (activityExtensions_id) REFERENCES xapi_extensions (identifier); +ALTER TABLE xapi_object ADD CONSTRAINT FK_E2B68640FE54D947 FOREIGN KEY (group_id) REFERENCES xapi_object (identifier); +ALTER TABLE xapi_object ADD CONSTRAINT FK_E2B6864010DAF24A FOREIGN KEY (actor_id) REFERENCES xapi_object (identifier); +ALTER TABLE xapi_object ADD CONSTRAINT FK_E2B68640C1D03483 FOREIGN KEY (verb_id) REFERENCES xapi_verb (identifier); +ALTER TABLE xapi_object ADD CONSTRAINT FK_E2B68640232D562B FOREIGN KEY (object_id) REFERENCES xapi_object (identifier); +ALTER TABLE xapi_object ADD CONSTRAINT FK_E2B68640988A4CEC FOREIGN KEY (parentContext_id) REFERENCES xapi_context (identifier); +ALTER TABLE xapi_object ADD CONSTRAINT FK_E2B686404F542860 FOREIGN KEY (groupingContext_id) REFERENCES xapi_context (identifier); +ALTER TABLE xapi_object ADD CONSTRAINT FK_E2B68640AEA1B132 FOREIGN KEY (categoryContext_id) REFERENCES xapi_context (identifier); +ALTER TABLE xapi_object ADD CONSTRAINT FK_E2B68640B73EEAB7 FOREIGN KEY (otherContext_id) REFERENCES xapi_context (identifier); +ALTER TABLE xapi_result ADD CONSTRAINT FK_5971ECBFD0A19400 FOREIGN KEY (extensions_id) REFERENCES xapi_extensions (identifier); +ALTER TABLE xapi_context ADD CONSTRAINT FK_3D7771908C4FC193 FOREIGN KEY (instructor_id) REFERENCES xapi_object (identifier); +ALTER TABLE xapi_context ADD CONSTRAINT FK_3D777190296CD8AE FOREIGN KEY (team_id) REFERENCES xapi_object (identifier); +ALTER TABLE xapi_context ADD CONSTRAINT FK_3D777190D0A19400 FOREIGN KEY (extensions_id) REFERENCES xapi_extensions (identifier); +ALTER TABLE xapi_statement ADD CONSTRAINT FK_BAF6663B10DAF24A FOREIGN KEY (actor_id) REFERENCES xapi_object (identifier); +ALTER TABLE xapi_statement ADD CONSTRAINT FK_BAF6663BC1D03483 FOREIGN KEY (verb_id) REFERENCES xapi_verb (identifier); +ALTER TABLE xapi_statement ADD CONSTRAINT FK_BAF6663B232D562B FOREIGN KEY (object_id) REFERENCES xapi_object (identifier); +ALTER TABLE xapi_statement ADD CONSTRAINT FK_BAF6663B7A7B643 FOREIGN KEY (result_id) REFERENCES xapi_result (identifier); +ALTER TABLE xapi_statement ADD CONSTRAINT FK_BAF6663B81EC865B FOREIGN KEY (authority_id) REFERENCES xapi_object (identifier); +ALTER TABLE xapi_statement ADD CONSTRAINT FK_BAF6663B6B00C1CF FOREIGN KEY (context_id) REFERENCES xapi_context (identifier); + +CREATE TABLE xapi_shared_statement (id INT AUTO_INCREMENT NOT NULL, uuid VARCHAR(255) DEFAULT NULL, statement LONGTEXT NOT NULL COMMENT '(DC2Type:array)', sent TINYINT(1) DEFAULT '0' NOT NULL, INDEX idx_uuid (uuid), PRIMARY KEY(id)) DEFAULT CHARACTER SET utf8 COLLATE `utf8_unicode_ci` ENGINE = InnoDB; + +CREATE TABLE xapi_lrs_auth (id INT AUTO_INCREMENT NOT NULL, username VARCHAR(255) NOT NULL, password VARCHAR(255) NOT NULL, enabled TINYINT(1) NOT NULL, created_at DATETIME NOT NULL, PRIMARY KEY(id)) DEFAULT CHARACTER SET utf8 COLLATE `utf8_unicode_ci` ENGINE = InnoDB; + +CREATE TABLE xapi_tool_launch (id INT AUTO_INCREMENT NOT NULL, c_id INT NOT NULL, session_id INT DEFAULT NULL, title VARCHAR(255) NOT NULL, description LONGTEXT DEFAULT NULL, launch_url VARCHAR(255) NOT NULL, activity_id VARCHAR(255) DEFAULT NULL, activity_type VARCHAR(255) DEFAULT NULL, allow_multiple_attempts TINYINT(1) DEFAULT '1' NOT NULL, lrs_url VARCHAR(255) DEFAULT NULL, lrs_auth_username VARCHAR(255) DEFAULT NULL, lrs_auth_password VARCHAR(255) DEFAULT NULL, INDEX IDX_E18CB58391D79BD3 (c_id), INDEX IDX_E18CB583613FECDF (session_id), PRIMARY KEY(id)) DEFAULT CHARACTER SET utf8 COLLATE `utf8_unicode_ci` ENGINE = InnoDB; +ALTER TABLE xapi_tool_launch ADD CONSTRAINT FK_E18CB58391D79BD3 FOREIGN KEY (c_id) REFERENCES course (id); +ALTER TABLE xapi_tool_launch ADD CONSTRAINT FK_E18CB583613FECDF FOREIGN KEY (session_id) REFERENCES session (id); + +CREATE TABLE xapi_cmi5_item (id INT AUTO_INCREMENT NOT NULL, tree_root INT DEFAULT NULL, parent_id INT DEFAULT NULL, identifier VARCHAR(255) NOT NULL, type VARCHAR(255) NOT NULL, title LONGTEXT NOT NULL COMMENT '(DC2Type:json)', description LONGTEXT NOT NULL COMMENT '(DC2Type:json)', url VARCHAR(255) DEFAULT NULL, activity_type VARCHAR(255) DEFAULT NULL, launch_method VARCHAR(255) DEFAULT NULL, move_on VARCHAR(255) DEFAULT NULL, mastery_score DOUBLE PRECISION DEFAULT NULL, launch_parameters VARCHAR(255) DEFAULT NULL, entitlement_key VARCHAR(255) DEFAULT NULL, status VARCHAR(255) DEFAULT NULL, lft INT NOT NULL, lvl INT NOT NULL, rgt INT NOT NULL, tool_id INT DEFAULT NULL, INDEX IDX_7CA116D88F7B22CC (tool_id), INDEX IDX_7CA116D8A977936C (tree_root), INDEX IDX_7CA116D8727ACA70 (parent_id), PRIMARY KEY(id)) DEFAULT CHARACTER SET utf8 COLLATE `utf8_unicode_ci` ENGINE = InnoDB; +ALTER TABLE xapi_cmi5_item ADD CONSTRAINT FK_7CA116D8A977936C FOREIGN KEY (tree_root) REFERENCES xapi_cmi5_item (id) ON DELETE CASCADE; +ALTER TABLE xapi_cmi5_item ADD CONSTRAINT FK_7CA116D8727ACA70 FOREIGN KEY (parent_id) REFERENCES xapi_cmi5_item (id) ON DELETE CASCADE; + +CREATE TABLE xapi_activity_state (id INT AUTO_INCREMENT NOT NULL, state_id VARCHAR(255) NOT NULL, activity_id VARCHAR(255) NOT NULL, agent LONGTEXT NOT NULL COMMENT '(DC2Type:json)', document_data LONGTEXT NOT NULL COMMENT '(DC2Type:json)', PRIMARY KEY(id)) DEFAULT CHARACTER SET utf8 COLLATE `utf8_unicode_ci` ENGINE = InnoDB; +CREATE TABLE xapi_activity_profile (id INT AUTO_INCREMENT NOT NULL, profile_id VARCHAR(255) NOT NULL, activity_id VARCHAR(255) NOT NULL, document_data LONGTEXT NOT NULL COMMENT '(DC2Type:json)', PRIMARY KEY(id)) DEFAULT CHARACTER SET utf8 COLLATE `utf8_unicode_ci` ENGINE = InnoDB; +``` + +**From 0.2 (beta) [2021-10-15]** +- With the LRS an internal log is registered based on the actor mbox's email or the actor account's name coming from the statement +To update, execute this queries: + +```sql +CREATE TABLE xapi_internal_log (id INT AUTO_INCREMENT NOT NULL, user_id INT DEFAULT NULL, statement_id VARCHAR(255) NOT NULL, verb VARCHAR(255) NOT NULL, object_id VARCHAR(255) NOT NULL, activity_name VARCHAR(255) DEFAULT NULL, activity_description VARCHAR(255) DEFAULT NULL, score_scaled DOUBLE PRECISION DEFAULT NULL, score_raw DOUBLE PRECISION DEFAULT NULL, score_min DOUBLE PRECISION DEFAULT NULL, score_max DOUBLE PRECISION DEFAULT NULL, created_at DATETIME DEFAULT NULL, INDEX IDX_C1C667ACA76ED395 (user_id), PRIMARY KEY(id)) DEFAULT CHARACTER SET utf8 COLLATE `utf8_unicode_ci` ENGINE = InnoDB; +ALTER TABLE xapi_internal_log ADD CONSTRAINT FK_C1C667ACA76ED395 FOREIGN KEY (user_id) REFERENCES user (id); +``` + +**From 0.3 (beta) [2021-11-11]** + +- Fix: Add foreign keys with course/session in tool_launch table and foreign key with user in internal_log table. +```sql +ALTER TABLE xapi_internal_log ADD CONSTRAINT FK_C1C667ACA76ED395 FOREIGN KEY (user_id) REFERENCES user (id) ON DELETE CASCADE; +ALTER TABLE xapi_tool_launch ADD CONSTRAINT FK_E18CB58391D79BD3 FOREIGN KEY (c_id) REFERENCES course (id) ON DELETE CASCADE; +ALTER TABLE xapi_tool_launch ADD CONSTRAINT FK_E18CB583613FECDF FOREIGN KEY (session_id) REFERENCES session (id) ON DELETE CASCADE; +``` diff --git a/public/plugin/xapi/admin.php b/public/plugin/xapi/admin.php new file mode 100644 index 00000000000..4affc59fd0f --- /dev/null +++ b/public/plugin/xapi/admin.php @@ -0,0 +1,196 @@ +getId()}"; + } + + $form = new FormValidator('frm_xapi_auth', 'post', $action); + $form->addText('username', get_lang('Username'), true); + $form->addText('password', get_lang('Password'), true); + $form->addCheckBox('enabled', get_lang('Enabled'), get_lang('Yes')); + + $form->addButtonSave(get_lang('Save')); + + if (null != $auth) { + $form->setDefaults( + [ + 'username' => $auth->getUsername(), + 'password' => $auth->getPassword(), + 'enabled' => $auth->isEnabled(), + ] + ); + } + + return $form; +} + +switch ($request->query->getAlpha('action')) { + case 'add': + $form = createForm(); + + if ($form->validate()) { + $values = $form->exportValues(); + + $auth = new LrsAuth(); + $auth + ->setUsername($values['username']) + ->setPassword($values['password']) + ->setEnabled(isset($values['enabled'])) + ->setCreatedAt( + api_get_utc_datetime(null, false, true) + ); + + $em->persist($auth); + $em->flush(); + + Display::addFlash( + Display::return_message(get_lang('ItemAdded'), 'success') + ); + + header('Location: '.$pageBaseUrl); + exit; + } + + $pageActions = Display::url( + Display::return_icon('back.png', get_lang('Back'), [], ICON_SIZE_MEDIUM), + $pageBaseUrl + ); + $pageContent = $form->returnForm(); + break; + case 'edit': + $auth = $em->find(LrsAuth::class, $request->query->getInt('id')); + + if (null == $auth) { + api_not_allowed(true); + } + + $form = createForm($auth); + + if ($form->validate()) { + $values = $form->exportValues(); + + $auth + ->setUsername($values['username']) + ->setPassword($values['password']) + ->setEnabled(isset($values['enabled'])) + ->setCreatedAt( + api_get_utc_datetime(null, false, true) + ); + + $em->persist($auth); + $em->flush(); + + Display::addFlash( + Display::return_message(get_lang('ItemUpdated'), 'success') + ); + + header('Location: '.$pageBaseUrl); + exit; + } + + $pageActions = Display::url( + Display::return_icon('back.png', get_lang('Back'), [], ICON_SIZE_MEDIUM), + $pageBaseUrl + ); + $pageContent = $form->returnForm(); + break; + case 'delete': + $auth = $em->find(LrsAuth::class, $request->query->getInt('id')); + + if (null == $auth) { + api_not_allowed(true); + } + + $em->remove($auth); + $em->flush(); + + Display::addFlash( + Display::return_message(get_lang('ItemDeleted'), 'success') + ); + + header('Location: '.$pageBaseUrl); + exit; + case 'list': + default: + $pageActions = Display::url( + Display::return_icon('add.png', get_lang('Add'), [], ICON_SIZE_MEDIUM), + $pageBaseUrl.'?action=add' + ); + $pageContent = Display::return_message(get_lang('NoData'), 'warning'); + + $auths = $em->getRepository(LrsAuth::class)->findAll(); + + if (count($auths) > 0) { + $row = 0; + + $table = new HTML_Table(['class' => 'table table-striped table-hover']); + $table->setHeaderContents($row, 0, get_lang('Username')); + $table->setHeaderContents($row, 1, get_lang('Password')); + $table->setHeaderContents($row, 2, get_lang('Enabled')); + $table->setHeaderContents($row, 3, get_lang('CreatedAt')); + $table->setHeaderContents($row, 4, get_lang('Actions')); + + foreach ($auths as $auth) { + $row++; + + $actions = [ + Display::url( + Display::return_icon('edit.png', get_lang('Edit')), + $pageBaseUrl.'?action=edit&id='.$auth->getId() + ), + Display::url( + Display::return_icon('delete.png', get_lang('Edit')), + $pageBaseUrl.'?action=delete&id='.$auth->getId() + ), + ]; + + $table->setCellContents($row, 0, $auth->getUsername()); + $table->setCellContents($row, 1, $auth->getPassword()); + $table->setCellContents($row, 2, $auth->isEnabled() ? get_lang('Yes') : get_lang('No')); + $table->setCellContents($row, 3, api_convert_and_format_date($auth->getCreatedAt())); + $table->setCellContents($row, 4, implode(PHP_EOL, $actions)); + } + + $pageContent = $table->toHtml(); + } + break; +} + +$interbreadcrumb[] = [ + 'name' => get_lang('Administration'), + 'url' => api_get_path(WEB_CODE_PATH).'admin/index.php', +]; + +$view = new Template($plugin->get_title()); +$view->assign('actions', Display::toolbarAction('xapi_actions', [$pageActions])); +$view->assign('content', $pageContent); +$view->display_one_col_template(); diff --git a/public/plugin/xapi/assets/css/cmi5_launch.css b/public/plugin/xapi/assets/css/cmi5_launch.css new file mode 100644 index 00000000000..eaa82c2da1d --- /dev/null +++ b/public/plugin/xapi/assets/css/cmi5_launch.css @@ -0,0 +1,40 @@ +.section-global { + margin: 0; +} + +#pnl-left ul { + list-style: none; + margin: 0; + padding: 0; +} +#pnl-left ul li a { + padding: 5px 0; + display: block; +} +#pnl-left ul ul li { + padding: 0 0 0 15px ; +} + +@media (min-width: 992px) { + #pnl-left { + overflow: auto; + position: absolute; + top: 0; + bottom: 0; + left: 0; + } + #pnl-right { + position: absolute; + right: 0; + top: 0; + bottom: 0; + } + #ifr-content { + min-width: 100%; + min-height: 100%; + padding: 0; + margin: 0; + position: absolute; + right: 0; + } +} \ No newline at end of file diff --git a/public/plugin/xapi/assets/js/cmi5_launch.js b/public/plugin/xapi/assets/js/cmi5_launch.js new file mode 100644 index 00000000000..22d2d963e07 --- /dev/null +++ b/public/plugin/xapi/assets/js/cmi5_launch.js @@ -0,0 +1,2 @@ +$(function () { +}); \ No newline at end of file diff --git a/public/plugin/xapi/cmi5/launch.php b/public/plugin/xapi/cmi5/launch.php new file mode 100644 index 00000000000..9c9c65536b0 --- /dev/null +++ b/public/plugin/xapi/cmi5/launch.php @@ -0,0 +1,188 @@ +find(Cmi5Item::class, $request->query->getInt('id')); +$toolLaunch = $item->getTool(); + +if ($toolLaunch->getId() !== $request->query->getInt('tool')) { + api_not_allowed( + false, + Display::return_message(get_lang('NotAllwed'), 'error') + ); +} + +$plugin = XApiPlugin::create(); +$user = api_get_user_entity(api_get_user_id()); +$nowDate = api_get_utc_datetime(null, false, true)->format('c'); + +$registration = (string) Uuid::uuid4(); +$actor = new Agent( + InverseFunctionalIdentifier::withAccount( + new Account( + $user->getCompleteName(), + IRL::fromString(api_get_path(WEB_PATH)) + ) + ), + $user->getCompleteName() +); +$verb = new Verb( + IRI::fromString('http://adlnet.gov/expapi/verbs/launched'), + LanguageMap::create($plugin->getLangMap('Launched')) +); +$customActivityId = $plugin->generateIri($item->getId(), 'cmi5_item'); + +$activity = new Activity( + $customActivityId, + new Definition( + LanguageMap::create($item->getTitle()), + LanguageMap::create($item->getDescription()), + IRI::fromString($item->getIdentifier()) + ) +); + +$context = (new Context()) + ->withPlatform( + api_get_setting('Institution').' - '.api_get_setting('siteName') + ) + ->withLanguage(api_get_language_isocode()) + ->withRegistration($registration); + +$statementUuid = Uuid::uuid5( + $plugin->get(XApiPlugin::SETTING_UUID_NAMESPACE), + "cmi5_item/{$item->getId()}" +); + +$statement = new Statement( + StatementId::fromUuid($statementUuid), + $actor, + $verb, + $activity, + null, + null, + api_get_utc_datetime(null, false, true), + null, + $context +); + +$statementClient = XApiPlugin::create()->getXApiStatementClient(); + +//try { +// $statementClient->storeStatement($statement); +//} catch (ConflictException $e) { +// echo Display::return_message($e->getMessage(), 'error'); +// +// exit; +//} catch (XApiException $e) { +// echo Display::return_message($e->getMessage(), 'error'); +// +// exit; +//} + +$viewSessionId = (string) Uuid::uuid4(); + +$state = new State( + $activity, + $actor, + 'LMS.LaunchData', + (string) $registration +); + +$documentDataData = []; +$documentDataData['contentTemplate'] = [ + 'extensions' => [ + 'https://w3id.org/xapi/cmi5/context/extensions/sessionid' => $viewSessionId, + ], +]; +$documentDataData['launchMode'] = 'Normal'; +$documentDataData['launchMethod'] = $item->getLaunchMethod(); + +if ($item->getLaunchParameters()) { + $documentDataData['launchParameteres'] = $item->getLaunchParameters(); +} + +if ($item->getMasteryScore()) { + $documentDataData['masteryScore'] = $item->getMasteryScore(); +} + +if ($item->getEntitlementKey()) { + $documentDataData['entitlementKey'] = [ + 'courseStructure' => $item->getEntitlementKey(), + ]; +} + +$documentData = new DocumentData($documentDataData); + +try { + $plugin + ->getXApiStateClient() + ->createOrReplaceDocument( + new StateDocument($state, $documentData) + ); +} catch (Exception $exception) { + echo Display::return_message($exception->getMessage(), 'error'); + + exit; +} + +$launchUrl = $plugin->generateLaunchUrl( + 'cmi5', + $item->getUrl(), + $customActivityId->getValue(), + $actor, + $registration, + $toolLaunch->getLrsUrl(), + $toolLaunch->getLrsAuthUsername(), + $toolLaunch->getLrsAuthPassword(), + $viewSessionId +); + +if ('OwnWindow' === $item->getLaunchMethod()) { + Display::display_reduced_header(); + + echo '

'; + echo Display::toolbarButton( + $plugin->get_lang('LaunchNewAttempt'), + $launchUrl, + 'external-link fa-fw', + 'success', + [ + 'target' => '_blank', + ] + ); + echo ''; + + Display::display_reduced_footer(); + + exit; +} + +header("Location: $launchUrl"); diff --git a/public/plugin/xapi/cmi5/token.php b/public/plugin/xapi/cmi5/token.php new file mode 100644 index 00000000000..be04b7e189e --- /dev/null +++ b/public/plugin/xapi/cmi5/token.php @@ -0,0 +1,22 @@ +getMethod()) { + $token = base64_encode(uniqid()); + + $response->setStatusCode(Response::HTTP_OK); + $response->setData(['auth-token' => $token]); +} + +$response->send(); diff --git a/public/plugin/xapi/cmi5/view.php b/public/plugin/xapi/cmi5/view.php new file mode 100644 index 00000000000..dc0f0833593 --- /dev/null +++ b/public/plugin/xapi/cmi5/view.php @@ -0,0 +1,84 @@ +find( + ToolLaunch::class, + $request->query->getInt('id') +); + +if (null === $toolLaunch + || 'cmi5' !== $toolLaunch->getActivityType() +) { + header('Location: '.api_get_course_url()); + exit; +} + +$plugin = XApiPlugin::create(); +$course = api_get_course_entity(); +$session = api_get_session_entity(); +$cidReq = api_get_cidreq(); +$user = api_get_user_entity(api_get_user_id()); +$interfaceLanguage = api_get_interface_language(); + +$itemsRepo = $em->getRepository(Cmi5Item::class); + +$query = $em->createQueryBuilder() + ->select('item') + ->from(Cmi5Item::class, 'item') + ->where('item.tool = :tool') + ->setParameter('tool', $toolLaunch->getId()) + ->getQuery(); + +$tocHtml = $itemsRepo->buildTree( + $query->getArrayResult(), + [ + 'decorate' => true, + 'rootOpen' => '

', + 'childOpen' => '
  • ', + 'childClose' => '
  • ', + 'nodeDecorator' => function ($node) use ($interfaceLanguage, $cidReq, $toolLaunch) { + $titleMap = LanguageMap::create($node['title']); + $title = XApiPlugin::extractVerbInLanguage($titleMap, $interfaceLanguage); + + if ('block' === $node['type']) { + return Display::page_subheader($title, null, 'h4'); + } + + return Display::url( + $title, + "launch.php?tool={$toolLaunch->getId()}&id={$node['id']}&$cidReq", + [ + 'target' => 'ifr_content', + 'class' => 'text-left btn-link', + ] + ); + }, + ] +); + +$webPluginPath = api_get_path(WEB_PLUGIN_PATH); + +$htmlHeadXtra[] = api_get_css($webPluginPath.'xapi/assets/css/cmi5_launch.css'); +$htmlHeadXtra[] = api_get_js_simple($webPluginPath.'xapi/assets/js/cmi5_launch.js'); + +$view = new Template('', false, false, true, true, false); +$view->assign('tool', $toolLaunch); +$view->assign('toc_html', $tocHtml); +$view->assign('content', $view->fetch('xapi/views/cmi5_launch.twig')); +$view->display_no_layout_template(); diff --git a/public/plugin/xapi/cron/send_statements.php b/public/plugin/xapi/cron/send_statements.php new file mode 100644 index 00000000000..21cfc2bcbce --- /dev/null +++ b/public/plugin/xapi/cron/send_statements.php @@ -0,0 +1,79 @@ +getRepository(SharedStatement::class) + ->findBy( + ['uuid' => null, 'sent' => false], + null, + 100 + ); +$countNotSent = count($notSentSharedStatements); + +if ($countNotSent > 0) { + echo '['.time().'] Trying to send '.$countNotSent.' statements to LRS'.PHP_EOL; + + $client = XApiPlugin::create()->getXapiStatementCronClient(); + + /** @var SharedStatement $notSentSharedStatement */ + foreach ($notSentSharedStatements as $notSentSharedStatement) { + $notSentStatement = $statementSerializer->deserializeStatement( + json_encode($notSentSharedStatement->getStatement()) + ); + + if (null == $notSentStatement->getId()) { + $notSentStatement = $notSentStatement->withId( + StatementId::fromUuid(Uuid::uuid4()) + ); + } + + try { + echo '['.time()."] Sending shared statement ({$notSentSharedStatement->getId()})"; + + $sentStatement = $client->storeStatement($notSentStatement); + + echo "\t\tStatement ID received: \"{$sentStatement->getId()->getValue()}\""; + } catch (ConflictException $e) { + echo $e->getMessage().PHP_EOL; + + continue; + } catch (XApiException $e) { + echo $e->getMessage().PHP_EOL; + + continue; + } + + $notSentSharedStatement + ->setUuid($sentStatement->getId()->getValue()) + ->setSent(true); + + $em->persist($notSentSharedStatement); + + echo "\t\tShared statement updated".PHP_EOL; + } + + $em->flush(); +} else { + echo 'No statements to process.'.PHP_EOL; +} diff --git a/public/plugin/xapi/install.php b/public/plugin/xapi/install.php new file mode 100644 index 00000000000..c939bf5325f --- /dev/null +++ b/public/plugin/xapi/install.php @@ -0,0 +1,5 @@ +install(); diff --git a/public/plugin/xapi/lang/english.php b/public/plugin/xapi/lang/english.php new file mode 100644 index 00000000000..bee4d9a2b8e --- /dev/null +++ b/public/plugin/xapi/lang/english.php @@ -0,0 +1,56 @@ +This is generated automatically by Chamilo LMS. Don\'t replace it.'; +$strings['lrs_url'] = 'LRS endpoint'; +$strings['lrs_url_help'] = 'Base URL of the LRS'; +$strings['lrs_auth_username'] = 'LRS user'; +$strings['lrs_auth_username_help'] = 'Username for basic HTTP authentication'; +$strings['lrs_auth_password'] = 'LRS password'; +$strings['lrs_auth_password_help'] = 'Password for basic HTTP authentication'; +$strings['cron_lrs_url'] = 'Cron: LRS endpoint'; +$strings['cron_lrs_url_help'] = 'Alternative base URL of the LRS for the cron process'; +$strings['cron_lrs_auth_username'] = 'Cron: LRS user'; +$strings['cron_lrs_auth_username_help'] = 'Alternative username for basic HTTP authentication for the cron process'; +$strings['cron_lrs_auth_password'] = 'Cron: LRS password'; +$strings['cron_lrs_auth_password_help'] = 'Alternative password for basic HTTP authentication for the cron process'; +$strings['lrs_lp_item_viewed_active'] = 'Learning path item viewed'; +$strings['lrs_lp_end_active'] = 'Learning path ended'; +$strings['lrs_quiz_active'] = 'Quiz ended'; +$strings['lrs_quiz_question_active'] = 'Quiz question answered'; +$strings['lrs_portfolio_active'] = 'Portfolio events'; + +$strings['NoActivities'] = 'No activities added yet'; +$strings['ActivityTitle'] = 'Activity'; +$strings['AddActivity'] = 'Add activity'; +$strings['TinCanPackage'] = 'TinCan package (zip)'; +$strings['Cmi5Package'] = 'Cmi5 package (zip)'; +$strings['OnlyZipAllowed'] = 'Only ZIP file allowed (.zip).'; +$strings['ActivityImported'] = 'Activity imported.'; +$strings['EditActivity'] = 'Edit activity'; +$strings['ActivityUpdated'] = 'Activity updated'; +$strings['ActivityLaunchUrl'] = 'Launch URL'; +$strings['ActivityId'] = 'Activity ID'; +$strings['ActivityType'] = 'Activity type'; +$strings['ActivityDeleted'] = 'Activity deleted'; +$strings['ActivityLaunch'] = 'Launch'; +$strings['ActivityFirstLaunch'] = 'First launch at'; +$strings['ActivityLastLaunch'] = 'Last launch at'; +$strings['LaunchNewAttempt'] = 'Launch new attempt'; +$strings['LrsConfiguration'] = 'LRS Configuration'; +$strings['Verb'] = 'Verb'; +$strings['Actor'] = 'Actor'; +$strings['ToolTinCan'] = 'Activities'; +$strings['Terminated'] = 'Terminated'; +$strings['Completed'] = 'Completed'; +$strings['Answered'] = 'Answered'; +$strings['Viewed'] = 'Viewed'; +$strings['ActivityAddedToLPCannotBeAccessed'] = 'This activity has been included in a learning path, so it cannot be accessed by students directly from here.'; +$strings['XApiPackage'] = 'XApi Package'; +$strings['TinCanAllowMultipleAttempts'] = 'Allow multiple attempts'; diff --git a/public/plugin/xapi/lang/french.php b/public/plugin/xapi/lang/french.php new file mode 100644 index 00000000000..d53ea9794e2 --- /dev/null +++ b/public/plugin/xapi/lang/french.php @@ -0,0 +1,49 @@ +Cette valeur est générée automatiquement par Chamilo, ne la modifiez pas.'; +$strings['lrs_url'] = 'Point d\'entrée LRS'; +$strings['lrs_url_help'] = 'URL de base du LRS'; +$strings['lrs_auth_username'] = 'Utilisateur LRS'; +$strings['lrs_auth_username_help'] = 'Nom d\'utilisateur pour l\'authentification HTTP de base'; +$strings['lrs_auth_password'] = 'Mot de passe LRS'; +$strings['lrs_auth_password_help'] = 'Mot de passe pour l\'authentification HTTP de base'; +$strings['cron_lrs_url'] = 'Cron: LRS endpoint'; +$strings['cron_lrs_url_help'] = 'Alternative base URL of the LRS for the cron process'; +$strings['cron_lrs_auth_username'] = 'Cron: LRS user'; +$strings['cron_lrs_auth_username_help'] = 'Alternative username for basic HTTP authentication for the cron process'; +$strings['cron_lrs_auth_password'] = 'Cron: LRS password'; +$strings['cron_lrs_auth_password_help'] = 'Alternative password for basic HTTP authentication for the cron process'; +$strings['lrs_lp_item_viewed_active'] = 'Élément de parcours visionné'; +$strings['lrs_lp_end_active'] = 'Parcours terminé'; +$strings['lrs_quiz_active'] = 'Exercice terminé'; +$strings['lrs_quiz_question_active'] = 'Question d\'exercice répondue'; +$strings['lrs_portfolio_active'] = 'Événements de portfolio'; + +$strings['NoActivities'] = 'Aucune activité ajoutée pour l\'instant'; +$strings['ActivityTitle'] = 'Activité'; +$strings['AddActivity'] = 'Ajouter activité'; +$strings['TinCanPackage'] = 'Paquet TinCan (zip)'; +$strings['OnlyZipAllowed'] = 'Seuls les fichiers ZIP sont autorisés (.zip).'; +$strings['ActivityImported'] = 'Activité importée.'; +$strings['EditActivity'] = 'Éditer activité'; +$strings['ActivityUpdated'] = 'Activité mise à jour'; +$strings['ActivityLaunchUrl'] = 'URL de lancement'; +$strings['ActivityId'] = 'ID d\'activité'; +$strings['ActivityType'] = 'Type d\'activité'; +$strings['ActivityDeleted'] = 'Activité supprimée'; +$strings['ActivityLaunch'] = 'Lancer'; +$strings['ActivityFirstLaunch'] = 'Premier lancement à'; +$strings['ActivityLastLaunch'] = 'Dernier lancement à'; +$strings['LaunchNewAttempt'] = 'Lancer nouvelle tentative'; +$strings['LrsConfiguration'] = 'Configuration LRS'; +$strings['Verb'] = 'Verbe'; +$strings['Actor'] = 'Acteur'; +$strings['ToolTinCan'] = 'Activités'; +$strings['ActivityAddedToLPCannotBeAccessed'] = 'Cet activité fait partie d\'un parcours d\'apprentissage, il n\'est donc pas accessible par les étudiants depuis cette page'; diff --git a/public/plugin/xapi/lang/spanish.php b/public/plugin/xapi/lang/spanish.php new file mode 100644 index 00000000000..2056da671a6 --- /dev/null +++ b/public/plugin/xapi/lang/spanish.php @@ -0,0 +1,56 @@ +Esto es generado automáticamente por Chamilo LMS. No reemplazarlo.'; +$strings['lrs_url'] = 'LRS endpoint'; +$strings['lrs_url_help'] = 'Base de la URL del LRS'; +$strings['lrs_auth_username'] = 'Usuario del LRS'; +$strings['lrs_auth_username_help'] = 'Usuario para autenticación con HTTP básica'; +$strings['lrs_auth_password'] = 'Contraseña del LRS'; +$strings['lrs_auth_password_help'] = 'Contraseña para autenticación con HTTP básica'; +$strings['cron_lrs_url'] = 'Cron: LRS endpoint'; +$strings['cron_lrs_url_help'] = 'Opcional. Base de la URL alternativa del LRS del proceso cron.'; +$strings['cron_lrs_auth_username'] = 'Cron: Usuario del LRS'; +$strings['cron_lrs_auth_username_help'] = 'Opcional. Usuario alternativo para autenticación con HTTP básica del proceso cron'; +$strings['cron_lrs_auth_password'] = 'Cron: Contraseña del LRS'; +$strings['cron_lrs_auth_password_help'] = 'Opcional. Contraseña alternativa para autenticación con HTTP básica del proceso cron'; +$strings['lrs_lp_item_viewed_active'] = 'Visualización de contenido de lección'; +$strings['lrs_lp_end_active'] = 'Finalización de lección'; +$strings['lrs_quiz_active'] = 'Finalización de ejercicio'; +$strings['lrs_quiz_question_active'] = 'Resolución de pregunta en ejercicio'; +$strings['lrs_portfolio_active'] = 'Eventos en portafolio'; + +$strings['NoActivities'] = 'No hay actividades aún'; +$strings['ActivityTitle'] = 'Actividad'; +$strings['AddActivity'] = 'Agregar actividad'; +$strings['TinCanPackage'] = 'Paquete TinCan (zip)'; +$strings['Cmi5Package'] = 'Paquete Cmi5(zip)'; +$strings['OnlyZipAllowed'] = 'Sólo archivos ZIP están permitidos (.zip).'; +$strings['ActivityImported'] = 'Actividad importada.'; +$strings['EditActivity'] = 'Editar actividad'; +$strings['ActivityUpdated'] = 'Actividad actualizada'; +$strings['ActivityLaunchUrl'] = 'URL de inicio'; +$strings['ActivityId'] = 'ID de actividad'; +$strings['ActivityType'] = 'Tipo de actividad'; +$strings['ActivityDeleted'] = 'Actividad eliminada'; +$strings['ActivityLaunch'] = 'Iniciar'; +$strings['ActivityFirstLaunch'] = 'Primer inicio'; +$strings['ActivityLastLaunch'] = 'Últimmo inicio'; +$strings['LaunchNewAttempt'] = 'Iniciar nuevo intento'; +$strings['LrsConfiguration'] = 'Configuración de LRS'; +$strings['Verb'] = 'Verbo'; +$strings['Actor'] = 'Actor'; +$strings['ToolTinCan'] = 'Actividades'; +$strings['Terminated'] = 'Terminó'; +$strings['Completed'] = 'Completó'; +$strings['Answered'] = 'Respondió'; +$strings['Viewed'] = 'Visualizó'; +$strings['ActivityAddedToLPCannotBeAccessed'] = 'Esta actividad ha sido incluida en una secuencia de aprendizaje, por lo cual no podrá ser accesible directamente por los estudiantes desde aquí.'; +$strings['XApiPackage'] = 'Paquete XApi'; +$strings['TinCanAllowMultipleAttempts'] = 'Permitir múltiples intentos'; diff --git a/public/plugin/xapi/lrs.php b/public/plugin/xapi/lrs.php new file mode 100644 index 00000000000..4fe9df4fdd7 --- /dev/null +++ b/public/plugin/xapi/lrs.php @@ -0,0 +1,12 @@ +send(); diff --git a/public/plugin/xapi/php-xapi/lrs-bundle/LICENSE b/public/plugin/xapi/php-xapi/lrs-bundle/LICENSE new file mode 100644 index 00000000000..de1d823c3d3 --- /dev/null +++ b/public/plugin/xapi/php-xapi/lrs-bundle/LICENSE @@ -0,0 +1,19 @@ +Copyright (c) 2016-2017 Christian Flothmann + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is furnished +to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/public/plugin/xapi/php-xapi/lrs-bundle/README.md b/public/plugin/xapi/php-xapi/lrs-bundle/README.md new file mode 100644 index 00000000000..e69de29bb2d diff --git a/public/plugin/xapi/php-xapi/lrs-bundle/composer.json b/public/plugin/xapi/php-xapi/lrs-bundle/composer.json new file mode 100644 index 00000000000..874027f0203 --- /dev/null +++ b/public/plugin/xapi/php-xapi/lrs-bundle/composer.json @@ -0,0 +1,51 @@ +{ + "name": "php-xapi/lrs-bundle", + "type": "symfony-bundle", + "description": "Experience API (xAPI) Learning Record Store (LRS) based on the Symfony Framework", + "keywords": ["xAPI", "Experience API", "Tin Can API", "LRS", "Learning Record Store", "Symfony", "bundle"], + "homepage": "https://github.com/php-xapi/lrs-bundle", + "license": "MIT", + "authors": [ + { + "name": "Christian Flothmann", + "homepage": "https://github.com/xabbuh" + }, + { + "name": "Jérôme Parmentier", + "homepage": "https://github.com/Lctrs" + } + ], + "require": { + "php": "^7.1", + "php-xapi/exception": "^0.1 || ^0.2", + "php-xapi/model": "^1.1 || ^2.0 || ^3.0", + "php-xapi/repository-api": "^0.3@dev || ^0.4@dev", + "php-xapi/serializer": "^1.0 || ^2.0", + "php-xapi/symfony-serializer": "^1.0 || ^2.0", + "symfony/config": "^3.4 || ^4.3", + "symfony/dependency-injection": "^3.4 || ^4.3", + "symfony/http-foundation": "^3.4 || ^4.3", + "symfony/http-kernel": "^3.4 || ^4.3" + }, + "require-dev": { + "phpspec/phpspec": "~2.3", + "php-xapi/json-test-fixtures": "^1.0 || ^2.0", + "php-xapi/test-fixtures": "^1.0.1", + "ramsey/uuid": "^2.9 || ^3.0" + }, + "autoload": { + "psr-4": { + "XApi\\LrsBundle\\": "src/" + } + }, + "autoload-dev": { + "psr-4": { + "spec\\XApi\\LrsBundle\\": "spec/" + } + }, + "extra": { + "branch-alias": { + "dev-master": "0.1.x-dev" + } + } +} diff --git a/public/plugin/xapi/php-xapi/lrs-bundle/src/Controller/StatementGetController.php b/public/plugin/xapi/php-xapi/lrs-bundle/src/Controller/StatementGetController.php new file mode 100644 index 00000000000..88469b3371d --- /dev/null +++ b/public/plugin/xapi/php-xapi/lrs-bundle/src/Controller/StatementGetController.php @@ -0,0 +1,209 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace XApi\LrsBundle\Controller; + +use Symfony\Component\HttpFoundation\JsonResponse; +use Symfony\Component\HttpFoundation\ParameterBag; +use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\HttpFoundation\Response; +use Symfony\Component\HttpKernel\Exception\BadRequestHttpException; +use Xabbuh\XApi\Common\Exception\NotFoundException; +use Xabbuh\XApi\Model\IRL; +use Xabbuh\XApi\Model\Statement; +use Xabbuh\XApi\Model\StatementId; +use Xabbuh\XApi\Model\StatementResult; +use Xabbuh\XApi\Serializer\StatementResultSerializerInterface; +use Xabbuh\XApi\Serializer\StatementSerializerInterface; +use XApi\LrsBundle\Model\StatementsFilterFactory; +use XApi\LrsBundle\Response\AttachmentResponse; +use XApi\LrsBundle\Response\MultipartResponse; +use XApi\Repository\Api\StatementRepositoryInterface; + +/** + * @author Jérôme Parmentier + */ +class StatementGetController +{ + protected static $getParameters = [ + 'statementId' => true, + 'voidedStatementId' => true, + 'agent' => true, + 'verb' => true, + 'activity' => true, + 'registration' => true, + 'related_activities' => true, + 'related_agents' => true, + 'since' => true, + 'until' => true, + 'limit' => true, + 'format' => true, + 'attachments' => true, + 'ascending' => true, + 'cursor' => true, + ]; + + protected $repository; + protected $statementSerializer; + protected $statementResultSerializer; + protected $statementsFilterFactory; + + public function __construct(StatementRepositoryInterface $repository, StatementSerializerInterface $statementSerializer, StatementResultSerializerInterface $statementResultSerializer, StatementsFilterFactory $statementsFilterFactory) + { + $this->repository = $repository; + $this->statementSerializer = $statementSerializer; + $this->statementResultSerializer = $statementResultSerializer; + $this->statementsFilterFactory = $statementsFilterFactory; + } + + /** + * @throws BadRequestHttpException if the query parameters does not comply with xAPI specification + * + * @return Response + */ + public function getStatement(Request $request) + { + $query = new ParameterBag(\array_intersect_key($request->query->all(), self::$getParameters)); + + $this->validate($query); + + $includeAttachments = $query->filter('attachments', false, FILTER_VALIDATE_BOOLEAN); + try { + if (($statementId = $query->get('statementId')) !== null) { + $statement = $this->repository->findStatementById(StatementId::fromString($statementId)); + + $response = $this->buildSingleStatementResponse($statement, $includeAttachments); + } elseif (($voidedStatementId = $query->get('voidedStatementId')) !== null) { + $statement = $this->repository->findVoidedStatementById(StatementId::fromString($voidedStatementId)); + + $response = $this->buildSingleStatementResponse($statement, $includeAttachments); + } else { + $statements = $this->repository->findStatementsBy($this->statementsFilterFactory->createFromParameterBag($query)); + + $response = $this->buildMultiStatementsResponse($statements, $query, $includeAttachments); + } + } catch (NotFoundException $e) { + $response = $this->buildMultiStatementsResponse([], $query) + ->setStatusCode(Response::HTTP_NOT_FOUND) + ->setContent(''); + } catch (\Exception $exception) { + $response = Response::create('', Response::HTTP_BAD_REQUEST); + } + + $now = new \DateTime(); + $response->headers->set('X-Experience-API-Consistent-Through', $now->format(\DateTime::ATOM)); + $response->headers->set('Content-Type', 'application/json'); + + return $response; + } + + /** + * @param bool $includeAttachments true to include the attachments in the response, false otherwise + * + * @return JsonResponse|MultipartResponse + */ + protected function buildSingleStatementResponse(Statement $statement, $includeAttachments = false) + { + $json = $this->statementSerializer->serializeStatement($statement); + + $response = new Response($json, 200); + + if ($includeAttachments) { + $response = $this->buildMultipartResponse($response, [$statement]); + } + + $response->setLastModified($statement->getStored()); + + return $response; + } + + /** + * @param Statement[] $statements + * @param bool $includeAttachments true to include the attachments in the response, false otherwise + * + * @return JsonResponse|MultipartResponse + */ + protected function buildMultiStatementsResponse(array $statements, ParameterBag $query, $includeAttachments = false) + { + $moreUrlPath = $statements ? $this->generateMoreIrl($query) : null; + + $json = $this->statementResultSerializer->serializeStatementResult( + new StatementResult($statements, $moreUrlPath) + ); + + $response = new Response($json, 200); + + if ($includeAttachments) { + $response = $this->buildMultipartResponse($response, $statements); + } + + return $response; + } + + /** + * @param Statement[] $statements + * + * @return MultipartResponse + */ + protected function buildMultipartResponse(JsonResponse $statementResponse, array $statements) + { + $attachmentsParts = []; + + foreach ($statements as $statement) { + foreach ((array) $statement->getAttachments() as $attachment) { + $attachmentsParts[] = new AttachmentResponse($attachment); + } + } + + return new MultipartResponse($statementResponse, $attachmentsParts); + } + + /** + * Validate the parameters. + * + * @throws BadRequestHttpException if the parameters does not comply with the xAPI specification + */ + protected function validate(ParameterBag $query) + { + $hasStatementId = $query->has('statementId'); + $hasVoidedStatementId = $query->has('voidedStatementId'); + + if ($hasStatementId && $hasVoidedStatementId) { + throw new BadRequestHttpException('Request must not have both statementId and voidedStatementId parameters at the same time.'); + } + + $hasAttachments = $query->has('attachments'); + $hasFormat = $query->has('format'); + $queryCount = $query->count(); + + if (($hasStatementId || $hasVoidedStatementId) && $hasAttachments && $hasFormat && $queryCount > 3) { + throw new BadRequestHttpException('Request must not contain statementId or voidedStatementId parameters, and also any other parameter besides "attachments" or "format".'); + } + + if (($hasStatementId || $hasVoidedStatementId) && ($hasAttachments || $hasFormat) && $queryCount > 2) { + throw new BadRequestHttpException('Request must not contain statementId or voidedStatementId parameters, and also any other parameter besides "attachments" or "format".'); + } + + if (($hasStatementId || $hasVoidedStatementId) && $queryCount > 1) { + throw new BadRequestHttpException('Request must not contain statementId or voidedStatementId parameters, and also any other parameter besides "attachments" or "format".'); + } + } + + protected function generateMoreIrl(ParameterBag $query): IRL + { + $params = $query->all(); + $params['cursor'] = empty($params['cursor']) ? 1 : $params['cursor'] + 1; + + return IRL::fromString( + '/plugin/xapi/lrs.php/statements?'.http_build_query($params) + ); + } +} diff --git a/public/plugin/xapi/php-xapi/lrs-bundle/src/Controller/StatementHeadController.php b/public/plugin/xapi/php-xapi/lrs-bundle/src/Controller/StatementHeadController.php new file mode 100644 index 00000000000..224a441850a --- /dev/null +++ b/public/plugin/xapi/php-xapi/lrs-bundle/src/Controller/StatementHeadController.php @@ -0,0 +1,27 @@ +setContent(''); + } +} diff --git a/public/plugin/xapi/php-xapi/lrs-bundle/src/Controller/StatementPostController.php b/public/plugin/xapi/php-xapi/lrs-bundle/src/Controller/StatementPostController.php new file mode 100644 index 00000000000..d4262541734 --- /dev/null +++ b/public/plugin/xapi/php-xapi/lrs-bundle/src/Controller/StatementPostController.php @@ -0,0 +1,67 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace XApi\LrsBundle\Controller; + +use Symfony\Component\HttpFoundation\JsonResponse; +use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\HttpKernel\Exception\ConflictHttpException; +use Xabbuh\XApi\Common\Exception\NotFoundException; +use Xabbuh\XApi\Model\Statement; +use XApi\Repository\Api\StatementRepositoryInterface; + +/** + * @author Jérôme Parmentier + */ +final class StatementPostController +{ + /** + * @var StatementRepositoryInterface + */ + private $repository; + + public function __construct(StatementRepositoryInterface $repository) + { + $this->repository = $repository; + } + + public function postStatements(Request $request, array $statements): JsonResponse + { + $statementsToStore = []; + + /** @var Statement $statement */ + foreach ($statements as $statement) { + if (null === $statementId = $statement->getId()) { + $statementsToStore[] = $statement; + + continue; + } + + try { + $existingStatement = $this->repository->findStatementById($statement->getId()); + + if (!$existingStatement->equals($statement)) { + throw new ConflictHttpException('The new statement is not equal to an existing statement with the same id.'); + } + } catch (NotFoundException $e) { + $statementsToStore[] = $statement; + } + } + + $uuids = []; + + foreach ($statementsToStore as $statement) { + $uuids[] = $this->repository->storeStatement($statement, true)->getValue(); + } + + return new JsonResponse($uuids); + } +} diff --git a/public/plugin/xapi/php-xapi/lrs-bundle/src/Controller/StatementPutController.php b/public/plugin/xapi/php-xapi/lrs-bundle/src/Controller/StatementPutController.php new file mode 100644 index 00000000000..660f99b3696 --- /dev/null +++ b/public/plugin/xapi/php-xapi/lrs-bundle/src/Controller/StatementPutController.php @@ -0,0 +1,65 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace XApi\LrsBundle\Controller; + +use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\HttpFoundation\Response; +use Symfony\Component\HttpKernel\Exception\BadRequestHttpException; +use Symfony\Component\HttpKernel\Exception\ConflictHttpException; +use Xabbuh\XApi\Common\Exception\NotFoundException; +use Xabbuh\XApi\Model\Statement; +use Xabbuh\XApi\Model\StatementId; +use XApi\Repository\Api\StatementRepositoryInterface; + +/** + * @author Christian Flothmann + */ +final class StatementPutController +{ + private $repository; + + public function __construct(StatementRepositoryInterface $repository) + { + $this->repository = $repository; + } + + public function putStatement(Request $request, Statement $statement): Response + { + if (null === $statementId = $request->query->get('statementId')) { + throw new BadRequestHttpException('Required statementId parameter is missing.'); + } + + try { + $id = StatementId::fromString($statementId); + } catch (\InvalidArgumentException $e) { + throw new BadRequestHttpException(sprintf('Parameter statementId ("%s") is not a valid UUID.', $statementId), $e); + } + + if (null !== $statement->getId() && !$id->equals($statement->getId())) { + throw new ConflictHttpException(sprintf('Id parameter ("%s") and statement id ("%s") do not match.', $id->getValue(), $statement->getId()->getValue())); + } + + try { + $existingStatement = $this->repository->findStatementById($id); + + if (!$existingStatement->equals($statement)) { + throw new ConflictHttpException('The new statement is not equal to an existing statement with the same id.'); + } + } catch (NotFoundException $e) { + $statement = $statement->withId($id); + + $this->repository->storeStatement($statement, true); + } + + return new Response('', 204); + } +} diff --git a/public/plugin/xapi/php-xapi/lrs-bundle/src/DependencyInjection/Configuration.php b/public/plugin/xapi/php-xapi/lrs-bundle/src/DependencyInjection/Configuration.php new file mode 100644 index 00000000000..40cfa87558c --- /dev/null +++ b/public/plugin/xapi/php-xapi/lrs-bundle/src/DependencyInjection/Configuration.php @@ -0,0 +1,35 @@ + + */ +final class Configuration implements ConfigurationInterface +{ + public function getConfigTreeBuilder() + { + $treeBuilder = new TreeBuilder(); + + $treeBuilder + ->root('xapi_lrs') + ->beforeNormalization() + ->ifTrue(function ($v) { return isset($v['type']) && in_array($v['type'], ['mongodb', 'orm']) && !isset($v['object_manager_service']); }) + ->thenInvalid('You need to configure the object manager service when the repository type is "mongodb" or orm".') + ->end() + ->children() + ->enumNode('type') + ->isRequired() + ->values(['in_memory', 'mongodb', 'orm']) + ->end() + ->scalarNode('object_manager_service')->end() + ->end() + ->end() + ; + + return $treeBuilder; + } +} diff --git a/public/plugin/xapi/php-xapi/lrs-bundle/src/DependencyInjection/XApiLrsExtension.php b/public/plugin/xapi/php-xapi/lrs-bundle/src/DependencyInjection/XApiLrsExtension.php new file mode 100644 index 00000000000..e2ff9e6d190 --- /dev/null +++ b/public/plugin/xapi/php-xapi/lrs-bundle/src/DependencyInjection/XApiLrsExtension.php @@ -0,0 +1,60 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace XApi\LrsBundle\DependencyInjection; + +use Symfony\Component\Config\FileLocator; +use Symfony\Component\DependencyInjection\ContainerBuilder; +use Symfony\Component\DependencyInjection\Extension\Extension; +use Symfony\Component\DependencyInjection\Loader\XmlFileLoader; + +/** + * @author Christian Flothmann + */ +final class XApiLrsExtension extends Extension +{ + public function load(array $configs, ContainerBuilder $container) + { + $configuration = new Configuration(); + $config = $this->processConfiguration($configuration, $configs); + + $loader = new XmlFileLoader($container, new FileLocator(__DIR__.'/../Resources/config')); + + $loader->load('controller.xml'); + $loader->load('event_listener.xml'); + $loader->load('factory.xml'); + $loader->load('serializer.xml'); + + switch ($config['type']) { + case 'in_memory': + break; + case 'mongodb': + $loader->load('doctrine.xml'); + $loader->load('mongodb.xml'); + + $container->setAlias('xapi_lrs.doctrine.object_manager', $config['object_manager_service']); + $container->setAlias('xapi_lrs.repository.statement', 'xapi_lrs.repository.statement.doctrine'); + break; + case 'orm': + $loader->load('doctrine.xml'); + $loader->load('orm.xml'); + + $container->setAlias('xapi_lrs.doctrine.object_manager', $config['object_manager_service']); + $container->setAlias('xapi_lrs.repository.statement', 'xapi_lrs.repository.statement.doctrine'); + break; + } + } + + public function getAlias() + { + return 'xapi_lrs'; + } +} diff --git a/public/plugin/xapi/php-xapi/lrs-bundle/src/EventListener/AlternateRequestSyntaxListener.php b/public/plugin/xapi/php-xapi/lrs-bundle/src/EventListener/AlternateRequestSyntaxListener.php new file mode 100644 index 00000000000..ef4a5a1eacf --- /dev/null +++ b/public/plugin/xapi/php-xapi/lrs-bundle/src/EventListener/AlternateRequestSyntaxListener.php @@ -0,0 +1,73 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace XApi\LrsBundle\EventListener; + +use Symfony\Component\HttpKernel\Event\GetResponseEvent; +use Symfony\Component\HttpKernel\Exception\BadRequestHttpException; + +/** + * @author Jérôme Parmentier + */ +class AlternateRequestSyntaxListener +{ + public function onKernelRequest(GetResponseEvent $event) + { + if (!$event->isMasterRequest()) { + return; + } + + $request = $event->getRequest(); + + if (!$request->attributes->has('xapi_lrs.route')) { + return; + } + + if ('POST' !== $request->getMethod()) { + return; + } + + if (null === $method = $request->query->get('method')) { + return; + } + + if ($request->query->count() > 1) { + throw new BadRequestHttpException('Including other query parameters than "method" is not allowed. You have to send them as POST parameters inside the request body.'); + } + + $request->setMethod($method); + $request->query->remove('method'); + + if (null !== $content = $request->request->get('content')) { + $request->request->remove('content'); + + $request->initialize( + $request->query->all(), + $request->request->all(), + $request->attributes->all(), + $request->cookies->all(), + $request->files->all(), + $request->server->all(), + $content + ); + } + + foreach ($request->request as $key => $value) { + if (in_array($key, ['Authorization', 'X-Experience-API-Version', 'Content-Type', 'Content-Length', 'If-Match', 'If-None-Match'], true)) { + $request->headers->set($key, $value); + } else { + $request->query->set($key, $value); + } + + $request->request->remove($key); + } + } +} diff --git a/public/plugin/xapi/php-xapi/lrs-bundle/src/EventListener/ExceptionListener.php b/public/plugin/xapi/php-xapi/lrs-bundle/src/EventListener/ExceptionListener.php new file mode 100644 index 00000000000..956f2f27e98 --- /dev/null +++ b/public/plugin/xapi/php-xapi/lrs-bundle/src/EventListener/ExceptionListener.php @@ -0,0 +1,17 @@ + + */ +class ExceptionListener +{ + public function onKernelException(GetResponseForExceptionEvent $event) + { + } +} diff --git a/public/plugin/xapi/php-xapi/lrs-bundle/src/EventListener/SerializerListener.php b/public/plugin/xapi/php-xapi/lrs-bundle/src/EventListener/SerializerListener.php new file mode 100644 index 00000000000..3bdda2f66b9 --- /dev/null +++ b/public/plugin/xapi/php-xapi/lrs-bundle/src/EventListener/SerializerListener.php @@ -0,0 +1,40 @@ + + */ +class SerializerListener +{ + private $statementSerializer; + + public function __construct(StatementSerializerInterface $statementSerializer) + { + $this->statementSerializer = $statementSerializer; + } + + public function onKernelRequest(GetResponseEvent $event) + { + $request = $event->getRequest(); + + if (!$request->attributes->has('xapi_lrs.route')) { + return; + } + + try { + switch ($request->attributes->get('xapi_serializer')) { + case 'statement': + $request->attributes->set('statement', $this->statementSerializer->deserializeStatement($request->getContent())); + break; + } + } catch (BaseSerializerException $e) { + throw new BadRequestHttpException(sprintf('The content of the request cannot be deserialized into a valid xAPI %s.', $request->attributes->get('xapi_serializer')), $e); + } + } +} diff --git a/public/plugin/xapi/php-xapi/lrs-bundle/src/EventListener/VersionListener.php b/public/plugin/xapi/php-xapi/lrs-bundle/src/EventListener/VersionListener.php new file mode 100644 index 00000000000..f82af7595ed --- /dev/null +++ b/public/plugin/xapi/php-xapi/lrs-bundle/src/EventListener/VersionListener.php @@ -0,0 +1,66 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace XApi\LrsBundle\EventListener; + +use Symfony\Component\HttpKernel\Event\FilterResponseEvent; +use Symfony\Component\HttpKernel\Event\GetResponseEvent; +use Symfony\Component\HttpKernel\Exception\BadRequestHttpException; + +/** + * @author Jérôme Parmentier + */ +class VersionListener +{ + public function onKernelRequest(GetResponseEvent $event) + { + if (!$event->isMasterRequest()) { + return; + } + + $request = $event->getRequest(); + + if (!$request->attributes->has('xapi_lrs.route')) { + return; + } + + if (null === $version = $request->headers->get('X-Experience-API-Version')) { + throw new BadRequestHttpException('Missing required "X-Experience-API-Version" header.'); + } + + if (preg_match('/^1\.0(?:\.\d+)?$/', $version)) { + if ('1.0' === $version) { + $request->headers->set('X-Experience-API-Version', '1.0.0'); + } + + return; + } + + throw new BadRequestHttpException(sprintf('xAPI version "%s" is not supported.', $version)); + } + + public function onKernelResponse(FilterResponseEvent $event) + { + if (!$event->isMasterRequest()) { + return; + } + + if (!$event->getRequest()->attributes->has('xapi_lrs.route')) { + return; + } + + $headers = $event->getResponse()->headers; + + if (!$headers->has('X-Experience-API-Version')) { + $headers->set('X-Experience-API-Version', '1.0.3'); + } + } +} diff --git a/public/plugin/xapi/php-xapi/lrs-bundle/src/Model/StatementsFilterFactory.php b/public/plugin/xapi/php-xapi/lrs-bundle/src/Model/StatementsFilterFactory.php new file mode 100644 index 00000000000..7e1ba67f46c --- /dev/null +++ b/public/plugin/xapi/php-xapi/lrs-bundle/src/Model/StatementsFilterFactory.php @@ -0,0 +1,86 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace XApi\LrsBundle\Model; + +use Symfony\Component\HttpFoundation\ParameterBag; +use Xabbuh\XApi\Model\Activity; +use Xabbuh\XApi\Model\IRI; +use Xabbuh\XApi\Model\StatementsFilter; +use Xabbuh\XApi\Model\Verb; +use Xabbuh\XApi\Serializer\ActorSerializerInterface; + +/** + * @author Jérôme Parmentier + */ +class StatementsFilterFactory +{ + private $actorSerializer; + + public function __construct(ActorSerializerInterface $actorSerializer) + { + $this->actorSerializer = $actorSerializer; + } + + /** + * @return StatementsFilter + */ + public function createFromParameterBag(ParameterBag $parameters) + { + $filter = new StatementsFilter(); + + if (($actor = $parameters->get('agent')) !== null) { + $filter->byActor($this->actorSerializer->deserializeActor($actor)); + } + + if (($verbId = $parameters->get('verb')) !== null) { + $filter->byVerb(new Verb(IRI::fromString($verbId))); + } + + if (($activityId = $parameters->get('activity')) !== null) { + $filter->byActivity(new Activity(IRI::fromString($activityId))); + } + + if (($registration = $parameters->get('registration')) !== null) { + $filter->byRegistration($registration); + } + + if ($parameters->filter('related_activities', false, FILTER_VALIDATE_BOOLEAN)) { + $filter->enableRelatedActivityFilter(); + } else { + $filter->disableRelatedActivityFilter(); + } + + if ($parameters->filter('related_agents', false, FILTER_VALIDATE_BOOLEAN)) { + $filter->enableRelatedAgentFilter(); + } else { + $filter->disableRelatedAgentFilter(); + } + + if (($since = $parameters->get('since')) !== null) { + $filter->since(\DateTime::createFromFormat(\DateTime::ATOM, $since)); + } + + if (($until = $parameters->get('until')) !== null) { + $filter->until(\DateTime::createFromFormat(\DateTime::ATOM, $until)); + } + + if ($parameters->filter('ascending', false, FILTER_VALIDATE_BOOLEAN)) { + $filter->ascending(); + } else { + $filter->descending(); + } + + $filter->limit($parameters->getInt('limit')); + + return $filter; + } +} diff --git a/public/plugin/xapi/php-xapi/lrs-bundle/src/Resources/config/controller.xml b/public/plugin/xapi/php-xapi/lrs-bundle/src/Resources/config/controller.xml new file mode 100644 index 00000000000..053e487dfed --- /dev/null +++ b/public/plugin/xapi/php-xapi/lrs-bundle/src/Resources/config/controller.xml @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + + + + + diff --git a/public/plugin/xapi/php-xapi/lrs-bundle/src/Resources/config/doctrine.xml b/public/plugin/xapi/php-xapi/lrs-bundle/src/Resources/config/doctrine.xml new file mode 100644 index 00000000000..1eb2760b65f --- /dev/null +++ b/public/plugin/xapi/php-xapi/lrs-bundle/src/Resources/config/doctrine.xml @@ -0,0 +1,12 @@ + + + + + + + + + diff --git a/public/plugin/xapi/php-xapi/lrs-bundle/src/Resources/config/event_listener.xml b/public/plugin/xapi/php-xapi/lrs-bundle/src/Resources/config/event_listener.xml new file mode 100644 index 00000000000..08ad38c13a5 --- /dev/null +++ b/public/plugin/xapi/php-xapi/lrs-bundle/src/Resources/config/event_listener.xml @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/public/plugin/xapi/php-xapi/lrs-bundle/src/Resources/config/factory.xml b/public/plugin/xapi/php-xapi/lrs-bundle/src/Resources/config/factory.xml new file mode 100644 index 00000000000..4c43d9db121 --- /dev/null +++ b/public/plugin/xapi/php-xapi/lrs-bundle/src/Resources/config/factory.xml @@ -0,0 +1,12 @@ + + + + + + + + + diff --git a/public/plugin/xapi/php-xapi/lrs-bundle/src/Resources/config/orm.xml b/public/plugin/xapi/php-xapi/lrs-bundle/src/Resources/config/orm.xml new file mode 100644 index 00000000000..a7c82047830 --- /dev/null +++ b/public/plugin/xapi/php-xapi/lrs-bundle/src/Resources/config/orm.xml @@ -0,0 +1,18 @@ + + + + + + + + + + + + diff --git a/public/plugin/xapi/php-xapi/lrs-bundle/src/Resources/config/routing.xml b/public/plugin/xapi/php-xapi/lrs-bundle/src/Resources/config/routing.xml new file mode 100644 index 00000000000..27c4d2bc101 --- /dev/null +++ b/public/plugin/xapi/php-xapi/lrs-bundle/src/Resources/config/routing.xml @@ -0,0 +1,29 @@ + + + + + xapi_lrs.controller.statement.put:putStatement + statement + + true + + + + + xapi_lrs.controller.statement.post:postStatement + statement + + true + + + + + xapi_lrs.controller.statement.get:getStatement + + true + + + diff --git a/public/plugin/xapi/php-xapi/lrs-bundle/src/Resources/config/serializer.xml b/public/plugin/xapi/php-xapi/lrs-bundle/src/Resources/config/serializer.xml new file mode 100644 index 00000000000..2ce345a97af --- /dev/null +++ b/public/plugin/xapi/php-xapi/lrs-bundle/src/Resources/config/serializer.xml @@ -0,0 +1,32 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/public/plugin/xapi/php-xapi/lrs-bundle/src/Response/AttachmentResponse.php b/public/plugin/xapi/php-xapi/lrs-bundle/src/Response/AttachmentResponse.php new file mode 100644 index 00000000000..860de13f0fa --- /dev/null +++ b/public/plugin/xapi/php-xapi/lrs-bundle/src/Response/AttachmentResponse.php @@ -0,0 +1,76 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace XApi\LrsBundle\Response; + +use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\HttpFoundation\Response; +use Xabbuh\XApi\Model\Attachment; + +/** + * @author Jérôme Parmentier + */ +class AttachmentResponse extends Response +{ + protected $attachment; + + public function __construct(Attachment $attachment) + { + parent::__construct(null); + + $this->attachment = $attachment; + } + + /** + * {@inheritdoc} + */ + public function prepare(Request $request) + { + if (!$this->headers->has('Content-Type')) { + $this->headers->set('Content-Type', $this->attachment->getContentType()); + } + + $this->headers->set('Content-Transfer-Encoding', 'binary'); + $this->headers->set('X-Experience-API-Hash', $this->attachment->getSha2()); + } + + /** + * {@inheritdoc} + * + * @throws \LogicException + */ + public function sendContent() + { + throw new \LogicException('An AttachmentResponse is only meant to be part of a multipart Response.'); + } + + /** + * {@inheritdoc} + * + * @throws \LogicException when the content is not null + */ + public function setContent($content) + { + if (null !== $content) { + throw new \LogicException('The content cannot be set on an AttachmentResponse instance.'); + } + } + + /** + * {@inheritdoc} + * + * @return string|null + */ + public function getContent() + { + return $this->attachment->getContent(); + } +} diff --git a/public/plugin/xapi/php-xapi/lrs-bundle/src/Response/MultipartResponse.php b/public/plugin/xapi/php-xapi/lrs-bundle/src/Response/MultipartResponse.php new file mode 100644 index 00000000000..a5cb9e1f5a1 --- /dev/null +++ b/public/plugin/xapi/php-xapi/lrs-bundle/src/Response/MultipartResponse.php @@ -0,0 +1,135 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace XApi\LrsBundle\Response; + +use Symfony\Component\HttpFoundation\JsonResponse; +use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\HttpFoundation\Response; + +/** + * @author Jérôme Parmentier + */ +class MultipartResponse extends Response +{ + protected $subtype; + protected $boundary; + protected $statementPart; + /** + * @var Response[] + */ + protected $parts; + + /** + * @param AttachmentResponse[] $attachmentsParts + * @param int $status + * @param string|null $subtype + */ + public function __construct(JsonResponse $statementPart, array $attachmentsParts = [], $status = 200, array $headers = [], $subtype = null) + { + parent::__construct(null, $status, $headers); + + if (null === $subtype) { + $subtype = 'mixed'; + } + + $this->subtype = $subtype; + $this->boundary = uniqid('', true); + $this->statementPart = $statementPart; + + $this->setAttachmentsParts($attachmentsParts); + } + + /** + * @return $this + */ + public function addAttachmentPart(AttachmentResponse $part) + { + if ($part->getContent() !== null) { + $this->parts[] = $part; + } + + return $this; + } + + /** + * @param AttachmentResponse[] $attachmentsParts + * + * @return $this + */ + public function setAttachmentsParts(array $attachmentsParts) + { + $this->parts = [$this->statementPart]; + + foreach ($attachmentsParts as $part) { + $this->addAttachmentPart($part); + } + + return $this; + } + + /** + * {@inheritdoc} + */ + public function prepare(Request $request) + { + foreach ($this->parts as $part) { + $part->prepare($request); + } + + $this->headers->set('Content-Type', sprintf('multipart/%s; boundary="%s"', $this->subtype, $this->boundary)); + $this->headers->set('Transfer-Encoding', 'chunked'); + + return parent::prepare($request); + } + + /** + * {@inheritdoc} + */ + public function sendContent() + { + $content = ''; + foreach ($this->parts as $part) { + $content .= sprintf('--%s', $this->boundary)."\r\n"; + $content .= $part->headers."\r\n"; + $content .= $part->getContent(); + $content .= "\r\n"; + } + + $content .= sprintf('--%s--', $this->boundary)."\r\n"; + + echo $content; + + return $this; + } + + /** + * {@inheritdoc} + * + * @throws \LogicException when the content is not null + */ + public function setContent($content) + { + if (null !== $content) { + throw new \LogicException('The content cannot be set on a MultipartResponse instance.'); + } + } + + /** + * {@inheritdoc} + * + * @return false + */ + public function getContent() + { + return false; + } +} diff --git a/public/plugin/xapi/php-xapi/lrs-bundle/src/XApiLrsBundle.php b/public/plugin/xapi/php-xapi/lrs-bundle/src/XApiLrsBundle.php new file mode 100644 index 00000000000..33d7775ff18 --- /dev/null +++ b/public/plugin/xapi/php-xapi/lrs-bundle/src/XApiLrsBundle.php @@ -0,0 +1,26 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace XApi\LrsBundle; + +use Symfony\Component\HttpKernel\Bundle\Bundle; +use XApi\LrsBundle\DependencyInjection\XApiLrsExtension; + +/** + * @author Christian Flothmann + */ +class XApiLrsBundle extends Bundle +{ + public function getContainerExtension() + { + return new XApiLrsExtension(); + } +} diff --git a/public/plugin/xapi/php-xapi/repository-doctrine-orm/LICENSE b/public/plugin/xapi/php-xapi/repository-doctrine-orm/LICENSE new file mode 100644 index 00000000000..de1d823c3d3 --- /dev/null +++ b/public/plugin/xapi/php-xapi/repository-doctrine-orm/LICENSE @@ -0,0 +1,19 @@ +Copyright (c) 2016-2017 Christian Flothmann + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is furnished +to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/public/plugin/xapi/php-xapi/repository-doctrine-orm/README.md b/public/plugin/xapi/php-xapi/repository-doctrine-orm/README.md new file mode 100644 index 00000000000..e69de29bb2d diff --git a/public/plugin/xapi/php-xapi/repository-doctrine-orm/composer.json b/public/plugin/xapi/php-xapi/repository-doctrine-orm/composer.json new file mode 100644 index 00000000000..dace61cba6d --- /dev/null +++ b/public/plugin/xapi/php-xapi/repository-doctrine-orm/composer.json @@ -0,0 +1,41 @@ +{ + "name": "php-xapi/repository-doctrine-orm", + "description": "Doctrine based ORM implementations of an Experience API (xAPI) repository", + "keywords": ["xAPI", "Tin Can API", "Experience API", "storage", "database", "repository", "entity", "Doctrine", "ORM"], + "homepage": "https://github.com/php-xapi/repository-orm/", + "license": "MIT", + "authors": [ + { + "name": "Christian Flothmann", + "homepage": "https://github.com/xabbuh" + } + ], + "require": { + "php": "^5.6 || ^7.0", + "doctrine/orm": "^2.3", + "php-xapi/repository-api": "^0.4", + "php-xapi/repository-doctrine": "^0.4" + }, + "require-dev": { + "symfony/phpunit-bridge": "^3.4 || ^4.0" + }, + "provide": { + "php-xapi/repository-implementation": "0.3" + }, + "autoload": { + "psr-4": { + "XApi\\Repository\\ORM\\": "src/" + } + }, + "autoload-dev": { + "psr-4": { + "XApi\\Repository\\ORM\\Tests\\": "tests/" + } + }, + "minimum-stability": "dev", + "extra": { + "branch-alias": { + "dev-master": "0.1.x-dev" + } + } +} diff --git a/public/plugin/xapi/php-xapi/repository-doctrine-orm/metadata/Actor.orm.xml b/public/plugin/xapi/php-xapi/repository-doctrine-orm/metadata/Actor.orm.xml new file mode 100644 index 00000000000..040148a0946 --- /dev/null +++ b/public/plugin/xapi/php-xapi/repository-doctrine-orm/metadata/Actor.orm.xml @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + diff --git a/public/plugin/xapi/php-xapi/repository-doctrine-orm/metadata/Attachment.orm.xml b/public/plugin/xapi/php-xapi/repository-doctrine-orm/metadata/Attachment.orm.xml new file mode 100644 index 00000000000..4fb517de63e --- /dev/null +++ b/public/plugin/xapi/php-xapi/repository-doctrine-orm/metadata/Attachment.orm.xml @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/public/plugin/xapi/php-xapi/repository-doctrine-orm/metadata/Context.orm.xml b/public/plugin/xapi/php-xapi/repository-doctrine-orm/metadata/Context.orm.xml new file mode 100644 index 00000000000..a33661e1cf6 --- /dev/null +++ b/public/plugin/xapi/php-xapi/repository-doctrine-orm/metadata/Context.orm.xml @@ -0,0 +1,59 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/public/plugin/xapi/php-xapi/repository-doctrine-orm/metadata/Extensions.orm.xml b/public/plugin/xapi/php-xapi/repository-doctrine-orm/metadata/Extensions.orm.xml new file mode 100644 index 00000000000..efec3463284 --- /dev/null +++ b/public/plugin/xapi/php-xapi/repository-doctrine-orm/metadata/Extensions.orm.xml @@ -0,0 +1,13 @@ + + + + + + + + + + diff --git a/public/plugin/xapi/php-xapi/repository-doctrine-orm/metadata/Result.orm.xml b/public/plugin/xapi/php-xapi/repository-doctrine-orm/metadata/Result.orm.xml new file mode 100644 index 00000000000..d1ffe53054a --- /dev/null +++ b/public/plugin/xapi/php-xapi/repository-doctrine-orm/metadata/Result.orm.xml @@ -0,0 +1,28 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/public/plugin/xapi/php-xapi/repository-doctrine-orm/metadata/Statement.orm.xml b/public/plugin/xapi/php-xapi/repository-doctrine-orm/metadata/Statement.orm.xml new file mode 100644 index 00000000000..321d5b86410 --- /dev/null +++ b/public/plugin/xapi/php-xapi/repository-doctrine-orm/metadata/Statement.orm.xml @@ -0,0 +1,62 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/public/plugin/xapi/php-xapi/repository-doctrine-orm/metadata/StatementObject.orm.xml b/public/plugin/xapi/php-xapi/repository-doctrine-orm/metadata/StatementObject.orm.xml new file mode 100644 index 00000000000..22464ce3faa --- /dev/null +++ b/public/plugin/xapi/php-xapi/repository-doctrine-orm/metadata/StatementObject.orm.xml @@ -0,0 +1,83 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/public/plugin/xapi/php-xapi/repository-doctrine-orm/metadata/Verb.orm.xml b/public/plugin/xapi/php-xapi/repository-doctrine-orm/metadata/Verb.orm.xml new file mode 100644 index 00000000000..2e2277c6d7e --- /dev/null +++ b/public/plugin/xapi/php-xapi/repository-doctrine-orm/metadata/Verb.orm.xml @@ -0,0 +1,14 @@ + + + + + + + + + + + diff --git a/public/plugin/xapi/php-xapi/repository-doctrine-orm/src/StatementRepository.php b/public/plugin/xapi/php-xapi/repository-doctrine-orm/src/StatementRepository.php new file mode 100644 index 00000000000..a551eb1d548 --- /dev/null +++ b/public/plugin/xapi/php-xapi/repository-doctrine-orm/src/StatementRepository.php @@ -0,0 +1,67 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace XApi\Repository\ORM; + +use Doctrine\ORM\EntityRepository; +use XApi\Repository\Doctrine\Mapping\Context; +use XApi\Repository\Doctrine\Mapping\Statement; +use XApi\Repository\Doctrine\Repository\Mapping\StatementRepository as BaseStatementRepository; + +/** + * @author Christian Flothmann + */ +final class StatementRepository extends EntityRepository implements BaseStatementRepository +{ + /** + * {@inheritdoc} + */ + public function findStatement(array $criteria) + { + return parent::findOneBy($criteria); + } + + /** + * {@inheritdoc} + */ + public function findStatements(array $criteria) + { + if (!empty($criteria['registration'])) { + $contexts = $this->_em->getRepository(Context::class)->findBy([ + 'registration' => $criteria['registration'], + ]); + + $criteria['context'] = $contexts; + } + + unset( + $criteria['registration'], + $criteria['related_activities'], + $criteria['related_agents'], + $criteria['ascending'], + $criteria['limit'] + ); + + return parent::findBy($criteria, ['created' => 'ASC']); + } + + /** + * {@inheritdoc} + */ + public function storeStatement(Statement $mappedStatement, $flush = true) + { + $this->_em->persist($mappedStatement); + + if ($flush) { + $this->_em->flush(); + } + } +} diff --git a/public/plugin/xapi/plugin.php b/public/plugin/xapi/plugin.php new file mode 100644 index 00000000000..0e2770b43bc --- /dev/null +++ b/public/plugin/xapi/plugin.php @@ -0,0 +1,5 @@ +get_info(); diff --git a/public/plugin/xapi/src/Entity/ActivityProfile.php b/public/plugin/xapi/src/Entity/ActivityProfile.php new file mode 100644 index 00000000000..82f07523cc9 --- /dev/null +++ b/public/plugin/xapi/src/Entity/ActivityProfile.php @@ -0,0 +1,93 @@ +id; + } + + public function setId(int $id): ActivityProfile + { + $this->id = $id; + + return $this; + } + + public function getProfileId(): string + { + return $this->profileId; + } + + public function setProfileId(string $profileId): ActivityProfile + { + $this->profileId = $profileId; + + return $this; + } + + public function getActivityId(): string + { + return $this->activityId; + } + + public function setActivityId(string $activityId): ActivityProfile + { + $this->activityId = $activityId; + + return $this; + } + + public function getDocumentData(): array + { + return $this->documentData; + } + + public function setDocumentData(array $documentData): ActivityProfile + { + $this->documentData = $documentData; + + return $this; + } +} diff --git a/public/plugin/xapi/src/Entity/ActivityState.php b/public/plugin/xapi/src/Entity/ActivityState.php new file mode 100644 index 00000000000..f63d03b66d4 --- /dev/null +++ b/public/plugin/xapi/src/Entity/ActivityState.php @@ -0,0 +1,111 @@ +id; + } + + public function setId(int $id): ActivityState + { + $this->id = $id; + + return $this; + } + + public function getStateId(): string + { + return $this->stateId; + } + + public function setStateId(string $stateId): ActivityState + { + $this->stateId = $stateId; + + return $this; + } + + public function getActivityId(): string + { + return $this->activityId; + } + + public function setActivityId(string $activityId): ActivityState + { + $this->activityId = $activityId; + + return $this; + } + + public function getAgent(): array + { + return $this->agent; + } + + public function setAgent(array $agent): ActivityState + { + $this->agent = $agent; + + return $this; + } + + public function getDocumentData(): array + { + return $this->documentData; + } + + public function setDocumentData(array $documentData): ActivityState + { + $this->documentData = $documentData; + + return $this; + } +} diff --git a/public/plugin/xapi/src/Entity/Cmi5Item.php b/public/plugin/xapi/src/Entity/Cmi5Item.php new file mode 100644 index 00000000000..c3f86deea04 --- /dev/null +++ b/public/plugin/xapi/src/Entity/Cmi5Item.php @@ -0,0 +1,372 @@ +children = new ArrayCollection(); + } + + public function getId(): int + { + return $this->id; + } + + public function setId(int $id): Cmi5Item + { + $this->id = $id; + + return $this; + } + + public function getIdentifier(): string + { + return $this->identifier; + } + + public function setIdentifier(string $identifier): Cmi5Item + { + $this->identifier = $identifier; + + return $this; + } + + public function getType(): string + { + return $this->type; + } + + public function setType(string $type): Cmi5Item + { + $this->type = $type; + + return $this; + } + + public function getTitle(): array + { + return $this->title; + } + + public function setTitle(array $title): Cmi5Item + { + $this->title = $title; + + return $this; + } + + public function getDescription(): array + { + return $this->description; + } + + public function setDescription(array $description): Cmi5Item + { + $this->description = $description; + + return $this; + } + + public function getUrl(): ?string + { + return $this->url; + } + + public function setUrl(?string $url): Cmi5Item + { + $this->url = $url; + + return $this; + } + + public function getActivityType(): ?string + { + return $this->activityType; + } + + public function setActivityType(?string $activityType): Cmi5Item + { + $this->activityType = $activityType; + + return $this; + } + + public function getLaunchMethod(): ?string + { + return $this->launchMethod; + } + + public function setLaunchMethod(?string $launchMethod): Cmi5Item + { + $this->launchMethod = $launchMethod; + + return $this; + } + + public function getMoveOn(): ?string + { + return $this->moveOn; + } + + public function setMoveOn(?string $moveOn): Cmi5Item + { + $this->moveOn = $moveOn; + + return $this; + } + + public function getMasteryScore(): ?float + { + return $this->masteryScore; + } + + public function setMasteryScore(?float $masteryScore): Cmi5Item + { + $this->masteryScore = $masteryScore; + + return $this; + } + + public function getLaunchParameters(): ?string + { + return $this->launchParameters; + } + + public function setLaunchParameters(?string $launchParameters): Cmi5Item + { + $this->launchParameters = $launchParameters; + + return $this; + } + + public function getEntitlementKey(): ?string + { + return $this->entitlementKey; + } + + public function setEntitlementKey(?string $entitlementKey): Cmi5Item + { + $this->entitlementKey = $entitlementKey; + + return $this; + } + + /** + * @return \Chamilo\PluginBundle\Entity\XApi\Cmi5Item|null + */ + public function getParent(): ?Cmi5Item + { + return $this->parent; + } + + /** + * @param \Chamilo\PluginBundle\Entity\XApi\Cmi5Item|null $parent + */ + public function setParent(?Cmi5Item $parent): Cmi5Item + { + $this->parent = $parent; + + return $this; + } + + public function getChildren(): ArrayCollection + { + return $this->children; + } + + public function setChildren(ArrayCollection $children): Cmi5Item + { + $this->children = $children; + + return $this; + } + + public function getStatus(): ?string + { + return $this->status; + } + + public function setStatus(?string $status): Cmi5Item + { + $this->status = $status; + + return $this; + } + + /** + * @return \Chamilo\PluginBundle\Entity\XApi\ToolLaunch + */ + public function getTool(): ToolLaunch + { + return $this->tool; + } + + /** + * @param \Chamilo\PluginBundle\Entity\XApi\ToolLaunch $tool + */ + public function setTool(ToolLaunch $tool): Cmi5Item + { + $this->tool = $tool; + + return $this; + } + + /** + * @return \Chamilo\PluginBundle\Entity\XApi\Cmi5Item + */ + public function getRoot(): Cmi5Item + { + return $this->root; + } +} diff --git a/public/plugin/xapi/src/Entity/InternalLog.php b/public/plugin/xapi/src/Entity/InternalLog.php new file mode 100644 index 00000000000..b9c807f94da --- /dev/null +++ b/public/plugin/xapi/src/Entity/InternalLog.php @@ -0,0 +1,227 @@ +id; + } + + public function getUser(): User + { + return $this->user; + } + + public function setUser(User $user): InternalLog + { + $this->user = $user; + + return $this; + } + + public function getStatementId(): string + { + return $this->statementId; + } + + public function setStatementId(string $statementId): InternalLog + { + $this->statementId = $statementId; + + return $this; + } + + public function getVerb(): string + { + return $this->verb; + } + + public function setVerb(string $verb): InternalLog + { + $this->verb = $verb; + + return $this; + } + + public function getObjectId(): string + { + return $this->objectId; + } + + public function setObjectId(string $objectId): InternalLog + { + $this->objectId = $objectId; + + return $this; + } + + public function getActivityName(): ?string + { + return $this->activityName; + } + + public function setActivityName(?string $activityName): InternalLog + { + $this->activityName = $activityName; + + return $this; + } + + public function getActivityDescription(): ?string + { + return $this->activityDescription; + } + + public function setActivityDescription(?string $activityDescription): InternalLog + { + $this->activityDescription = $activityDescription; + + return $this; + } + + public function getScoreScaled(): ?float + { + return $this->scoreScaled; + } + + public function setScoreScaled(?float $scoreScaled): InternalLog + { + $this->scoreScaled = $scoreScaled; + + return $this; + } + + public function getScoreRaw(): ?float + { + return $this->scoreRaw; + } + + public function setScoreRaw(?float $scoreRaw): InternalLog + { + $this->scoreRaw = $scoreRaw; + + return $this; + } + + public function getScoreMin(): ?float + { + return $this->scoreMin; + } + + public function setScoreMin(?float $scoreMin): InternalLog + { + $this->scoreMin = $scoreMin; + + return $this; + } + + public function getScoreMax(): ?float + { + return $this->scoreMax; + } + + public function setScoreMax(?float $scoreMax): InternalLog + { + $this->scoreMax = $scoreMax; + + return $this; + } + + public function getCreatedAt(): ?DateTime + { + return $this->createdAt; + } + + public function setCreatedAt(?DateTime $createdAt): InternalLog + { + $this->createdAt = $createdAt; + + return $this; + } +} diff --git a/public/plugin/xapi/src/Entity/LrsAuth.php b/public/plugin/xapi/src/Entity/LrsAuth.php new file mode 100644 index 00000000000..69cc839ea4f --- /dev/null +++ b/public/plugin/xapi/src/Entity/LrsAuth.php @@ -0,0 +1,104 @@ +id; + } + + public function getUsername(): string + { + return $this->username; + } + + public function setUsername(string $username): LrsAuth + { + $this->username = $username; + + return $this; + } + + public function getPassword(): string + { + return $this->password; + } + + public function setPassword(string $password): LrsAuth + { + $this->password = $password; + + return $this; + } + + public function isEnabled(): bool + { + return $this->enabled; + } + + public function setEnabled(bool $enabled): LrsAuth + { + $this->enabled = $enabled; + + return $this; + } + + public function getCreatedAt(): \DateTime + { + return $this->createdAt; + } + + public function setCreatedAt(\DateTime $createdAt): LrsAuth + { + $this->createdAt = $createdAt; + + return $this; + } +} diff --git a/public/plugin/xapi/src/Entity/Repository/ToolLaunchRepository.php b/public/plugin/xapi/src/Entity/Repository/ToolLaunchRepository.php new file mode 100644 index 00000000000..0446c9fc093 --- /dev/null +++ b/public/plugin/xapi/src/Entity/Repository/ToolLaunchRepository.php @@ -0,0 +1,85 @@ + $course, + 'session' => null, + ]; + + if ($session) { + $criteria['session'] = $session; + } + + return $this->findBy($criteria, $orderBy, $limit, $start); + } + + public function countByCourseAndSession(Course $course, Session $session = null, $filteredForStudent = false): int + { + $qb = $this->createQueryBuilder('tl'); + $qb->select($qb->expr()->count('tl')) + ->where($qb->expr()->eq('tl.course', ':course')) + ->setParameter('course', $course); + + if ($session) { + $qb->andWhere($qb->expr()->eq('tl.session', ':session')) + ->setParameter('session', $session); + } else { + $qb->andWhere($qb->expr()->isNull('tl.session')); + } + + if ($filteredForStudent) { + $qb + ->leftJoin( + CLpItem::class, + 'lpi', + Join::WITH, + "tl.id = lpi.path AND tl.course = lpi.cId AND lpi.itemType = 'xapi'" + ) + ->andWhere($qb->expr()->isNull('lpi.path')); + } + + $query = $qb->getQuery(); + + return (int) $query->getSingleScalarResult(); + } + + public function wasAddedInLp(ToolLaunch $toolLaunch): int + { + $qb = $this->getEntityManager()->createQueryBuilder(); + + return (int) $qb->select($qb->expr()->count('lp')) + ->from(CLp::class, 'lp') + ->innerJoin(CLpItem::class, 'lpi', Join::WITH, 'lp.id = lpi.lpId') + ->where('lpi.itemType = :type') + ->andWhere('lpi.path = :tool_id') + ->setParameter('type', TOOL_XAPI) + ->setParameter('tool_id', $toolLaunch->getId()) + ->getQuery() + ->getSingleScalarResult(); + } +} diff --git a/public/plugin/xapi/src/Entity/SharedStatement.php b/public/plugin/xapi/src/Entity/SharedStatement.php new file mode 100644 index 00000000000..5c4d81caad0 --- /dev/null +++ b/public/plugin/xapi/src/Entity/SharedStatement.php @@ -0,0 +1,105 @@ +statement = $statement; + $this->uuid = $uuid; + $this->sent = $sent; + } + + public function getId(): int + { + return $this->id; + } + + public function getUuid(): ?string + { + return $this->uuid; + } + + public function setUuid(?string $uuid): SharedStatement + { + $this->uuid = $uuid; + + return $this; + } + + public function getStatement(): array + { + return $this->statement; + } + + public function setStatement(array $statement): SharedStatement + { + $this->statement = $statement; + + return $this; + } + + public function isSent(): bool + { + return $this->sent; + } + + public function setSent(bool $sent): SharedStatement + { + $this->sent = $sent; + + return $this; + } +} diff --git a/public/plugin/xapi/src/Entity/ToolLaunch.php b/public/plugin/xapi/src/Entity/ToolLaunch.php new file mode 100644 index 00000000000..6d9d4f5b8ea --- /dev/null +++ b/public/plugin/xapi/src/Entity/ToolLaunch.php @@ -0,0 +1,295 @@ +allowMultipleAttempts = true; + $this->items = new ArrayCollection(); + } + + public function getId(): int + { + return $this->id; + } + + public function setId(int $id): ToolLaunch + { + $this->id = $id; + + return $this; + } + + public function getTitle(): string + { + return $this->title; + } + + public function setTitle(string $title): ToolLaunch + { + $this->title = $title; + + return $this; + } + + public function getDescription(): ?string + { + return $this->description; + } + + public function setDescription(?string $description): ToolLaunch + { + $this->description = $description; + + return $this; + } + + public function getCourse(): Course + { + return $this->course; + } + + public function setCourse(Course $course): ToolLaunch + { + $this->course = $course; + + return $this; + } + + public function getSession(): ?Session + { + return $this->session; + } + + public function setSession(?Session $session): ToolLaunch + { + $this->session = $session; + + return $this; + } + + public function getLaunchUrl(): string + { + return $this->launchUrl; + } + + public function setLaunchUrl(string $launchUrl): ToolLaunch + { + $this->launchUrl = $launchUrl; + + return $this; + } + + public function getActivityId(): ?string + { + return $this->activityId; + } + + public function setActivityId(?string $activityId): ToolLaunch + { + $this->activityId = $activityId; + + return $this; + } + + public function getCreatedAt(): DateTime + { + return $this->createdAt; + } + + public function setCreatedAt(DateTime $createdAt): ToolLaunch + { + $this->createdAt = $createdAt; + + return $this; + } + + public function getActivityType(): ?string + { + return $this->activityType; + } + + public function setActivityType(?string $activityType): ToolLaunch + { + $this->activityType = $activityType; + + return $this; + } + + public function isAllowMultipleAttempts(): bool + { + return $this->allowMultipleAttempts; + } + + public function setAllowMultipleAttempts(bool $allowMultipleAttempts): ToolLaunch + { + $this->allowMultipleAttempts = $allowMultipleAttempts; + + return $this; + } + + public function getLrsUrl(): ?string + { + return $this->lrsUrl; + } + + public function setLrsUrl(?string $lrsUrl): ToolLaunch + { + $this->lrsUrl = $lrsUrl; + + return $this; + } + + public function getLrsAuthUsername(): ?string + { + return $this->lrsAuthUsername; + } + + public function setLrsAuthUsername(?string $lrsAuthUsername): ToolLaunch + { + $this->lrsAuthUsername = $lrsAuthUsername; + + return $this; + } + + public function getLrsAuthPassword(): ?string + { + return $this->lrsAuthPassword; + } + + public function setLrsAuthPassword(?string $lrsAuthPassword): ToolLaunch + { + $this->lrsAuthPassword = $lrsAuthPassword; + + return $this; + } + + public function getItems(): ArrayCollection + { + return $this->items; + } + + /** + * @param \Chamilo\PluginBundle\Entity\XApi\Cmi5Item $cmi5Item + * + * @return $this + */ + public function addItem(Cmi5Item $cmi5Item) + { + $cmi5Item->setTool($this); + + $this->items->add($cmi5Item); + + return $this; + } +} diff --git a/public/plugin/xapi/src/Hook/XApiActivityHookObserver.php b/public/plugin/xapi/src/Hook/XApiActivityHookObserver.php new file mode 100644 index 00000000000..c622e5eeecc --- /dev/null +++ b/public/plugin/xapi/src/Hook/XApiActivityHookObserver.php @@ -0,0 +1,66 @@ +plugin = XApiPlugin::create(); + } + + /** + * @throws \Doctrine\ORM\ORMException + * @throws \Doctrine\ORM\OptimisticLockException + * + * @return \Chamilo\PluginBundle\Entity\XApi\SharedStatement|null + */ + protected function saveSharedStatement(Statement $statement) + { + $statementSerialized = $this->serializeStatement($statement); + + $sharedStmt = new SharedStatement( + json_decode($statementSerialized, true) + ); + + $em = Database::getManager(); + $em->persist($sharedStmt); + $em->flush(); + + return $sharedStmt; + } + + /** + * Serialize a statement to JSON. + * + * @return string + */ + private function serializeStatement(Statement $statement) + { + $serializer = Serializer::createSerializer(); + $statementSerializer = new StatementSerializer($serializer); + + return $statementSerializer->serializeStatement($statement); + } +} diff --git a/public/plugin/xapi/src/Hook/XApiCreateCourseHookObserver.php b/public/plugin/xapi/src/Hook/XApiCreateCourseHookObserver.php new file mode 100644 index 00000000000..dd5ca328b99 --- /dev/null +++ b/public/plugin/xapi/src/Hook/XApiCreateCourseHookObserver.php @@ -0,0 +1,37 @@ +getEventData(); + + $type = $data['type']; + $courseInfo = $data['course_info']; + + $plugin = XApiPlugin::create(); + + if (HOOK_EVENT_TYPE_POST == $type) { + $plugin->addCourseToolForTinCan($courseInfo['real_id']); + } + } +} diff --git a/public/plugin/xapi/src/Hook/XApiLearningPathEndHookObserver.php b/public/plugin/xapi/src/Hook/XApiLearningPathEndHookObserver.php new file mode 100644 index 00000000000..5993cfacc26 --- /dev/null +++ b/public/plugin/xapi/src/Hook/XApiLearningPathEndHookObserver.php @@ -0,0 +1,26 @@ +getEventData(); + $em = Database::getManager(); + + $lpView = $em->find('ChamiloCourseBundle:CLpView', $data['lp_view_id']); + $lp = $em->find('ChamiloCourseBundle:CLp', $lpView->getLpId()); + + $learningPathEnded = new LearningPathCompleted($lpView, $lp); + + $statement = $learningPathEnded->generate(); + + $this->saveSharedStatement($statement); + } +} diff --git a/public/plugin/xapi/src/Hook/XApiLearningPathItemViewedHookObserver.php b/public/plugin/xapi/src/Hook/XApiLearningPathItemViewedHookObserver.php new file mode 100644 index 00000000000..676b504c3d0 --- /dev/null +++ b/public/plugin/xapi/src/Hook/XApiLearningPathItemViewedHookObserver.php @@ -0,0 +1,35 @@ +getEventData(); + $em = Database::getManager(); + + $lpItemView = $em->find('ChamiloCourseBundle:CLpItemView', $data['item_view_id']); + $lpItem = $em->find('ChamiloCourseBundle:CLpItem', $lpItemView->getLpItemId()); + + if ('quiz' == $lpItem->getItemType()) { + return null; + } + + $lpView = $em->find('ChamiloCourseBundle:CLpView', $lpItemView->getLpViewId()); + + $lpItemViewed = new LearningPathItemViewed($lpItemView, $lpItem, $lpView); + + $statement = $lpItemViewed->generate(); + + $this->saveSharedStatement($statement); + } +} diff --git a/public/plugin/xapi/src/Hook/XApiPortfolioCommentEditedHookObserver.php b/public/plugin/xapi/src/Hook/XApiPortfolioCommentEditedHookObserver.php new file mode 100644 index 00000000000..4bd43bfd0c6 --- /dev/null +++ b/public/plugin/xapi/src/Hook/XApiPortfolioCommentEditedHookObserver.php @@ -0,0 +1,19 @@ +getEventData()['comment']; + + $statement = (new PortfolioCommentEdited($comment))->generate(); + + $this->saveSharedStatement($statement); + } +} diff --git a/public/plugin/xapi/src/Hook/XApiPortfolioCommentScoredHookObserver.php b/public/plugin/xapi/src/Hook/XApiPortfolioCommentScoredHookObserver.php new file mode 100644 index 00000000000..3dcd0de5db5 --- /dev/null +++ b/public/plugin/xapi/src/Hook/XApiPortfolioCommentScoredHookObserver.php @@ -0,0 +1,19 @@ +getEventData()['comment']; + + $statement = (new PortfolioCommentScored($comment))->generate(); + + $this->saveSharedStatement($statement); + } +} diff --git a/public/plugin/xapi/src/Hook/XApiPortfolioDownloadedHookObserver.php b/public/plugin/xapi/src/Hook/XApiPortfolioDownloadedHookObserver.php new file mode 100644 index 00000000000..b992006c1e6 --- /dev/null +++ b/public/plugin/xapi/src/Hook/XApiPortfolioDownloadedHookObserver.php @@ -0,0 +1,17 @@ +getEventData()['owner']; + + $statement = (new PortfolioDownloaded($owner))->generate(); + + $this->saveSharedStatement($statement); + } +} diff --git a/public/plugin/xapi/src/Hook/XApiPortfolioItemAddedHookObserver.php b/public/plugin/xapi/src/Hook/XApiPortfolioItemAddedHookObserver.php new file mode 100644 index 00000000000..5738a1f4fb3 --- /dev/null +++ b/public/plugin/xapi/src/Hook/XApiPortfolioItemAddedHookObserver.php @@ -0,0 +1,25 @@ +getEventData()['portfolio']; + + $portfolioItemShared = new PortfolioItemShared($item); + + $statement = $portfolioItemShared->generate(); + + $this->saveSharedStatement($statement); + } +} diff --git a/public/plugin/xapi/src/Hook/XApiPortfolioItemCommentedHookObserver.php b/public/plugin/xapi/src/Hook/XApiPortfolioItemCommentedHookObserver.php new file mode 100644 index 00000000000..32bc4a8d8a8 --- /dev/null +++ b/public/plugin/xapi/src/Hook/XApiPortfolioItemCommentedHookObserver.php @@ -0,0 +1,25 @@ +getEventData()['comment']; + + $portfolioItemCommented = new PortfolioItemCommented($comment); + + $statement = $portfolioItemCommented->generate(); + + $this->saveSharedStatement($statement); + } +} diff --git a/public/plugin/xapi/src/Hook/XApiPortfolioItemEditedHookObserver.php b/public/plugin/xapi/src/Hook/XApiPortfolioItemEditedHookObserver.php new file mode 100644 index 00000000000..a5535c2a27a --- /dev/null +++ b/public/plugin/xapi/src/Hook/XApiPortfolioItemEditedHookObserver.php @@ -0,0 +1,19 @@ +getEventData()['item']; + + $statement = (new PortfolioItemEdited($item))->generate(); + + $this->saveSharedStatement($statement); + } +} diff --git a/public/plugin/xapi/src/Hook/XApiPortfolioItemHighlightedHookObserver.php b/public/plugin/xapi/src/Hook/XApiPortfolioItemHighlightedHookObserver.php new file mode 100644 index 00000000000..9a7092329f8 --- /dev/null +++ b/public/plugin/xapi/src/Hook/XApiPortfolioItemHighlightedHookObserver.php @@ -0,0 +1,22 @@ +getEventData()['item']; + + $statement = (new PortfolioItemHighlighted($item))->generate(); + + $this->saveSharedStatement($statement); + } +} diff --git a/public/plugin/xapi/src/Hook/XApiPortfolioItemScoredHookObserver.php b/public/plugin/xapi/src/Hook/XApiPortfolioItemScoredHookObserver.php new file mode 100644 index 00000000000..bed2dfb9427 --- /dev/null +++ b/public/plugin/xapi/src/Hook/XApiPortfolioItemScoredHookObserver.php @@ -0,0 +1,19 @@ +getEventData()['item']; + + $statement = (new PortfolioItemScored($item))->generate(); + + $this->saveSharedStatement($statement); + } +} diff --git a/public/plugin/xapi/src/Hook/XApiPortfolioItemViewedHookObserver.php b/public/plugin/xapi/src/Hook/XApiPortfolioItemViewedHookObserver.php new file mode 100644 index 00000000000..fbe52506644 --- /dev/null +++ b/public/plugin/xapi/src/Hook/XApiPortfolioItemViewedHookObserver.php @@ -0,0 +1,25 @@ +getEventData()['portfolio']; + + $statement = (new PortfolioItemViewed($item))->generate(); + + $this->saveSharedStatement($statement); + } +} diff --git a/public/plugin/xapi/src/Hook/XApiQuizEndHookObserver.php b/public/plugin/xapi/src/Hook/XApiQuizEndHookObserver.php new file mode 100644 index 00000000000..c8540795343 --- /dev/null +++ b/public/plugin/xapi/src/Hook/XApiQuizEndHookObserver.php @@ -0,0 +1,29 @@ +getEventData(); + $em = Database::getManager(); + + $exe = $em->find('ChamiloCoreBundle:TrackEExercises', $data['exe_id']); + $quiz = $em->find('ChamiloCourseBundle:CQuiz', $exe->getExeExoId()); + + $quizCompleted = new QuizCompleted($exe, $quiz); + + $statement = $quizCompleted->generate(); + + $this->saveSharedStatement($statement); + } +} diff --git a/public/plugin/xapi/src/Hook/XApiQuizQuestionAnsweredHookObserver.php b/public/plugin/xapi/src/Hook/XApiQuizQuestionAnsweredHookObserver.php new file mode 100644 index 00000000000..f1645425591 --- /dev/null +++ b/public/plugin/xapi/src/Hook/XApiQuizQuestionAnsweredHookObserver.php @@ -0,0 +1,42 @@ +getEventData(); + + $em = Database::getManager(); + $attemptRepo = $em->getRepository(TrackEAttempt::class); + + $exe = $em->find(TrackEExercises::class, $data['exe_id']); + $question = $em->find(CQuizQuestion::class, $data['question']['id']); + $attempt = $attemptRepo->findOneBy( + [ + 'exeId' => $exe->getExeId(), + 'questionId' => $question->getId(), + ] + ); + $quiz = $em->find(CQuiz::class, $data['quiz']['id']); + + $quizQuestionAnswered = new QuizQuestionAnswered($attempt, $question, $quiz); + + $statement = $quizQuestionAnswered->generate(); + + $this->saveSharedStatement($statement); + } +} diff --git a/public/plugin/xapi/src/Importer/PackageImporter.php b/public/plugin/xapi/src/Importer/PackageImporter.php new file mode 100644 index 00000000000..25a8b4b1f60 --- /dev/null +++ b/public/plugin/xapi/src/Importer/PackageImporter.php @@ -0,0 +1,67 @@ +packageFileInfo = $fileInfo; + $this->course = $course; + + $this->courseDirectoryPath = api_get_path(SYS_COURSE_PATH).$this->course->getDirectory(); + } + + /** + * @return \Chamilo\PluginBundle\XApi\Importer\XmlPackageImporter|\Chamilo\PluginBundle\XApi\Importer\ZipPackageImporter + */ + public static function create(array $fileInfo, Course $course) + { + if ('text/xml' === $fileInfo['type']) { + return new XmlPackageImporter($fileInfo, $course); + } + + return new ZipPackageImporter($fileInfo, $course); + } + + /** + * @throws \Exception + * + * @return mixed + */ + abstract public function import(): string; + + public function getPackageType(): string + { + return $this->packageType; + } +} diff --git a/public/plugin/xapi/src/Importer/XmlPackageImporter.php b/public/plugin/xapi/src/Importer/XmlPackageImporter.php new file mode 100644 index 00000000000..2a48fddb375 --- /dev/null +++ b/public/plugin/xapi/src/Importer/XmlPackageImporter.php @@ -0,0 +1,29 @@ +packageFileInfo['name'], ['tincan.xml', 'cmi5.xml'])) { + throw new Exception('Invalid package'); + } + + $this->packageType = explode('.', $this->packageFileInfo['name'], 2)[0]; + + return $this->packageFileInfo['tmp_name']; + } +} diff --git a/public/plugin/xapi/src/Importer/ZipPackageImporter.php b/public/plugin/xapi/src/Importer/ZipPackageImporter.php new file mode 100644 index 00000000000..ae5be37a286 --- /dev/null +++ b/public/plugin/xapi/src/Importer/ZipPackageImporter.php @@ -0,0 +1,88 @@ +packageFileInfo['tmp_name']); + $zipContent = $zipFile->listContent(); + + $packageSize = array_reduce( + $zipContent, + function ($accumulator, $zipEntry) { + if (preg_match('~.(php.*|phtml)$~i', $zipEntry['filename'])) { + throw new Exception("File \"{$zipEntry['filename']}\" contains a PHP script"); + } + + if (in_array($zipEntry['filename'], ['tincan.xml', 'cmi5.xml'])) { + $this->packageType = explode('.', $zipEntry['filename'], 2)[0]; + } + + return $accumulator + $zipEntry['size']; + } + ); + + if (empty($this->packageType)) { + throw new Exception('Invalid package'); + } + + $this->validateEnoughSpace($packageSize); + + $pathInfo = pathinfo($this->packageFileInfo['name']); + + $packageDirectoryPath = $this->generatePackageDirectory($pathInfo['filename']); + + $zipFile->extract($packageDirectoryPath); + + return "$packageDirectoryPath/{$this->packageType}.xml"; + } + + /** + * @throws \Exception + */ + protected function validateEnoughSpace(int $packageSize) + { + $courseSpaceQuota = DocumentManager::get_course_quota($this->course->getCode()); + + if (!enough_size($packageSize, $this->courseDirectoryPath, $courseSpaceQuota)) { + throw new Exception('Not enough space to storage package.'); + } + } + + private function generatePackageDirectory(string $name): string + { + $directoryPath = implode( + '/', + [ + $this->courseDirectoryPath, + $this->packageType, + api_replace_dangerous_char($name), + ] + ); + + $fs = new Filesystem(); + $fs->mkdir( + $directoryPath, + api_get_permissions_for_new_directories() + ); + + return $directoryPath; + } +} diff --git a/public/plugin/xapi/src/Lrs/AboutController.php b/public/plugin/xapi/src/Lrs/AboutController.php new file mode 100644 index 00000000000..084f2ddae68 --- /dev/null +++ b/public/plugin/xapi/src/Lrs/AboutController.php @@ -0,0 +1,30 @@ + [ + '1.0.3', + '1.0.2', + '1.0.1', + '1.0.0', + ], + ]; + + return JsonResponse::create($json); + } +} diff --git a/public/plugin/xapi/src/Lrs/ActivitiesProfileController.php b/public/plugin/xapi/src/Lrs/ActivitiesProfileController.php new file mode 100644 index 00000000000..bf12e82f0ff --- /dev/null +++ b/public/plugin/xapi/src/Lrs/ActivitiesProfileController.php @@ -0,0 +1,78 @@ +httpRequest->query->get('profileId'); + $activityId = $this->httpRequest->query->get('activityId'); + + $em = \Database::getManager(); + $profileRepo = $em->getRepository(ActivityProfile::class); + + /** @var ActivityProfile $activityProfile */ + $activityProfile = $profileRepo->findOneBy( + [ + 'profileId' => $profileId, + 'activityId' => $activityId, + ] + ); + + if (empty($activityProfile)) { + return Response::create(null, Response::HTTP_NO_CONTENT); + } + + return Response::create( + json_encode($activityProfile->getDocumentData()) + ); + } + + public function head(): Response + { + return $this->get()->setContent(''); + } + + public function put(): Response + { + $profileId = $this->httpRequest->query->get('profileId'); + $activityId = $this->httpRequest->query->get('activityId'); + $documentData = $this->httpRequest->getContent(); + + $em = \Database::getManager(); + $profileRepo = $em->getRepository(ActivityProfile::class); + + /** @var ActivityProfile $activityProfile */ + $activityProfile = $profileRepo->findOneBy( + [ + 'profileId' => $profileId, + 'activityId' => $activityId, + ] + ); + + if (empty($activityProfile)) { + $activityProfile = new ActivityProfile(); + $activityProfile + ->setProfileId($profileId) + ->setActivityId($activityId); + } + + $activityProfile->setDocumentData(json_decode($documentData, true)); + + $em->persist($activityProfile); + $em->flush(); + + return Response::create(null, Response::HTTP_NO_CONTENT); + } +} diff --git a/public/plugin/xapi/src/Lrs/ActivitiesStateController.php b/public/plugin/xapi/src/Lrs/ActivitiesStateController.php new file mode 100644 index 00000000000..6d05fc7b865 --- /dev/null +++ b/public/plugin/xapi/src/Lrs/ActivitiesStateController.php @@ -0,0 +1,120 @@ +httpRequest->query->get('agent'); + $activityId = $this->httpRequest->query->get('activityId'); + $stateId = $this->httpRequest->query->get('stateId'); + + $state = Database::select( + '*', + Database::get_main_table('xapi_activity_state'), + [ + 'where' => [ + 'state_id = ? AND activity_id = ? AND MD5(agent) = ?' => [ + Database::escape_string($stateId), + Database::escape_string($activityId), + md5($requestedAgent), + ], + ], + ], + 'first' + ); + + if (empty($state)) { + return JsonResponse::create([], Response::HTTP_NOT_FOUND); + } + + $requestedAgent = $serializer->deserialize( + $this->httpRequest->query->get('agent'), + Actor::class, + 'json' + ); + + /** @var Actor $stateAgent */ + $stateAgent = $serializer->deserialize( + $state['agent'], + Actor::class, + 'json' + ); + + if (!$stateAgent->equals($requestedAgent)) { + return JsonResponse::create([], Response::HTTP_NOT_FOUND); + } + + $documentData = json_decode($state['document_data'], true); + + return JsonResponse::create($documentData); + } + + public function head(): Response + { + return $this->get()->setContent(''); + } + + public function post(): Response + { + return $this->put(); + } + + public function put(): Response + { + $activityId = $this->httpRequest->query->get('activityId'); + $agent = $this->httpRequest->query->get('agent'); + $stateId = $this->httpRequest->query->get('stateId'); + $documentData = $this->httpRequest->getContent(); + + $state = Database::select( + 'id', + Database::get_main_table('xapi_activity_state'), + [ + 'where' => [ + 'state_id = ? AND activity_id = ? AND MD5(agent) = ?' => [ + Database::escape_string($stateId), + Database::escape_string($activityId), + md5($agent), + ], + ], + ], + 'first' + ); + + $em = Database::getManager(); + + if (empty($state)) { + $state = new ActivityState(); + $state + ->setActivityId($activityId) + ->setAgent(json_decode($agent, true)) + ->setStateId($stateId); + } else { + $state = $em->find(ActivityState::class, $state['id']); + } + + $state->setDocumentData(json_decode($documentData, true)); + + $em->persist($state); + $em->flush(); + + return Response::create('', Response::HTTP_NO_CONTENT); + } +} diff --git a/public/plugin/xapi/src/Lrs/BaseController.php b/public/plugin/xapi/src/Lrs/BaseController.php new file mode 100644 index 00000000000..865b2b71b68 --- /dev/null +++ b/public/plugin/xapi/src/Lrs/BaseController.php @@ -0,0 +1,28 @@ +httpRequest = $httpRequest; + } +} diff --git a/public/plugin/xapi/src/Lrs/LrsRequest.php b/public/plugin/xapi/src/Lrs/LrsRequest.php new file mode 100644 index 00000000000..f409f61943b --- /dev/null +++ b/public/plugin/xapi/src/Lrs/LrsRequest.php @@ -0,0 +1,221 @@ +request = HttpRequest::createFromGlobals(); + } + + public function send() + { + try { + $this->alternateRequestSyntax(); + + $controllerName = $this->getControllerName(); + $methodName = $this->getMethodName(); + + $response = $this->generateResponse($controllerName, $methodName); + } catch (XApiException $xApiException) { + $response = HttpResponse::create('', HttpResponse::HTTP_BAD_REQUEST); + } catch (HttpException $httpException) { + $response = HttpResponse::create( + $httpException->getMessage(), + $httpException->getStatusCode() + ); + } catch (\Exception $exception) { + $response = HttpResponse::create($exception->getMessage(), HttpResponse::HTTP_BAD_REQUEST); + } + + $response->headers->set('X-Experience-API-Version', '1.0.3'); + + $response->send(); + } + + /** + * @throws \Xabbuh\XApi\Common\Exception\AccessDeniedException + */ + private function validateAuth(): bool + { + if (!$this->request->headers->has('Authorization')) { + throw new AccessDeniedException(); + } + + $authHeader = $this->request->headers->get('Authorization'); + + $parts = explode('Basic ', $authHeader, 2); + + if (empty($parts[1])) { + throw new AccessDeniedException(); + } + + $authDecoded = base64_decode($parts[1]); + + $parts = explode(':', $authDecoded, 2); + + if (empty($parts) || count($parts) !== 2) { + throw new AccessDeniedException(); + } + + list($username, $password) = $parts; + + $auth = Database::getManager() + ->getRepository(LrsAuth::class) + ->findOneBy( + ['username' => $username, 'password' => $password, 'enabled' => true] + ); + + if (null == $auth) { + throw new AccessDeniedException(); + } + + return true; + } + + private function validateVersion() + { + $version = $this->request->headers->get('X-Experience-API-Version'); + + if (null === $version) { + throw new BadRequestHttpException('The "X-Experience-API-Version" header is required.'); + } + + if (preg_match('/^1\.0(?:\.\d+)?$/', $version)) { + if ('1.0' === $version) { + $this->request->headers->set('X-Experience-API-Version', '1.0.0'); + } + + return; + } + + throw new BadRequestHttpException("The xAPI version \"$version\" is not supported."); + } + + private function getControllerName(): ?string + { + $segments = explode('/', $this->request->getPathInfo()); + $segments = array_filter($segments); + $segments = array_values($segments); + + if (empty($segments)) { + throw new BadRequestHttpException('Bad request'); + } + + $segments = array_map('ucfirst', $segments); + $controllerName = implode('', $segments).'Controller'; + + return "Chamilo\\PluginBundle\\XApi\Lrs\\$controllerName"; + } + + private function getMethodName(): string + { + $method = $this->request->getMethod(); + + return strtolower($method); + } + + /** + * @throws \Xabbuh\XApi\Common\Exception\AccessDeniedException + */ + private function generateResponse(string $controllerName, string $methodName): HttpResponse + { + if (!class_exists($controllerName) + || !method_exists($controllerName, $methodName) + ) { + throw new NotFoundHttpException(); + } + + if ($controllerName !== AboutController::class) { + $this->validateAuth(); + $this->validateVersion(); + } + + /** @var HttpResponse $response */ + $response = call_user_func( + [ + new $controllerName($this->request), + $methodName, + ] + ); + + return $response; + } + + private function alternateRequestSyntax() + { + if ('POST' !== $this->request->getMethod()) { + return; + } + + if (null === $method = $this->request->query->get('method')) { + return; + } + + if ($this->request->query->count() > 1) { + throw new BadRequestHttpException('Including other query parameters than "method" is not allowed. You have to send them as POST parameters inside the request body.'); + } + + $this->request->setMethod($method); + $this->request->query->remove('method'); + + if (null !== $content = $this->request->request->get('content')) { + $this->request->request->remove('content'); + + $this->request->initialize( + $this->request->query->all(), + $this->request->request->all(), + $this->request->attributes->all(), + $this->request->cookies->all(), + $this->request->files->all(), + $this->request->server->all(), + $content + ); + } + + $headerNames = [ + 'Authorization', + 'X-Experience-API-Version', + 'Content-Type', + 'Content-Length', + 'If-Match', + 'If-None-Match', + ]; + + foreach ($this->request->request as $key => $value) { + if (in_array($key, $headerNames, true)) { + $this->request->headers->set($key, $value); + } else { + $this->request->query->set($key, $value); + } + + $this->request->request->remove($key); + } + } +} diff --git a/public/plugin/xapi/src/Lrs/StatementsController.php b/public/plugin/xapi/src/Lrs/StatementsController.php new file mode 100644 index 00000000000..86dc548b0ce --- /dev/null +++ b/public/plugin/xapi/src/Lrs/StatementsController.php @@ -0,0 +1,147 @@ +statementRepository = new StatementRepository( + $pluginEm->getRepository(StatementEntity::class) + ); + $this->serializer = Serializer::createSerializer(); + $this->serializerFactory = new SerializerFactory($this->serializer); + } + + public function get(): Response + { + $getStatementController = new StatementGetController( + $this->statementRepository, + $this->serializerFactory->createStatementSerializer(), + $this->serializerFactory->createStatementResultSerializer(), + new StatementsFilterFactory( + new ActorSerializer($this->serializer) + ) + ); + + return $getStatementController->getStatement($this->httpRequest); + } + + public function head(): Response + { + $headStatementController = new StatementHeadController( + $this->statementRepository, + $this->serializerFactory->createStatementSerializer(), + $this->serializerFactory->createStatementResultSerializer(), + new StatementsFilterFactory( + new ActorSerializer($this->serializer) + ) + ); + + return $headStatementController->getStatement($this->httpRequest); + } + + public function put(): Response + { + $statement = $this->serializerFactory + ->createStatementSerializer() + ->deserializeStatement( + $this->httpRequest->getContent() + ) + ; + + $putStatementController = new StatementPutController($this->statementRepository); + + $response = $putStatementController->putStatement($this->httpRequest, $statement); + + $this->saveLog( + [$this->httpRequest->query->get('statementId')] + ); + + return $response; + } + + public function post(): Response + { + $content = $this->httpRequest->getContent(); + + if (substr($content, 0, 1) !== '[') { + $content = "[$content]"; + } + + $statements = $this->serializerFactory + ->createStatementSerializer() + ->deserializeStatements($content) + ; + + $postStatementController = new StatementPostController($this->statementRepository); + + $response = $postStatementController->postStatements($this->httpRequest, $statements); + + $this->saveLog( + json_decode($response->getContent(), false) + ); + + return $response; + } + + /** + * @param array $statementsId + * + * @return void + */ + private function saveLog(array $statementsId) + { + foreach ($statementsId as $statementId) { + try { + $storedStatement = $this->statementRepository->findStatementById( + StatementId::fromString($statementId) + ); + + InternalLogUtil::saveStatementForInternalLog($storedStatement); + } catch (NotFoundException $e) { + } + } + } +} diff --git a/public/plugin/xapi/src/Lrs/Utils/InternalLogUtil.php b/public/plugin/xapi/src/Lrs/Utils/InternalLogUtil.php new file mode 100644 index 00000000000..dc9a1cbcd14 --- /dev/null +++ b/public/plugin/xapi/src/Lrs/Utils/InternalLogUtil.php @@ -0,0 +1,115 @@ +getActor())) { + return; + } + + $statementObject = $statement->getObject(); + + if (!$statementObject instanceof Activity) { + return; + } + + $languageIso = api_get_language_isocode(); + $statementVerbString = XApiPlugin::extractVerbInLanguage($statement->getVerb()->getDisplay(), $languageIso); + + $internalLog = new InternalLog(); + $internalLog + ->setUser($user) + ->setVerb($statementVerbString) + ->setObjectId($statementObject->getId()->getValue()); + + if (null !== $statementId = $statement->getId()) { + $internalLog->setStatementId($statementId->getValue()); + } + + if (null !== $definition = $statementObject->getDefinition()) { + if (null !== $nameInLanguages = $definition->getName()) { + $internalLog->setActivityName( + XApiPlugin::extractVerbInLanguage($nameInLanguages, $languageIso) + ); + } + + if (null !== $descriptionInLanguage = $definition->getDescription()) { + $internalLog->setActivityDescription( + XApiPlugin::extractVerbInLanguage($descriptionInLanguage, $languageIso) + ); + } + } + + if (null !== $statementResult = $statement->getResult()) { + if (null !== $score = $statementResult->getScore()) { + $internalLog + ->setScoreScaled( + $score->getScaled() + ) + ->setScoreRaw( + $score->getRaw() + ) + ->setScoreMin( + $score->getMin() + ) + ->setScoreMax( + $score->getMax() + ); + } + } + + if (null !== $created = $statement->getCreated()) { + $internalLog->setCreatedAt($created); + } + + $em = Database::getManager(); + $em->persist($internalLog); + $em->flush(); + } + + private static function getUserFromActor(Actor $actor): ?object + { + if (!$actor instanceof Agent) { + return null; + } + + $actorIri = $actor->getInverseFunctionalIdentifier(); + + if (null === $actorIri) { + return null; + } + + $userRepo = UserManager::getRepository(); + + $user = null; + + if (null !== $mbox = $actorIri->getMbox()) { + if ((null !== $parts = explode(':', $mbox->getValue(), 2)) && !empty($parts[1])) { + $user = $userRepo->findOneBy(['email' => $parts[1]]); + } + } elseif (null !== $account = $actorIri->getAccount()) { + $chamiloIrl = IRL::fromString(api_get_path(WEB_PATH)); + + if ($account->getHomePage()->equals($chamiloIrl)) { + $user = $userRepo->findOneBy(['username' => $account->getName()]); + } + } + + return $user; + } +} diff --git a/public/plugin/xapi/src/Parser/Cmi5Parser.php b/public/plugin/xapi/src/Parser/Cmi5Parser.php new file mode 100644 index 00000000000..0d7cc96c340 --- /dev/null +++ b/public/plugin/xapi/src/Parser/Cmi5Parser.php @@ -0,0 +1,178 @@ +filePath); + $xml = new Crawler($content); + + $courseNode = $xml->filterXPath('//courseStructure/course'); + + $toolLaunch = new ToolLaunch(); + $toolLaunch + ->setTitle( + current( + $this->getLanguageStrings( + $courseNode->filterXPath('//title') + ) + ) + ) + ->setDescription( + current( + $this->getLanguageStrings( + $courseNode->filterXPath('//description') + ) + ) + ) + ->setLaunchUrl('') + ->setActivityId($courseNode->attr('id')) + ->setActivityType('cmi5') + ->setAllowMultipleAttempts(false) + ->setCreatedAt(api_get_utc_datetime(null, false, true)) + ->setCourse($this->course) + ->setSession($this->session); + + $toc = $this->generateToC($xml); + + foreach ($toc as $cmi5Item) { + $toolLaunch->addItem($cmi5Item); + } + + return $toolLaunch; + } + + /** + * @return array + */ + private function getLanguageStrings(Crawler $node) + { + $map = []; + + foreach ($node->children() as $child) { + $key = $child->attributes['lang']->value; + $value = trim($child->textContent); + + $map[$key] = $value; + } + + return $map; + } + + /** + * @return array|\Chamilo\PluginBundle\Entity\XApi\Cmi5Item[] + */ + private function generateToC(Crawler $xml) + { + $blocksMap = []; + + /** @var array|Cmi5Item[] $items */ + $items = $xml + ->filterXPath('//*') + ->reduce( + function (Crawler $node, $i) { + return in_array($node->nodeName(), ['au', 'block']); + } + ) + ->each( + function (Crawler $node, $i) use (&$blocksMap) { + $attributes = ['id', 'activityType', 'launchMethod', 'moveOn', 'masteryScore']; + + list($id, $activityType, $launchMethod, $moveOn, $masteryMode) = $node->extract($attributes)[0]; + + $item = new Cmi5Item(); + $item + ->setIdentifier($id) + ->setType($node->nodeName()) + ->setTitle( + $this->getLanguageStrings( + $node->filterXPath('//title') + ) + ) + ->setDescription( + $this->getLanguageStrings( + $node->filterXPath('//description') + ) + ); + + if ('au' === $node->nodeName()) { + $launchParametersNode = $node->filterXPath('//launchParameters'); + $entitlementKeyNode = $node->filterXPath('//entitlementKey'); + $url + = $item + ->setUrl( + $this->parseLaunchUrl( + trim($node->filterXPath('//url')->text()) + ) + ) + ->setActivityType($activityType ?: null) + ->setLaunchMethod($launchMethod ?: null) + ->setMoveOn($moveOn ?: 'NotApplicable') + ->setMasteryScore((float) $masteryMode ?: null) + ->setLaunchParameters( + $launchParametersNode->count() > 0 ? trim($launchParametersNode->text()) : null + ) + ->setEntitlementKey( + $entitlementKeyNode->count() > 0 ? trim($entitlementKeyNode->text()) : null + ); + } + + $parentNode = $node->parents()->first(); + + if ('block' === $parentNode->nodeName()) { + $blocksMap[$i] = $parentNode->attr('id'); + } + + return $item; + } + ); + + foreach ($blocksMap as $itemPos => $parentIdentifier) { + foreach ($items as $item) { + if ($parentIdentifier === $item->getIdentifier()) { + $items[$itemPos]->setParent($item); + } + } + } + + return $items; + } + + /** + * @param string $url + * + * @return string + */ + private function parseLaunchUrl($url) + { + $urlInfo = parse_url($url); + + if (empty($urlInfo['scheme'])) { + $baseUrl = str_replace( + api_get_path(SYS_COURSE_PATH), + api_get_path(WEB_COURSE_PATH), + dirname($this->filePath) + ); + + return "$baseUrl/$url"; + } + + return $url; + } +} diff --git a/public/plugin/xapi/src/Parser/PackageParser.php b/public/plugin/xapi/src/Parser/PackageParser.php new file mode 100644 index 00000000000..0454eca522a --- /dev/null +++ b/public/plugin/xapi/src/Parser/PackageParser.php @@ -0,0 +1,60 @@ +filePath = $filePath; + $this->course = $course; + $this->session = $session; + } + + /** + * @throws \Exception + * + * @return mixed + */ + public static function create(string $packageType, string $filePath, Course $course, Session $session = null) + { + switch ($packageType) { + case 'tincan': + return new TinCanParser($filePath, $course, $session); + case 'cmi5': + return new Cmi5Parser($filePath, $course, $session); + default: + throw new \Exception('Invalid package.'); + } + } + + abstract public function parse(): \Chamilo\PluginBundle\Entity\XApi\ToolLaunch; +} diff --git a/public/plugin/xapi/src/Parser/TinCanParser.php b/public/plugin/xapi/src/Parser/TinCanParser.php new file mode 100644 index 00000000000..c8de58dc137 --- /dev/null +++ b/public/plugin/xapi/src/Parser/TinCanParser.php @@ -0,0 +1,69 @@ +filePath); + + $xml = new Crawler($content); + + $activityNode = $xml->filter('tincan activities activity')->first(); + $nodeName = $activityNode->filter('name'); + $nodeDescription = $activityNode->filter('description'); + $nodeLaunch = $activityNode->filter('launch'); + + $toolLaunch = new ToolLaunch(); + $toolLaunch + ->setCourse($this->course) + ->setSession($this->session) + ->setCreatedAt(api_get_utc_datetime(null, false, true)) + ->setActivityId($activityNode->attr('id')) + ->setActivityType($activityNode->attr('type')) + ->setLaunchUrl($this->parseLaunchUrl($nodeLaunch)); + + if ($nodeName) { + $toolLaunch->setTitle($nodeName->text()); + } + + if ($nodeDescription) { + $toolLaunch->setDescription($nodeDescription->text() ?: null); + } + + return $toolLaunch; + } + + private function parseLaunchUrl(Crawler $launchNode): string + { + $launchUrl = $launchNode->text(); + + $urlInfo = parse_url($launchUrl); + + if (empty($urlInfo['scheme'])) { + $baseUrl = str_replace( + api_get_path(SYS_COURSE_PATH), + api_get_path(WEB_COURSE_PATH), + dirname($this->filePath) + ); + + return "$baseUrl/$launchUrl"; + } + + return $launchUrl; + } +} diff --git a/public/plugin/xapi/src/ToolExperience/Activity/BaseActivity.php b/public/plugin/xapi/src/ToolExperience/Activity/BaseActivity.php new file mode 100644 index 00000000000..c169021714d --- /dev/null +++ b/public/plugin/xapi/src/ToolExperience/Activity/BaseActivity.php @@ -0,0 +1,49 @@ +getCourseLanguage()); + + $courseUrl = api_get_course_url( + $course->getCode(), + $session ? $session->getId() : 0 + ); + + return new Activity( + IRI::fromString($courseUrl), + new Definition( + LanguageMap::create([$languageIso => $course->getTitle()]), + null, + IRI::fromString('http://id.tincanapi.com/activitytype/lms/course') + ) + ); + } +} diff --git a/public/plugin/xapi/src/ToolExperience/Activity/LearningPath.php b/public/plugin/xapi/src/ToolExperience/Activity/LearningPath.php new file mode 100644 index 00000000000..1dad3995054 --- /dev/null +++ b/public/plugin/xapi/src/ToolExperience/Activity/LearningPath.php @@ -0,0 +1,53 @@ +lp = $lp; + } + + public function generate(): Activity + { + $lanIso = api_get_language_isocode(); + + $iri = $this->generateIri( + WEB_CODE_PATH, + 'lp/lp_controller.php', + [ + 'action' => 'view', + 'lp_id' => $this->lp->getId(), + 'isStudentView' => 'true', + ] + ); + + return new Activity( + IRI::fromString($iri), + new Definition( + LanguageMap::create([$lanIso => $this->lp->getName()]), + null, + IRI::fromString('http://adlnet.gov/expapi/activities/lesson') + ) + ); + } +} diff --git a/public/plugin/xapi/src/ToolExperience/Activity/LearningPathItem.php b/public/plugin/xapi/src/ToolExperience/Activity/LearningPathItem.php new file mode 100644 index 00000000000..6ac65fd65c1 --- /dev/null +++ b/public/plugin/xapi/src/ToolExperience/Activity/LearningPathItem.php @@ -0,0 +1,54 @@ +lpItem = $lpItem; + } + + public function generate(): Activity + { + $langIso = api_get_language_isocode(); + + $iri = $this->generateIri( + WEB_CODE_PATH, + 'lp/lp_controller.php', + [ + 'action' => 'view', + 'lp_id' => $this->lpItem->getLpId(), + 'isStudentView' => 'true', + 'lp_item' => $this->lpItem->getId(), + ] + ); + + return new Activity( + IRI::fromString($iri), + new Definition( + LanguageMap::create([$langIso => $this->lpItem->getTitle()]), + null, + IRI::fromString('http://id.tincanapi.com/activitytype/resource') + ) + ); + } +} diff --git a/public/plugin/xapi/src/ToolExperience/Activity/Portfolio.php b/public/plugin/xapi/src/ToolExperience/Activity/Portfolio.php new file mode 100644 index 00000000000..7d391e17694 --- /dev/null +++ b/public/plugin/xapi/src/ToolExperience/Activity/Portfolio.php @@ -0,0 +1,50 @@ +owner = $owner; + } + + public function generate(): Activity + { + $langIso = api_get_language_isocode(); + + $iri = $this->generateIri( + WEB_CODE_PATH, + 'portfolio/index.php', + [ + 'action' => 'list', + 'user' => $this->owner->getId(), + ] + ); + + return new Activity( + IRI::fromString($iri), + new Definition( + LanguageMap::create( + [ + $langIso => sprintf( + get_lang('XUserPortfolioItems'), + $this->owner->getCompleteNameWithUsername() + ), + ] + ), + null, + IRI::fromString('http://id.tincanapi.com/activitytype/collection-simple') + ) + ); + } +} diff --git a/public/plugin/xapi/src/ToolExperience/Activity/PortfolioCategory.php b/public/plugin/xapi/src/ToolExperience/Activity/PortfolioCategory.php new file mode 100644 index 00000000000..5da9324b632 --- /dev/null +++ b/public/plugin/xapi/src/ToolExperience/Activity/PortfolioCategory.php @@ -0,0 +1,58 @@ +category = $category; + } + + public function generate(): Activity + { + $iri = $this->generateIri( + WEB_PATH, + 'xapi/portfolio/', + [ + 'user' => $this->category->getUser()->getId(), + 'category' => $this->category->getId(), + ] + ); + + $langIso = api_get_language_isocode(); + + $categoryDescription = $this->category->getDescription(); + + $definitionDescription = $categoryDescription + ? LanguageMap::create([$langIso => $categoryDescription]) + : null; + + return new Activity( + IRI::fromString($iri), + new Definition( + LanguageMap::create([$langIso => $this->category->getTitle()]), + $definitionDescription, + IRI::fromString('http://id.tincanapi.com/activitytype/category') + ) + ); + } +} diff --git a/public/plugin/xapi/src/ToolExperience/Activity/PortfolioComment.php b/public/plugin/xapi/src/ToolExperience/Activity/PortfolioComment.php new file mode 100644 index 00000000000..9e7f91a410f --- /dev/null +++ b/public/plugin/xapi/src/ToolExperience/Activity/PortfolioComment.php @@ -0,0 +1,50 @@ +comment = $comment; + } + + public function generate(): Activity + { + $iri = $this->generateIri( + WEB_CODE_PATH, + 'portfolio/index.php', + [ + 'action' => 'view', + 'id' => $this->comment->getItem()->getId(), + 'comment' => $this->comment->getId(), + ] + ); + + return new Activity( + IRI::fromString($iri), + new Definition( + null, + null, + IRI::fromString('http://activitystrea.ms/schema/1.0/comment') + ) + ); + } +} diff --git a/public/plugin/xapi/src/ToolExperience/Activity/PortfolioItem.php b/public/plugin/xapi/src/ToolExperience/Activity/PortfolioItem.php new file mode 100644 index 00000000000..8f995dd8a54 --- /dev/null +++ b/public/plugin/xapi/src/ToolExperience/Activity/PortfolioItem.php @@ -0,0 +1,49 @@ +item = $item; + } + + public function generate(): Activity + { + $langIso = api_get_language_isocode(); + + $iri = $this->generateIri( + WEB_CODE_PATH, + 'portfolio/index.php', + ['action' => 'view', 'id' => $this->item->getId()] + ); + + return new Activity( + IRI::fromString($iri), + new Definition( + LanguageMap::create([$langIso => $this->item->getTitle()]), + null, + IRI::fromString('http://activitystrea.ms/schema/1.0/article') + ) + ); + } +} diff --git a/public/plugin/xapi/src/ToolExperience/Activity/Quiz.php b/public/plugin/xapi/src/ToolExperience/Activity/Quiz.php new file mode 100644 index 00000000000..c4f9a593589 --- /dev/null +++ b/public/plugin/xapi/src/ToolExperience/Activity/Quiz.php @@ -0,0 +1,57 @@ +quiz = $quiz; + } + + public function generate(): Activity + { + $langIso = api_get_language_isocode(); + + $iri = $this->generateIri( + WEB_CODE_PATH, + 'exercise/overview.php', + ['exerciseId' => $this->quiz->getId()] + ); + + $definitionDescription = null; + + if ($this->quiz->getDescription()) { + $definitionDescription = LanguageMap::create( + [$langIso => $this->quiz->getDescription()] + ); + } + + return new Activity( + IRI::fromString($iri), + new Definition( + LanguageMap::create([$langIso => $this->quiz->getTitle()]), + $definitionDescription, + IRI::fromString('http://adlnet.gov/expapi/activities/assessment') + ) + ); + } +} diff --git a/public/plugin/xapi/src/ToolExperience/Activity/QuizQuestion.php b/public/plugin/xapi/src/ToolExperience/Activity/QuizQuestion.php new file mode 100644 index 00000000000..74c9a81101e --- /dev/null +++ b/public/plugin/xapi/src/ToolExperience/Activity/QuizQuestion.php @@ -0,0 +1,170 @@ +question = $question; + } + + public function generate(): Activity + { + $iri = $this->generateIri( + WEB_CODE_PATH, + 'xapi/quiz/', + ['question' => $this->question->getId()] + ); + + return new Activity( + IRI::fromString($iri), + $this->generateActivityDefinitionFromQuestionType() + ); + } + + /** + * @return \Xabbuh\XApi\Model\Interaction\InteractionDefinition + */ + private function generateActivityDefinitionFromQuestionType() + { + $languageIso = api_get_language_isocode(); + $courseId = api_get_course_int_id(); + + $questionTitle = strip_tags($this->question->getQuestion()); + $questionTitle = trim($questionTitle); + $questionDescription = strip_tags($this->question->getDescription()); + $questionDescription = trim($questionDescription); + + $titleMap = LanguageMap::create([$languageIso => $questionTitle]); + $descriptionMap = $questionDescription ? LanguageMap::create([$languageIso => $questionDescription]) : null; + + $objAnswer = new Answer($this->question->getId(), $courseId); + $objAnswer->read(); + + $type = IRI::fromString('http://adlnet.gov/expapi/activities/question'); + + switch ($this->question->getType()) { + case MULTIPLE_ANSWER: + case UNIQUE_ANSWER: + case UNIQUE_ANSWER_IMAGE: + case READING_COMPREHENSION: + $choices = []; + $correctResponsesPattern = []; + + for ($i = 1; $i <= $objAnswer->nbrAnswers; $i++) { + $choices[] = new InteractionComponent( + $objAnswer->iid[$i], + LanguageMap::create([$languageIso => $objAnswer->selectAnswer($i)]) + ); + + if ($objAnswer->isCorrect($i)) { + $correctResponsesPattern[] = $objAnswer->iid[$i]; + } + } + + return new ChoiceInteractionDefinition( + $titleMap, + $descriptionMap, + $type, + null, + null, + [implode('[,]', $correctResponsesPattern)], + $choices + ); + case DRAGGABLE: + $choices = []; + + for ($i = 1; $i <= $objAnswer->nbrAnswers; $i++) { + if ((int) $objAnswer->correct[$i] > 0) { + $choices[] = new InteractionComponent( + $objAnswer->correct[$i], + LanguageMap::create([$languageIso => $objAnswer->answer[$i]]) + ); + } + } + + $correctResponsesPattern = array_slice($objAnswer->autoId, 0, $objAnswer->nbrAnswers / 2); + + return new SequencingInteractionDefinition( + $titleMap, + $descriptionMap, + $type, + null, + null, + [implode('[,]', $correctResponsesPattern)], + $choices + ); + case MATCHING: + case MATCHING_DRAGGABLE: + /** @var array|InteractionComponent[] $source */ + $source = []; + /** @var array|InteractionComponent[] $source */ + $target = []; + $correctResponsesPattern = []; + + for ($i = 1; $i <= $objAnswer->nbrAnswers; $i++) { + $interactionComponent = new InteractionComponent( + $objAnswer->selectAutoId($i), + LanguageMap::create([$languageIso => $objAnswer->selectAnswer($i)]) + ); + + if ((int) $objAnswer->correct[$i] > 0) { + $source[] = $interactionComponent; + + $correctResponsesPattern[] = $objAnswer->selectAutoId($i).'[.]'.$objAnswer->correct[$i]; + } else { + $target[] = $interactionComponent; + } + } + + return new MatchingInteractionDefinition( + $titleMap, + $descriptionMap, + $type, + null, + null, + [implode('[,]', $correctResponsesPattern)], + $source, + $target + ); + case FREE_ANSWER: + return new LongFillInInteractionDefinition($titleMap, $descriptionMap, $type); + case FILL_IN_BLANKS: + case HOT_SPOT: + case HOT_SPOT_DELINEATION: + case MULTIPLE_ANSWER_COMBINATION: + case UNIQUE_ANSWER_NO_OPTION: + case MULTIPLE_ANSWER_TRUE_FALSE: + case MULTIPLE_ANSWER_TRUE_FALSE_DEGREE_CERTAINTY: + case MULTIPLE_ANSWER_COMBINATION_TRUE_FALSE: + case GLOBAL_MULTIPLE_ANSWER: + case CALCULATED_ANSWER: + case ANNOTATION: + case ORAL_EXPRESSION: + default: + return new OtherInteractionDefinition($titleMap, $descriptionMap, $type); + } + } +} diff --git a/public/plugin/xapi/src/ToolExperience/Activity/Site.php b/public/plugin/xapi/src/ToolExperience/Activity/Site.php new file mode 100644 index 00000000000..c074df33f36 --- /dev/null +++ b/public/plugin/xapi/src/ToolExperience/Activity/Site.php @@ -0,0 +1,33 @@ + $platform]) + ) + ); + } +} diff --git a/public/plugin/xapi/src/ToolExperience/Actor/BaseActor.php b/public/plugin/xapi/src/ToolExperience/Actor/BaseActor.php new file mode 100644 index 00000000000..3ed8f00d3ae --- /dev/null +++ b/public/plugin/xapi/src/ToolExperience/Actor/BaseActor.php @@ -0,0 +1,17 @@ +user = $user; + } + + public function generate(): Agent + { + return new Agent( + InverseFunctionalIdentifier::withMbox( + IRI::fromString('mailto:'.$this->user->getEmail()) + ), + $this->user->getCompleteName() + ); + } +} diff --git a/public/plugin/xapi/src/ToolExperience/Statement/BaseStatement.php b/public/plugin/xapi/src/ToolExperience/Statement/BaseStatement.php new file mode 100644 index 00000000000..370de27e698 --- /dev/null +++ b/public/plugin/xapi/src/ToolExperience/Statement/BaseStatement.php @@ -0,0 +1,53 @@ +get(XApiPlugin::SETTING_UUID_NAMESPACE), + uniqid($type) + ); + + return StatementId::fromUuid($uuid); + } + + protected function generateContext(): Context + { + $platform = api_get_setting('Institution').' - '.api_get_setting('siteName'); + + $groupingActivities = []; + $groupingActivities[] = (new SiteActivity())->generate(); + + if (api_get_course_id()) { + $groupingActivities[] = (new CourseActivity())->generate(); + } + + return (new Context()) + ->withPlatform($platform) + ->withLanguage(api_get_language_isocode()) + ->withContextActivities( + new ContextActivities(null, $groupingActivities) + ); + } +} diff --git a/public/plugin/xapi/src/ToolExperience/Statement/LearningPathCompleted.php b/public/plugin/xapi/src/ToolExperience/Statement/LearningPathCompleted.php new file mode 100644 index 00000000000..7629a9a8ff4 --- /dev/null +++ b/public/plugin/xapi/src/ToolExperience/Statement/LearningPathCompleted.php @@ -0,0 +1,61 @@ +lpView = $lpView; + $this->lp = $lp; + } + + public function generate(): Statement + { + $user = api_get_user_entity($this->lpView->getUserId()); + $userActor = new UserActor($user); + $completedVerb = new Completed(); + $lpActivity = new LearningPathActivity($this->lp); + + return new Statement( + $this->generateStatementId('learning-path'), + $userActor->generate(), + $completedVerb->generate(), + $lpActivity->generate(), + new Result( + new Score(1, 100, 0, 100), + null, + true + ), + null, + api_get_utc_datetime(null, false, true), + null, + $this->generateContext() + ); + } +} diff --git a/public/plugin/xapi/src/ToolExperience/Statement/LearningPathItemViewed.php b/public/plugin/xapi/src/ToolExperience/Statement/LearningPathItemViewed.php new file mode 100644 index 00000000000..f2cc3942dfe --- /dev/null +++ b/public/plugin/xapi/src/ToolExperience/Statement/LearningPathItemViewed.php @@ -0,0 +1,79 @@ +lpItemView = $lpItemView; + $this->lpItem = $lpItem; + $this->lpView = $lpView; + } + + public function generate(): Statement + { + $user = api_get_user_entity($this->lpView->getUserId()); + $lp = Database::getManager()->find(CLp::class, $this->lpView->getLpId()); + + $userActor = new UserActor($user); + $viewedVerb = new ViewedVerb(); + $lpItemActivity = new LearningPathItemActivity($this->lpItem); + $lpActivity = new LearningPath($lp); + + $context = $this->generateContext(); + $contextActivities = $context + ->getContextActivities() + ->withAddedGroupingActivity($lpActivity->generate()); + + return new Statement( + $this->generateStatementId('learning-path-item'), + $userActor->generate(), + $viewedVerb->generate(), + $lpItemActivity->generate(), + new Result( + null, + null, + 'completed' === $this->lpItemView->getStatus(), + null, + 'PT'.$this->lpItemView->getTotalTime().'S' + ), + null, + api_get_utc_datetime(null, false, true), + null, + $context->withContextActivities($contextActivities) + ); + } +} diff --git a/public/plugin/xapi/src/ToolExperience/Statement/PortfolioAttachmentsTrait.php b/public/plugin/xapi/src/ToolExperience/Statement/PortfolioAttachmentsTrait.php new file mode 100644 index 00000000000..4e66182f01d --- /dev/null +++ b/public/plugin/xapi/src/ToolExperience/Statement/PortfolioAttachmentsTrait.php @@ -0,0 +1,89 @@ + $portfolioAttachments + * + * @return array + */ + protected function generateAttachments(array $portfolioAttachments, User $user): array + { + if (empty($portfolioAttachments)) { + return []; + } + + $attachments = []; + + $userDirectory = UserManager::getUserPathById($user->getId(), 'system'); + $attachmentsDirectory = $userDirectory.'portfolio_attachments/'; + + $langIso = api_get_language_isocode(); + + $cidreq = api_get_cidreq(); + $baseUrl = api_get_path(WEB_CODE_PATH).'portfolio/index.php?'.($cidreq ? $cidreq.'&' : ''); + + foreach ($portfolioAttachments as $portfolioAttachment) { + $attachmentFilename = $attachmentsDirectory.$portfolioAttachment->getPath(); + + $display = LanguageMap::create( + ['und' => $portfolioAttachment->getFilename()] + ); + $description = null; + + if ($portfolioAttachment->getComment()) { + $description = LanguageMap::create( + [$langIso => $portfolioAttachment->getComment()] + ); + } + + $attachments[] = new Attachment( + IRI::fromString('http://id.tincanapi.com/attachment/supporting_media'), + mime_content_type($attachmentFilename), + $portfolioAttachment->getSize(), + hash_file('sha256', $attachmentFilename), + $display, + $description, + IRL::fromString( + $baseUrl.http_build_query(['action' => 'download', 'file' => $portfolioAttachment->getPath()]) + ) + ); + } + + return $attachments; + } + + protected function generateAttachmentsForItem(Portfolio $item): array + { + $itemAttachments = \Database::getManager() + ->getRepository(PortfolioAttachment::class) + ->findFromItem($item) + ; + + return $this->generateAttachments($itemAttachments, $item->getUser()); + } + + protected function generateAttachmentsForComment(PortfolioCommentEntity $comment): array + { + $commentAttachments = \Database::getManager() + ->getRepository(PortfolioAttachment::class) + ->findFromComment($this->comment) + ; + + return $this->generateAttachments($commentAttachments, $comment->getAuthor()); + } +} diff --git a/public/plugin/xapi/src/ToolExperience/Statement/PortfolioComment.php b/public/plugin/xapi/src/ToolExperience/Statement/PortfolioComment.php new file mode 100644 index 00000000000..f967d6a1c10 --- /dev/null +++ b/public/plugin/xapi/src/ToolExperience/Statement/PortfolioComment.php @@ -0,0 +1,25 @@ +comment = $comment; + $this->item = $this->comment->getItem(); + $this->parentComment = $this->comment->getParent(); + } +} diff --git a/public/plugin/xapi/src/ToolExperience/Statement/PortfolioCommentEdited.php b/public/plugin/xapi/src/ToolExperience/Statement/PortfolioCommentEdited.php new file mode 100644 index 00000000000..5c2e2076e44 --- /dev/null +++ b/public/plugin/xapi/src/ToolExperience/Statement/PortfolioCommentEdited.php @@ -0,0 +1,39 @@ +comment); + $context = $this->generateContext(); + $attachements = $this->generateAttachmentsForComment($this->comment); + + return new Statement( + $this->generateStatementId('portfolio-comment'), + $actor->generate(), + $verb->generate(), + $object->generate(), + null, + null, + api_get_utc_datetime(null, false, true), + null, + $context, + $attachements + ); + } +} diff --git a/public/plugin/xapi/src/ToolExperience/Statement/PortfolioCommentScored.php b/public/plugin/xapi/src/ToolExperience/Statement/PortfolioCommentScored.php new file mode 100644 index 00000000000..2892173e4a9 --- /dev/null +++ b/public/plugin/xapi/src/ToolExperience/Statement/PortfolioCommentScored.php @@ -0,0 +1,47 @@ +comment->getScore(); + $scaled = $maxScore ? ($rawScore / $maxScore) : 0; + + $actor = new User($user); + $verb = new Scored(); + $object = new PortfolioCommentActivity($this->comment); + $context = $this->generateContext(); + $attachments = $this->generateAttachmentsForComment($this->comment); + $score = new Score($scaled, $rawScore, 0, $maxScore); + $result = new Result($score); + + return new Statement( + $this->generateStatementId('portfolio-comment'), + $actor->generate(), + $verb->generate(), + $object->generate(), + $result, + null, + api_get_utc_datetime(null, false, true), + null, + $context, + $attachments + ); + } +} diff --git a/public/plugin/xapi/src/ToolExperience/Statement/PortfolioDownloaded.php b/public/plugin/xapi/src/ToolExperience/Statement/PortfolioDownloaded.php new file mode 100644 index 00000000000..419f051ff32 --- /dev/null +++ b/public/plugin/xapi/src/ToolExperience/Statement/PortfolioDownloaded.php @@ -0,0 +1,42 @@ +owner = $owner; + } + + public function generate(): Statement + { + $user = api_get_user_entity(api_get_user_id()); + + $actor = new UserActor($user); + $verb = new Downloaded(); + $object = new PortfolioActivity($this->owner); + $context = $this->generateContext(); + + return new Statement( + $this->generateStatementId('portfolio-item'), + $actor->generate(), + $verb->generate(), + $object->generate(), + null, + null, + api_get_utc_datetime(null, false, true), + null, + $context + ); + } +} diff --git a/public/plugin/xapi/src/ToolExperience/Statement/PortfolioItem.php b/public/plugin/xapi/src/ToolExperience/Statement/PortfolioItem.php new file mode 100644 index 00000000000..bc8940eb926 --- /dev/null +++ b/public/plugin/xapi/src/ToolExperience/Statement/PortfolioItem.php @@ -0,0 +1,41 @@ +item = $item; + } + + protected function generateContext(): Context + { + $context = parent::generateContext(); + + $category = $this->item->getCategory(); + + if ($category) { + $categoryActivity = new PortfolioCategory($category); + + $contextActivities = $context + ->getContextActivities() + ->withAddedCategoryActivity( + $categoryActivity->generate() + ) + ; + + $context = $context->withContextActivities($contextActivities); + } + + return $context; + } +} diff --git a/public/plugin/xapi/src/ToolExperience/Statement/PortfolioItemCommented.php b/public/plugin/xapi/src/ToolExperience/Statement/PortfolioItemCommented.php new file mode 100644 index 00000000000..4f4e37cacbd --- /dev/null +++ b/public/plugin/xapi/src/ToolExperience/Statement/PortfolioItemCommented.php @@ -0,0 +1,74 @@ +generateStatementId('portfolio-comment'); + + $userActor = new UserActor($this->comment->getAuthor()); + $statementResult = new Result(null, null, null, $this->comment->getContent()); + + $context = $this->generateContext(); + + $em = \Database::getManager(); + $commentAttachments = $em->getRepository(PortfolioAttachment::class)->findFromComment($this->comment); + + $attachments = $this->generateAttachments( + $commentAttachments, + $this->comment->getAuthor() + ); + + if ($this->parentComment) { + $repliedVerb = new RepliedVerb(); + + $itemActivity = new PortfolioItemActivity($this->item); + $parentCommentActivity = new PortfolioCommentActivity($this->parentComment); + + $contextActivities = $context + ->getContextActivities() + ->withAddedGroupingActivity($itemActivity->generate()); + + return new Statement( + $statementId, + $userActor->generate(), + $repliedVerb->generate(), + $parentCommentActivity->generate(), + $statementResult, + null, + $this->comment->getDate(), + null, + $context->withContextActivities($contextActivities), + $attachments + ); + } else { + $itemShared = new PortfolioItemShared($this->item); + + $commentedVerb = new CommentedVerb(); + + return $itemShared->generate() + ->withId($statementId) + ->withActor($userActor->generate()) + ->withVerb($commentedVerb->generate()) + ->withStored($this->comment->getDate()) + ->withResult($statementResult) + ->withContext($context) + ->withAttachments($attachments); + } + } +} diff --git a/public/plugin/xapi/src/ToolExperience/Statement/PortfolioItemEdited.php b/public/plugin/xapi/src/ToolExperience/Statement/PortfolioItemEdited.php new file mode 100644 index 00000000000..f8ed7ae2e0c --- /dev/null +++ b/public/plugin/xapi/src/ToolExperience/Statement/PortfolioItemEdited.php @@ -0,0 +1,39 @@ +item); + $context = $this->generateContext(); + $attachements = $this->generateAttachmentsForItem($this->item); + + return new Statement( + $this->generateStatementId('portfolio-item'), + $actor->generate(), + $verb->generate(), + $object->generate(), + null, + null, + api_get_utc_datetime(null, false, true), + null, + $context, + $attachements + ); + } +} diff --git a/public/plugin/xapi/src/ToolExperience/Statement/PortfolioItemHighlighted.php b/public/plugin/xapi/src/ToolExperience/Statement/PortfolioItemHighlighted.php new file mode 100644 index 00000000000..b0d6a33bfee --- /dev/null +++ b/public/plugin/xapi/src/ToolExperience/Statement/PortfolioItemHighlighted.php @@ -0,0 +1,39 @@ +item); + $context = $this->generateContext(); + $attachments = $this->generateAttachmentsForItem($this->item); + + return new Statement( + $this->generateStatementId('portfolio-item'), + $actor->generate(), + $verb->generate(), + $object->generate(), + null, + null, + api_get_utc_datetime(null, false, true), + null, + $context, + $attachments + ); + } +} diff --git a/public/plugin/xapi/src/ToolExperience/Statement/PortfolioItemScored.php b/public/plugin/xapi/src/ToolExperience/Statement/PortfolioItemScored.php new file mode 100644 index 00000000000..73977a0d73a --- /dev/null +++ b/public/plugin/xapi/src/ToolExperience/Statement/PortfolioItemScored.php @@ -0,0 +1,47 @@ +item->getScore(); + $scaled = $maxScore ? ($rawScore / $maxScore) : 0; + + $actor = new User($user); + $verb = new Scored(); + $object = new PortfolioItemActivity($this->item); + $context = $this->generateContext(); + $attachments = $this->generateAttachmentsForItem($this->item); + $score = new Score($scaled, $rawScore, 0, $maxScore); + $result = new Result($score); + + return new Statement( + $this->generateStatementId('portfolio-item'), + $actor->generate(), + $verb->generate(), + $object->generate(), + $result, + null, + api_get_utc_datetime(null, false, true), + null, + $context, + $attachments + ); + } +} diff --git a/public/plugin/xapi/src/ToolExperience/Statement/PortfolioItemShared.php b/public/plugin/xapi/src/ToolExperience/Statement/PortfolioItemShared.php new file mode 100644 index 00000000000..ee44f0473c9 --- /dev/null +++ b/public/plugin/xapi/src/ToolExperience/Statement/PortfolioItemShared.php @@ -0,0 +1,47 @@ +item->getUser(); + + $userActor = new User($itemAuthor); + $sharedVerb = new Shared(); + $itemActivity = new PortfolioItem($this->item); + + $context = $this->generateContext(); + + $attachments = $this->generateAttachmentsForItem($this->item); + + return new Statement( + $this->generateStatementId('portfolio-item'), + $userActor->generate(), + $sharedVerb->generate(), + $itemActivity->generate(), + null, + null, + $this->item->getCreationDate(), + null, + $context, + $attachments + ); + } +} diff --git a/public/plugin/xapi/src/ToolExperience/Statement/PortfolioItemViewed.php b/public/plugin/xapi/src/ToolExperience/Statement/PortfolioItemViewed.php new file mode 100644 index 00000000000..3612ab08893 --- /dev/null +++ b/public/plugin/xapi/src/ToolExperience/Statement/PortfolioItemViewed.php @@ -0,0 +1,40 @@ +item); + $context = $this->generateContext(); + $attachments = $this->generateAttachmentsForItem($this->item); + + return new Statement( + $this->generateStatementId('portfolio-item'), + $actor->generate(), + $verb->generate(), + $object->generate(), + null, + null, + $this->item->getCreationDate(), + null, + $context, + $attachments + ); + } +} diff --git a/public/plugin/xapi/src/ToolExperience/Statement/QuizCompleted.php b/public/plugin/xapi/src/ToolExperience/Statement/QuizCompleted.php new file mode 100644 index 00000000000..2a4e9ba517e --- /dev/null +++ b/public/plugin/xapi/src/ToolExperience/Statement/QuizCompleted.php @@ -0,0 +1,70 @@ +exe = $exe; + $this->quiz = $quiz; + } + + public function generate(): Statement + { + $user = api_get_user_entity($this->exe->getExeUserId()); + + $userActor = new UserActor($user); + $completedVerb = new CompletedVerb(); + $quizActivity = new QuizActivity($this->quiz); + + $rawResult = $this->exe->getExeResult(); + $maxResult = $this->exe->getExeWeighting(); + $scaledResult = $rawResult / $maxResult; + + $duration = $this->exe->getExeDuration(); + + return new Statement( + $this->generateStatementId('exercise'), + $userActor->generate(), + $completedVerb->generate(), + $quizActivity->generate(), + new Result( + new Score($scaledResult, $rawResult, 0, $maxResult), + null, + true, + null, + $duration ? "PT{$duration}S" : null + ), + null, + $this->exe->getExeDate(), + null, + $this->generateContext() + ); + } +} diff --git a/public/plugin/xapi/src/ToolExperience/Statement/QuizQuestionAnswered.php b/public/plugin/xapi/src/ToolExperience/Statement/QuizQuestionAnswered.php new file mode 100644 index 00000000000..f891e4ce440 --- /dev/null +++ b/public/plugin/xapi/src/ToolExperience/Statement/QuizQuestionAnswered.php @@ -0,0 +1,79 @@ +attempt = $attempt; + $this->question = $question; + $this->quiz = $quiz; + } + + public function generate(): Statement + { + $user = api_get_user_entity($this->attempt->getUserId()); + + $userActor = new UserActor($user); + $answeredVerb = new AnsweredVerb(); + $questionActivity = new QuizQuestionActivity($this->question); + $quizActivity = new QuizActivity($this->quiz); + + $rawResult = $this->attempt->getMarks(); + $maxResult = $this->question->getPonderation(); + $scaledResult = $maxResult ? ($rawResult / $maxResult) : 0; + + $context = $this->generateContext(); + $contextActivities = $context + ->getContextActivities() + ->withAddedGroupingActivity($quizActivity->generate()); + + return new Statement( + $this->generateStatementId('exercise-question'), + $userActor->generate(), + $answeredVerb->generate(), + $questionActivity->generate(), + new Result( + new Score($scaledResult, $rawResult, null, $maxResult), + $rawResult > 0, + true + ), + null, + $this->attempt->getTms(), + null, + $context->withContextActivities($contextActivities) + ); + } +} diff --git a/public/plugin/xapi/src/ToolExperience/Verb/Answered.php b/public/plugin/xapi/src/ToolExperience/Verb/Answered.php new file mode 100644 index 00000000000..4007892f389 --- /dev/null +++ b/public/plugin/xapi/src/ToolExperience/Verb/Answered.php @@ -0,0 +1,21 @@ +iri = $iri; + $this->display = $display; + } + + public function generate(): Verb + { + $langIso = api_get_language_isocode(); + + return new Verb( + IRI::fromString($this->iri), + LanguageMap::create( + [ + $langIso => get_lang($this->display), + ] + ) + ); + } +} diff --git a/public/plugin/xapi/src/ToolExperience/Verb/Commented.php b/public/plugin/xapi/src/ToolExperience/Verb/Commented.php new file mode 100644 index 00000000000..34865a6b384 --- /dev/null +++ b/public/plugin/xapi/src/ToolExperience/Verb/Commented.php @@ -0,0 +1,21 @@ +', + ]; + $settings = [ + self::SETTING_UUID_NAMESPACE => 'text', + + self::SETTING_LRS_URL => 'text', + self::SETTING_LRS_AUTH_USERNAME => 'text', + self::SETTING_LRS_AUTH_PASSWORD => 'text', + + self::SETTING_CRON_LRS_URL => 'text', + self::SETTING_CRON_LRS_AUTH_USERNAME => 'text', + self::SETTING_CRON_LRS_AUTH_PASSWORD => 'text', + + self::SETTING_LRS_LP_ITEM_ACTIVE => 'boolean', + self::SETTING_LRS_LP_ACTIVE => 'boolean', + self::SETTING_LRS_QUIZ_ACTIVE => 'boolean', + self::SETTING_LRS_QUIZ_QUESTION_ACTIVE => 'boolean', + self::SETTING_LRS_PORTFOLIO_ACTIVE => 'boolean', + ]; + + parent::__construct( + $version, + implode(', ', $author), + $settings + ); + } + + /** + * @return \XApiPlugin + */ + public static function create() + { + static $result = null; + + return $result ? $result : $result = new self(); + } + + /** + * Process to install plugin. + */ + public function install() + { + $em = Database::getManager(); + + $tablesExists = $em->getConnection()->getSchemaManager()->tablesExist( + [ + 'xapi_shared_statement', + 'xapi_tool_launch', + 'xapi_lrs_auth', + 'xapi_cmi5_item', + 'xapi_activity_state', + 'xapi_activity_profile', + 'xapi_internal_log', + + 'xapi_attachment', + 'xapi_object', + 'xapi_result', + 'xapi_verb', + 'xapi_extensions', + 'xapi_context', + 'xapi_actor', + 'xapi_statement', + ] + ); + + if ($tablesExists) { + return; + } + + $this->installPluginDbTables(); + $this->installInitialConfig(); + $this->addCourseTools(); + $this->installHook(); + } + + /** + * Process to uninstall plugin. + */ + public function uninstall() + { + $this->uninstallHook(); + $this->uninstallPluginDbTables(); + $this->deleteCourseTools(); + } + + /** + * {@inheritdoc} + */ + public function uninstallHook() + { + $learningPathItemViewedHook = XApiLearningPathItemViewedHookObserver::create(); + $learningPathEndHook = XApiLearningPathEndHookObserver::create(); + $quizQuestionAnsweredHook = XApiQuizQuestionAnsweredHookObserver::create(); + $quizEndHook = XApiQuizEndHookObserver::create(); + $createCourseHook = XApiCreateCourseHookObserver::create(); + $portfolioItemAddedHook = XApiPortfolioItemAddedHookObserver::create(); + $portfolioItemCommentedHook = XApiPortfolioItemCommentedHookObserver::create(); + $portfolioItemHighlightedHook = XApiPortfolioItemHighlightedHookObserver::create(); + $portfolioDownloaded = XApiPortfolioDownloadedHookObserver::create(); + $portfolioItemScoredHook = XApiPortfolioItemScoredHookObserver::create(); + $portfolioCommentedScoredHook = XApiPortfolioCommentScoredHookObserver::create(); + $portfolioItemEditedHook = XApiPortfolioItemEditedHookObserver::create(); + $portfolioCommentEditedHook = XApiPortfolioCommentEditedHookObserver::create(); + + HookLearningPathItemViewed::create()->detach($learningPathItemViewedHook); + HookLearningPathEnd::create()->detach($learningPathEndHook); + HookQuizQuestionAnswered::create()->detach($quizQuestionAnsweredHook); + HookQuizEnd::create()->detach($quizEndHook); + HookCreateCourse::create()->detach($createCourseHook); + HookPortfolioItemAdded::create()->detach($portfolioItemAddedHook); + HookPortfolioItemCommented::create()->detach($portfolioItemCommentedHook); + HookPortfolioItemHighlighted::create()->detach($portfolioItemHighlightedHook); + HookPortfolioDownloaded::create()->detach($portfolioDownloaded); + HookPortfolioItemScored::create()->detach($portfolioItemScoredHook); + HookPortfolioCommentScored::create()->detach($portfolioCommentedScoredHook); + HookPortfolioItemEdited::create()->detach($portfolioItemEditedHook); + HookPortfolioCommentEdited::create()->detach($portfolioCommentEditedHook); + + return 1; + } + + public function uninstallPluginDbTables() + { + $em = Database::getManager(); + $pluginEm = self::getEntityManager(); + + $schemaTool = new SchemaTool($em); + $schemaTool->dropSchema( + [ + $em->getClassMetadata(ActivityProfile::class), + $em->getClassMetadata(ActivityState::class), + $em->getClassMetadata(SharedStatement::class), + $em->getClassMetadata(ToolLaunch::class), + $em->getClassMetadata(LrsAuth::class), + $em->getClassMetadata(Cmi5Item::class), + $em->getClassMetadata(InternalLog::class), + ] + ); + + $pluginSchemaTool = new SchemaTool($pluginEm); + $pluginSchemaTool->dropSchema( + [ + $pluginEm->getClassMetadata(\XApi\Repository\Doctrine\Mapping\Attachment::class), + $pluginEm->getClassMetadata(\XApi\Repository\Doctrine\Mapping\StatementObject::class), + $pluginEm->getClassMetadata(\XApi\Repository\Doctrine\Mapping\Result::class), + $pluginEm->getClassMetadata(\XApi\Repository\Doctrine\Mapping\Verb::class), + $pluginEm->getClassMetadata(\XApi\Repository\Doctrine\Mapping\Extensions::class), + $pluginEm->getClassMetadata(\XApi\Repository\Doctrine\Mapping\Context::class), + $pluginEm->getClassMetadata(\XApi\Repository\Doctrine\Mapping\Actor::class), + $pluginEm->getClassMetadata(\XApi\Repository\Doctrine\Mapping\Statement::class), + ] + ); + } + + /** + * @param string|null $lrsUrl + * @param string|null $lrsAuthUsername + * @param string|null $lrsAuthPassword + * + * @return \Xabbuh\XApi\Client\Api\StateApiClientInterface + */ + public function getXApiStateClient($lrsUrl = null, $lrsAuthUsername = null, $lrsAuthPassword = null) + { + return $this + ->createXApiClient($lrsUrl, $lrsAuthUsername, $lrsAuthPassword) + ->getStateApiClient(); + } + + public function getXApiStatementClient(): StatementsApiClientInterface + { + return $this->createXApiClient()->getStatementsApiClient(); + } + + public function getXapiStatementCronClient(): StatementsApiClientInterface + { + $lrsUrl = $this->get(self::SETTING_CRON_LRS_URL); + $lrsUsername = $this->get(self::SETTING_CRON_LRS_AUTH_USERNAME); + $lrsPassword = $this->get(self::SETTING_CRON_LRS_AUTH_PASSWORD); + + return $this + ->createXApiClient( + empty($lrsUrl) ? null : $lrsUrl, + empty($lrsUsername) ? null : $lrsUsername, + empty($lrsPassword) ? null : $lrsPassword + ) + ->getStatementsApiClient(); + } + + /** + * Perform actions after save the plugin configuration. + * + * @return \XApiPlugin + */ + public function performActionsAfterConfigure() + { + $learningPathItemViewedHook = XApiLearningPathItemViewedHookObserver::create(); + $learningPathEndHook = XApiLearningPathEndHookObserver::create(); + $quizQuestionAnsweredHook = XApiQuizQuestionAnsweredHookObserver::create(); + $quizEndHook = XApiQuizEndHookObserver::create(); + $portfolioItemAddedHook = XApiPortfolioItemAddedHookObserver::create(); + $portfolioItemCommentedHook = XApiPortfolioItemCommentedHookObserver::create(); + $portfolioItemViewedHook = XApiPortfolioItemViewedHookObserver::create(); + $portfolioItemHighlightedHook = XApiPortfolioItemHighlightedHookObserver::create(); + $portfolioDownloadedHook = XApiPortfolioDownloadedHookObserver::create(); + $portfolioItemScoredHook = XApiPortfolioItemScoredHookObserver::create(); + $portfolioCommentScoredHook = XApiPortfolioCommentScoredHookObserver::create(); + $portfolioItemEditedHook = XApiPortfolioItemEditedHookObserver::create(); + $portfolioCommentEditedHook = XApiPortfolioCommentEditedHookObserver::create(); + + $learningPathItemViewedEvent = HookLearningPathItemViewed::create(); + $learningPathEndEvent = HookLearningPathEnd::create(); + $quizQuestionAnsweredEvent = HookQuizQuestionAnswered::create(); + $quizEndEvent = HookQuizEnd::create(); + $portfolioItemAddedEvent = HookPortfolioItemAdded::create(); + $portfolioItemCommentedEvent = HookPortfolioItemCommented::create(); + $portfolioItemViewedEvent = HookPortfolioItemViewed::create(); + $portfolioItemHighlightedEvent = HookPortfolioItemHighlighted::create(); + $portfolioDownloadedEvent = HookPortfolioDownloaded::create(); + $portfolioItemScoredEvent = HookPortfolioItemScored::create(); + $portfolioCommentScoredEvent = HookPortfolioCommentScored::create(); + $portfolioItemEditedEvent = HookPortfolioItemEdited::create(); + $portfolioCommentEditedEvent = HookPortfolioCommentEdited::create(); + + if ('true' === $this->get(self::SETTING_LRS_LP_ITEM_ACTIVE)) { + $learningPathItemViewedEvent->attach($learningPathItemViewedHook); + } else { + $learningPathItemViewedEvent->detach($learningPathItemViewedHook); + } + + if ('true' === $this->get(self::SETTING_LRS_LP_ACTIVE)) { + $learningPathEndEvent->attach($learningPathEndHook); + } else { + $learningPathEndEvent->detach($learningPathEndHook); + } + + if ('true' === $this->get(self::SETTING_LRS_QUIZ_ACTIVE)) { + $quizQuestionAnsweredEvent->attach($quizQuestionAnsweredHook); + } else { + $quizQuestionAnsweredEvent->detach($quizQuestionAnsweredHook); + } + + if ('true' === $this->get(self::SETTING_LRS_QUIZ_QUESTION_ACTIVE)) { + $quizEndEvent->attach($quizEndHook); + } else { + $quizEndEvent->detach($quizEndHook); + } + + if ('true' === $this->get(self::SETTING_LRS_PORTFOLIO_ACTIVE)) { + $portfolioItemAddedEvent->attach($portfolioItemAddedHook); + $portfolioItemCommentedEvent->attach($portfolioItemCommentedHook); + $portfolioItemViewedEvent->attach($portfolioItemViewedHook); + $portfolioItemHighlightedEvent->attach($portfolioItemHighlightedHook); + $portfolioDownloadedEvent->attach($portfolioDownloadedHook); + $portfolioItemScoredEvent->attach($portfolioItemScoredHook); + $portfolioCommentScoredEvent->attach($portfolioCommentScoredHook); + $portfolioItemEditedEvent->attach($portfolioItemEditedHook); + $portfolioCommentEditedEvent->attach($portfolioCommentEditedHook); + } else { + $portfolioItemAddedEvent->detach($portfolioItemAddedHook); + $portfolioItemCommentedEvent->detach($portfolioItemCommentedHook); + $portfolioItemViewedEvent->detach($portfolioItemViewedHook); + $portfolioItemHighlightedEvent->detach($portfolioItemHighlightedHook); + $portfolioDownloadedEvent->detach($portfolioDownloadedHook); + $portfolioItemScoredEvent->detach($portfolioItemScoredHook); + $portfolioCommentScoredEvent->detach($portfolioCommentScoredHook); + $portfolioItemEditedEvent->detach($portfolioItemEditedHook); + $portfolioCommentEditedEvent->detach($portfolioCommentEditedHook); + } + + return $this; + } + + /** + * {@inheritdoc} + */ + public function installHook() + { + $createCourseHook = XApiCreateCourseHookObserver::create(); + + HookCreateCourse::create()->attach($createCourseHook); + } + + /** + * @param string $variable + * + * @return array + */ + public function getLangMap($variable) + { + $platformLanguage = api_get_setting('platformLanguage'); + $platformLanguageIso = api_get_language_isocode($platformLanguage); + + $map = []; + $map[$platformLanguageIso] = $this->getLangFromFile($variable, $platformLanguage); + + try { + $interfaceLanguage = api_get_interface_language(); + } catch (Exception $e) { + return $map; + } + + if (!empty($interfaceLanguage) && $platformLanguage !== $interfaceLanguage) { + $interfaceLanguageIso = api_get_language_isocode($interfaceLanguage); + + $map[$interfaceLanguageIso] = $this->getLangFromFile($variable, $interfaceLanguage); + } + + return $map; + } + + /** + * @param string $value + * @param string $type + * + * @return \Xabbuh\XApi\Model\IRI + */ + public function generateIri($value, $type) + { + return IRI::fromString( + api_get_path(WEB_PATH)."xapi/$type/$value" + ); + } + + /** + * @param int $courseId + */ + public function addCourseToolForTinCan($courseId) + { + // The $link param is set to "../plugin" as a hack to link correctly to the plugin URL in course tool. + // Otherwise, the link en the course tool will link to "/main/" URL. + $this->createLinkToCourseTool( + $this->get_lang('ToolTinCan'), + $courseId, + 'sessions_category.png', + '../plugin/xapi/start.php', + 0, + 'authoring' + ); + } + + /** + * @param string $language + * + * @return mixed|string + */ + public static function extractVerbInLanguage(Xabbuh\XApi\Model\LanguageMap $languageMap, $language) + { + $iso = self::findLanguageIso($languageMap->languageTags(), $language); + + $text = current($languageMap); + + if (isset($languageMap[$iso])) { + $text = trim($languageMap[$iso]); + } elseif (isset($languageMap['und'])) { + $text = $languageMap['und']; + } + + return $text; + } + + /** + * @param string $needle + * + * @return string + */ + public static function findLanguageIso(array $haystack, $needle) + { + if (in_array($needle, $haystack)) { + return $needle; + } + + foreach ($haystack as $language) { + if (strpos($language, $needle) === 0) { + return $language; + } + } + + return $haystack[0]; + } + + public function generateLaunchUrl( + $type, + $launchUrl, + $activityId, + Agent $actor, + $attemptId, + $customLrsUrl = null, + $customLrsUsername = null, + $customLrsPassword = null, + $viewSessionId = null + ) { + $lrsUrl = $customLrsUrl ?: $this->get(self::SETTING_LRS_URL); + $lrsAuthUsername = $customLrsUsername ?: $this->get(self::SETTING_LRS_AUTH_USERNAME); + $lrsAuthPassword = $customLrsPassword ?: $this->get(self::SETTING_LRS_AUTH_PASSWORD); + + $queryData = [ + 'endpoint' => trim($lrsUrl, "/ \t\n\r\0\x0B"), + 'actor' => Serializer::createSerializer()->serialize($actor, 'json'), + 'registration' => $attemptId, + ]; + + if ('tincan' === $type) { + $queryData['auth'] = 'Basic '.base64_encode(trim($lrsAuthUsername).':'.trim($lrsAuthPassword)); + $queryData['activity_id'] = $activityId; + } elseif ('cmi5' === $type) { + $queryData['fetch'] = api_get_path(WEB_PLUGIN_PATH).'xapi/cmi5/token.php?session='.$viewSessionId; + $queryData['activityId'] = $activityId; + } + + return $launchUrl.'?'.http_build_query($queryData, null, '&', PHP_QUERY_RFC3986); + } + + /** + * @return \Doctrine\ORM\EntityManager|null + */ + public static function getEntityManager() + { + $em = Database::getManager(); + + $prefixes = [ + __DIR__.'/../php-xapi/repository-doctrine-orm/metadata' => 'XApi\Repository\Doctrine\Mapping', + ]; + + $driver = new SimplifiedXmlDriver($prefixes); + $driver->setGlobalBasename('global'); + + $config = Database::getDoctrineConfig(api_get_configuration_value('root_sys')); + $config->setMetadataDriverImpl($driver); + + try { + return EntityManager::create($em->getConnection()->getParams(), $config); + } catch (ORMException $e) { + api_not_allowed(true, $e->getMessage()); + } + + return null; + } + + /** + * {@inheritdoc} + */ + public function getAdminUrl() + { + $webPath = api_get_path(WEB_PLUGIN_PATH).$this->get_name(); + + return "$webPath/admin.php"; + } + + public function getLpResourceBlock(int $lpId) + { + $cidReq = api_get_cidreq(true, true, 'lp'); + $webPath = api_get_path(WEB_PLUGIN_PATH).'xapi/'; + $course = api_get_course_entity(); + $session = api_get_session_entity(); + + $tools = Database::getManager() + ->getRepository(ToolLaunch::class) + ->findByCourseAndSession($course, $session); + + $importIcon = Display::return_icon('import_scorm.png'); + $moveIcon = Display::url( + Display::return_icon('move_everywhere.png', get_lang('Move'), [], ICON_SIZE_TINY), + '#', + ['class' => 'moved'] + ); + + $return = '
    • ' + .$importIcon + .Display::url( + get_lang('Import'), + $webPath."tool_import.php?$cidReq&".http_build_query(['lp_id' => $lpId]) + ) + .'
    • '; + + /** @var ToolLaunch $tool */ + foreach ($tools as $tool) { + $toolAnchor = Display::url( + Security::remove_XSS($tool->getTitle()), + api_get_self()."?$cidReq&" + .http_build_query( + ['action' => 'add_item', 'type' => TOOL_XAPI, 'file' => $tool->getId(), 'lp_id' => $lpId] + ), + ['class' => 'moved'] + ); + + $return .= Display::tag( + 'li', + $moveIcon.$importIcon.$toolAnchor, + [ + 'class' => 'lp_resource_element', + 'data_id' => $tool->getId(), + 'data_type' => TOOL_XAPI, + 'title' => $tool->getTitle(), + ] + ); + } + + $return .= '
    '; + + return $return; + } + + /** + * @throws \Doctrine\ORM\Tools\ToolsException + */ + private function installPluginDbTables() + { + $em = Database::getManager(); + $pluginEm = self::getEntityManager(); + + $schemaTool = new SchemaTool($em); + $schemaTool->createSchema( + [ + $em->getClassMetadata(SharedStatement::class), + $em->getClassMetadata(ToolLaunch::class), + $em->getClassMetadata(LrsAuth::class), + $em->getClassMetadata(Cmi5Item::class), + $em->getClassMetadata(ActivityState::class), + $em->getClassMetadata(ActivityProfile::class), + $em->getClassMetadata(InternalLog::class), + ] + ); + + $pluginSchemaTool = new SchemaTool($pluginEm); + $pluginSchemaTool->createSchema( + [ + $pluginEm->getClassMetadata(\XApi\Repository\Doctrine\Mapping\Attachment::class), + $pluginEm->getClassMetadata(\XApi\Repository\Doctrine\Mapping\StatementObject::class), + $pluginEm->getClassMetadata(\XApi\Repository\Doctrine\Mapping\Result::class), + $pluginEm->getClassMetadata(\XApi\Repository\Doctrine\Mapping\Verb::class), + $pluginEm->getClassMetadata(\XApi\Repository\Doctrine\Mapping\Extensions::class), + $pluginEm->getClassMetadata(\XApi\Repository\Doctrine\Mapping\Context::class), + $pluginEm->getClassMetadata(\XApi\Repository\Doctrine\Mapping\Actor::class), + $pluginEm->getClassMetadata(\XApi\Repository\Doctrine\Mapping\Statement::class), + ] + ); + } + + /** + * @throws \Exception + */ + private function installInitialConfig() + { + $uuidNamespace = Uuid::uuid1(); + + $pluginName = $this->get_name(); + $urlId = api_get_current_access_url_id(); + + api_add_setting( + $uuidNamespace, + $pluginName.'_'.self::SETTING_UUID_NAMESPACE, + $pluginName, + 'setting', + 'Plugins', + $pluginName, + '', + '', + '', + $urlId, + 1 + ); + + api_add_setting( + api_get_path(WEB_PATH).'plugin/xapi/lrs.php', + $pluginName.'_'.self::SETTING_LRS_URL, + $pluginName, + 'setting', + 'Plugins', + $pluginName, + '', + '', + '', + $urlId, + 1 + ); + } + + /** + * @param string|null $lrsUrl + * @param string|null $lrsAuthUsername + * @param string|null $lrsAuthPassword + * + * @return \Xabbuh\XApi\Client\XApiClientInterface + */ + private function createXApiClient($lrsUrl = null, $lrsAuthUsername = null, $lrsAuthPassword = null) + { + $baseUrl = $lrsUrl ?: $this->get(self::SETTING_LRS_URL); + $lrsAuthUsername = $lrsAuthUsername ?: $this->get(self::SETTING_LRS_AUTH_USERNAME); + $lrsAuthPassword = $lrsAuthPassword ?: $this->get(self::SETTING_LRS_AUTH_PASSWORD); + + $clientBuilder = new XApiClientBuilder(); + $clientBuilder + ->setHttpClient(Client::createWithConfig([RequestOptions::VERIFY => false])) + ->setRequestFactory(new GuzzleMessageFactory()) + ->setBaseUrl(trim($baseUrl, "/ \t\n\r\0\x0B")) + ->setAuth(trim($lrsAuthUsername), trim($lrsAuthPassword)); + + return $clientBuilder->build(); + } + + private function addCourseTools() + { + $courses = Database::getManager() + ->createQuery('SELECT c.id FROM ChamiloCoreBundle:Course c') + ->getResult(); + + foreach ($courses as $course) { + $this->addCourseToolForTinCan($course['id']); + } + } + + private function deleteCourseTools() + { + Database::getManager() + ->createQuery('DELETE FROM ChamiloCourseBundle:CTool t WHERE t.category = :category AND t.link LIKE :link') + ->execute(['category' => 'authoring', 'link' => '../plugin/xapi/start.php%']); + } +} diff --git a/public/plugin/xapi/start.php b/public/plugin/xapi/start.php new file mode 100644 index 00000000000..f3d305a4bad --- /dev/null +++ b/public/plugin/xapi/start.php @@ -0,0 +1,151 @@ +getRepository(ToolLaunch::class); + +$course = api_get_course_entity(); +$session = api_get_session_entity(); +$userInfo = api_get_user_info(); + +$cidReq = api_get_cidreq(); + +$table = new SortableTable( + 'tbl_xapi', + function () use ($em, $course, $session, $isAllowedToEdit) { + return $em->getRepository(ToolLaunch::class) + ->countByCourseAndSession($course, $session, !$isAllowedToEdit); + }, + function ($start, $limit, $orderBy, $orderDir) use ($toolLaunchRepo, $course, $session, $isAllowedToEdit) { + $tools = $toolLaunchRepo->findByCourseAndSession($course, $session, ['title' => $orderDir], $limit, $start); + + $data = []; + + /** @var ToolLaunch $toolLaunch */ + foreach ($tools as $toolLaunch) { + $wasAddedInLp = $toolLaunchRepo->wasAddedInLp($toolLaunch); + + if ($wasAddedInLp && !$isAllowedToEdit) { + continue; + } + + $datum = []; + $datum[] = [ + $toolLaunch->getId(), + $toolLaunch->getTitle(), + $toolLaunch->getDescription(), + $toolLaunch->getActivityType(), + $wasAddedInLp, + ]; + + if ($isAllowedToEdit) { + $datum[] = $toolLaunch->getId(); + } + + $data[] = $datum; + } + + return $data; + }, + 0 +); +$table->set_header(0, $plugin->get_lang('ActivityTitle'), true); +$table->set_column_filter( + 0, + function (array $toolInfo) use ($cidReq, $session, $userInfo, $plugin) { + list($id, $title, $description, $activityType, $wasAddedInLp) = $toolInfo; + + $sessionStar = api_get_session_image( + $session ? $session->getId() : 0, + $userInfo['status'] + ); + + $data = Display::url( + $title.$sessionStar, + ('cmi5' === $activityType ? 'cmi5/view.php' : 'tincan/view.php')."?id=$id&$cidReq", + ['class' => 'show'] + ); + + if ($description) { + $data .= PHP_EOL.Display::tag('small', $description, ['class' => 'text-muted']); + } + + if ($wasAddedInLp) { + $data .= Display::div( + $plugin->get_lang('ActivityAddedToLPCannotBeAccessed'), + ['class' => 'lp_content_type_label'] + ); + } + + return $data; + } +); + +if ($isAllowedToEdit) { + $thAttributes = ['class' => 'text-right', 'style' => 'width: 100px;']; + + $table->set_header(1, get_lang('Actions'), false, $thAttributes, $thAttributes); + $table->set_column_filter( + 1, + function ($id) use ($cidReq, $isAllowedToEdit) { + $actions = []; + + if ($isAllowedToEdit) { + $actions[] = Display::url( + Display::return_icon('statistics.png', get_lang('Reporting')), + "tincan/stats.php?$cidReq&id=$id" + ); + $actions[] = Display::url( + Display::return_icon('edit.png', get_lang('Edit')), + "tool_edit.php?$cidReq&edit=$id" + ); + $actions[] = Display::url( + Display::return_icon('delete.png', get_lang('Delete')), + "tool_delete.php?$cidReq&delete=$id" + ); + } + + return implode(PHP_EOL, $actions); + } + ); +} + +$pageTitle = $plugin->get_lang('ToolTinCan'); +$pageContent = Display::return_message($plugin->get_lang('NoActivities'), 'info'); + +if ($table->get_total_number_of_items() > 0) { + $pageContent = $table->return_table(); +} + +$view = new Template($pageTitle); +$view->assign('header', $pageTitle); + +if ($isAllowedToEdit) { + $actions = Display::url( + Display::return_icon('import_scorm.png', get_lang('Import'), [], ICON_SIZE_MEDIUM), + "tool_import.php?$cidReq" + ); + + $view->assign( + 'actions', + Display::toolbarAction( + 'xapi_actions', + [$actions] + ) + ); +} + +$view->assign('content', $pageContent); +$view->display_one_col_template(); diff --git a/public/plugin/xapi/tincan/launch.php b/public/plugin/xapi/tincan/launch.php new file mode 100644 index 00000000000..b61eee3bdeb --- /dev/null +++ b/public/plugin/xapi/tincan/launch.php @@ -0,0 +1,142 @@ +request->get('attempt_id'); +$toolLaunch = $em->find( + ToolLaunch::class, + $request->request->getInt('id') +); + +if (empty($attemptId) + || null === $toolLaunch + || $toolLaunch->getCourse()->getId() !== api_get_course_entity()->getId() +) { + api_not_allowed(true); +} + +$plugin = XApiPlugin::create(); + +$activity = new Activity( + IRI::fromString($toolLaunch->getActivityId()) +); +$actor = new Agent( + InverseFunctionalIdentifier::withMbox( + IRI::fromString('mailto:'.$user->getEmail()) + ), + $user->getCompleteName() +); +$state = new State( + $activity, + $actor, + $plugin->generateIri('tool-'.$toolLaunch->getId(), 'state')->getValue() +); + +$nowDate = api_get_utc_datetime(null, false, true)->format('c'); + +try { + $stateDocument = $plugin + ->getXApiStateClient( + $toolLaunch->getLrsUrl(), + $toolLaunch->getLrsAuthUsername(), + $toolLaunch->getLrsAuthPassword() + ) + ->getDocument($state); + + $data = $stateDocument->getData()->getData(); + + if ($stateDocument->offsetExists($attemptId)) { + $data[$attemptId][XApiPlugin::STATE_LAST_LAUNCH] = $nowDate; + } else { + $data[$attemptId] = [ + XApiPlugin::STATE_FIRST_LAUNCH => $nowDate, + XApiPlugin::STATE_LAST_LAUNCH => $nowDate, + ]; + } + + uasort( + $data, + function ($attemptA, $attemptB) { + $timeA = strtotime($attemptA[XApiPlugin::STATE_LAST_LAUNCH]); + $timeB = strtotime($attemptB[XApiPlugin::STATE_LAST_LAUNCH]); + + return $timeB - $timeA; + } + ); + + $documentData = new DocumentData($data); +} catch (NotFoundException $notFoundException) { + $documentData = new DocumentData( + [ + $attemptId => [ + XApiPlugin::STATE_FIRST_LAUNCH => $nowDate, + XApiPlugin::STATE_LAST_LAUNCH => $nowDate, + ], + ] + ); +} catch (Exception $exception) { + Display::addFlash( + Display::return_message($exception->getMessage(), 'error') + ); + + header('Location: '.api_get_course_url()); + exit; +} + +try { + $plugin + ->getXApiStateClient() + ->createOrReplaceDocument( + new StateDocument($state, $documentData) + ); +} catch (Exception $exception) { + Display::addFlash( + Display::return_message($exception->getMessage(), 'error') + ); + + header('Location: '.api_get_course_url()); + exit; +} + +$lrsUrl = $toolLaunch->getLrsUrl() ?: $plugin->get(XApiPlugin::SETTING_LRS_URL); +$lrsAuthUsername = $toolLaunch->getLrsAuthUsername() ?: $plugin->get(XApiPlugin::SETTING_LRS_AUTH_USERNAME); +$lrsAuthPassword = $toolLaunch->getLrsAuthPassword() ?: $plugin->get(XApiPlugin::SETTING_LRS_AUTH_PASSWORD); + +$activityLaunchUrl = $toolLaunch->getLaunchUrl().'?' + .http_build_query( + [ + 'endpoint' => trim($lrsUrl, "/ \t\n\r\0\x0B"), + 'auth' => 'Basic '.base64_encode(trim($lrsAuthUsername).':'.trim($lrsAuthPassword)), + 'actor' => Serializer::createSerializer()->serialize($actor, 'json'), + 'registration' => $attemptId, + 'activity_id' => $toolLaunch->getActivityId(), + ], + null, + '&', + PHP_QUERY_RFC3986 + ); + +header("Location: $activityLaunchUrl"); diff --git a/public/plugin/xapi/tincan/stats.php b/public/plugin/xapi/tincan/stats.php new file mode 100644 index 00000000000..b019db5d914 --- /dev/null +++ b/public/plugin/xapi/tincan/stats.php @@ -0,0 +1,163 @@ +find( + ToolLaunch::class, + $request->query->getInt('id') +); + +if (null === $toolLaunch) { + header('Location: '.api_get_course_url()); + exit; +} + +$course = api_get_course_entity(); +$session = api_get_session_entity(); + +$cidReq = api_get_cidreq(); + +$plugin = XApiPlugin::create(); + +$length = 20; +$page = $request->query->getInt('page', 1); +$start = ($page - 1) * $length; +$countStudentList = CourseManager::get_student_list_from_course_code( + $course->getCode(), + (bool) $session, + $session ? $session->getId() : 0, + null, + null, + true, + 0, + true +); + +$statsUrl = api_get_self().'?'.api_get_cidreq().'&id='.$toolLaunch->getId(); + +$paginator = new Paginator(); +$pagination = $paginator->paginate([]); +$pagination->setTotalItemCount($countStudentList); +$pagination->setItemNumberPerPage($length); +$pagination->setCurrentPageNumber($page); +$pagination->renderer = function ($data) use ($statsUrl) { + $render = ''; + if ($data['pageCount'] > 1) { + $render = '
      '; + for ($i = 1; $i <= $data['pageCount']; $i++) { + $pageContent = '
    • '.$i.'
    • '; + if ($data['current'] == $i) { + $pageContent = '
    • '.$i.'
    • '; + } + $render .= $pageContent; + } + $render .= '
    '; + } + + return $render; +}; + +$students = CourseManager::get_student_list_from_course_code( + $course->getCode(), + (bool) $session, + $session ? $session->getId() : 0, + null, + null, + true, + 0, + false, + $start, + $length +); + +$content = ''; +$content .= '
    '; + +$loadingMessage = Display::returnFontAwesomeIcon('spinner', '', true, 'fa-pulse').' '.get_lang('Loading'); + +foreach ($students as $studentInfo) { + $content .= Display::panelCollapse( + api_get_person_name($studentInfo['firstname'], $studentInfo['lastname']), + $loadingMessage, + "pnl-student-{$studentInfo['id']}", + [ + 'class' => 'pnl-student', + 'data-student' => $studentInfo['id'], + 'data-tool' => $toolLaunch->getId(), + ], + "pnl-student-{$studentInfo['id']}-accordion", + "pnl-student-{$studentInfo['id']}-collapse", + false + ); +} + +$content .= '
    '; +$content .= $pagination; + +// View +$interbreadcrumb[] = [ + 'name' => $plugin->get_title(), + 'url' => '../start.php', +]; + +$htmlHeadXtra[] = ""; + +$actions = Display::url( + Display::return_icon('back.png', get_lang('Back'), [], ICON_SIZE_MEDIUM), + "../start.php?$cidReq" +); + +$view = new Template($toolLaunch->getTitle()); +$view->assign( + 'actions', + Display::toolbarAction('xapi_actions', [$actions]) +); +$view->assign('header', $toolLaunch->getTitle()); +$view->assign('content', $content); +$view->display_one_col_template(); diff --git a/public/plugin/xapi/tincan/stats_attempts.ajax.php b/public/plugin/xapi/tincan/stats_attempts.ajax.php new file mode 100644 index 00000000000..8339518d9ad --- /dev/null +++ b/public/plugin/xapi/tincan/stats_attempts.ajax.php @@ -0,0 +1,131 @@ +isXmlHttpRequest() + || !api_is_allowed_to_edit() + || !$course +) { + echo Display::return_message(get_lang('NotAllowed'), 'error'); + exit; +} + +$plugin = XApiPlugin::create(); +$em = Database::getManager(); + +$toolLaunch = $em->find( + ToolLaunch::class, + $request->request->getInt('tool') +); + +$student = api_get_user_entity($request->request->getInt('student')); + +if (!$toolLaunch || !$student) { + echo Display::return_message(get_lang('NotAllowed'), 'error'); + exit; +} + +$userIsSubscribedToCourse = CourseManager::is_user_subscribed_in_course( + $student->getId(), + $course->getCode(), + (bool) $session, + $session ? $session->getId() : 0 +); + +if (!$userIsSubscribedToCourse) { + echo Display::return_message(get_lang('NotAllowed'), 'error'); + exit; +} + +$cidReq = api_get_cidreq(); + +$xApiStateClient = $plugin->getXApiStateClient( + $toolLaunch->getLrsUrl(), + $toolLaunch->getLrsAuthUsername(), + $toolLaunch->getLrsAuthPassword() +); + +$activity = new Activity( + IRI::fromString($toolLaunch->getActivityId()) +); + +$actor = new Agent( + InverseFunctionalIdentifier::withMbox( + IRI::fromString('mailto:'.$student->getEmail()) + ), + $student->getCompleteName() +); + +try { + $stateDocument = $xApiStateClient->getDocument( + new State( + $activity, + $actor, + $plugin->generateIri('tool-'.$toolLaunch->getId(), 'state')->getValue() + ) + ); +} catch (NotFoundException $notFoundException) { + echo Display::return_message(get_lang('NoResults'), 'warning'); + exit; +} catch (XApiException $exception) { + echo Display::return_message($exception->getMessage(), 'error'); + exit; +} + +$content = ''; + +if ($stateDocument) { + $i = 1; + + foreach ($stateDocument->getData()->getData() as $attemptId => $attempt) { + $firstLaunch = api_convert_and_format_date( + $attempt[XApiPlugin::STATE_FIRST_LAUNCH], + DATE_TIME_FORMAT_LONG + ); + $lastLaunch = api_convert_and_format_date( + $attempt[XApiPlugin::STATE_LAST_LAUNCH], + DATE_TIME_FORMAT_LONG + ); + + $content .= '
    ' + .'
    '.$plugin->get_lang('ActivityFirstLaunch').'
    ' + .'
    '.$firstLaunch.'
    ' + .'
    '.$plugin->get_lang('ActivityLastLaunch').'
    ' + .'
    '.$lastLaunch.'
    ' + .'
    ' + .Display::toolbarButton( + get_lang('ShowAllAttempts'), + '#', + 'th-list', + 'default', + [ + 'class' => 'btn_xapi_attempt_detail', + 'data-attempt' => $attemptId, + 'data-tool' => $toolLaunch->getId(), + 'style' => 'margin-bottom: 20px; margin-left: 180px;', + 'role' => 'button', + ] + ); + + $i++; + } +} + +echo $content; diff --git a/public/plugin/xapi/tincan/stats_statements.ajax.php b/public/plugin/xapi/tincan/stats_statements.ajax.php new file mode 100644 index 00000000000..6d14bf046a9 --- /dev/null +++ b/public/plugin/xapi/tincan/stats_statements.ajax.php @@ -0,0 +1,113 @@ +isXmlHttpRequest() + || !api_is_allowed_to_edit() + || !$course +) { + echo Display::return_message(get_lang('NotAllowed'), 'error'); + exit; +} + +$plugin = XApiPlugin::create(); +$em = Database::getManager(); + +$toolLaunch = $em->find( + ToolLaunch::class, + $request->request->getInt('tool') +); + +$attempt = $request->request->get('attempt'); + +if (!$toolLaunch || !$attempt) { + echo Display::return_message(get_lang('NoResults'), 'error'); + exit; +} + +$cidReq = api_get_cidreq(); + +$xapiStatementClient = $plugin->getXApiStatementClient(); + +$activity = new Activity( + IRI::fromString($toolLaunch->getActivityId()) +); + +$filter = new StatementsFilter(); +$filter + ->byRegistration($attempt); + +try { + $result = $xapiStatementClient->getStatements($filter); +} catch (XApiException $xApiException) { + echo Display::return_message($xApiException->getMessage(), 'error'); + exit; +} catch (Exception $exception) { + echo Display::return_message($exception->getMessage(), 'error'); + exit; +} + +$statements = $result->getStatements(); + +if (count($statements) <= 0) { + echo Display::return_message(get_lang('NoResults'), 'warning'); + exit; +} + +$table = new HTML_Table(['class' => 'table table-condensed table-bordered table-striped table-hover']); +$table->setHeaderContents(0, 0, get_lang('CreatedAt')); +$table->setHeaderContents(0, 1, $plugin->get_lang('Actor')); +$table->setHeaderContents(0, 2, $plugin->get_lang('Verb')); +$table->setHeaderContents(0, 3, $plugin->get_lang('ActivityId')); + +$i = 1; + +$languageIso = api_get_language_isocode(api_get_interface_language()); + +foreach ($statements as $statement) { + $timestampStored = $statement->getCreated() ? api_convert_and_format_date($statement->getCreated()) : '-'; + $actor = $statement->getActor()->getName(); + $verb = XApiPlugin::extractVerbInLanguage($statement->getVerb()->getDisplay(), $languageIso); + $activity = ''; + + $statementObject = $statement->getObject(); + + if ($statementObject instanceof Activity) { + if (null !== $statementObject->getDefinition()) { + $definition = $statementObject->getDefinition(); + + if (null !== $definition->getName()) { + $activity = XApiPlugin::extractVerbInLanguage($definition->getName(), $languageIso).'
    '; + } + } + + $activity .= Display::tag( + 'small', + $statementObject->getId()->getValue(), + ['class' => 'text-muted'] + ); + } + + $table->setCellContents($i, 0, $timestampStored); + $table->setCellContents($i, 1, $actor); + $table->setCellContents($i, 2, $verb); + $table->setCellContents($i, 3, $activity); + + $i++; +} + +$table->display(); diff --git a/public/plugin/xapi/tincan/view.php b/public/plugin/xapi/tincan/view.php new file mode 100644 index 00000000000..9c259d7f377 --- /dev/null +++ b/public/plugin/xapi/tincan/view.php @@ -0,0 +1,192 @@ +find( + ToolLaunch::class, + $request->query->getInt('id') +); + +if (null === $toolLaunch + || $toolLaunch->getCourse()->getId() !== api_get_course_entity()->getId() +) { + api_not_allowed(true); +} + +$plugin = XApiPlugin::create(); + +$activity = new Activity( + IRI::fromString($toolLaunch->getActivityId()) +); +$actor = new Agent( + InverseFunctionalIdentifier::withMbox( + IRI::fromString('mailto:'.$user->getEmail()) + ), + $user->getCompleteName() +); +$state = new State( + $activity, + $actor, + $plugin->generateIri('tool-'.$toolLaunch->getId(), 'state')->getValue() +); + +$cidReq = api_get_cidreq(); + +try { + $stateDocument = $plugin + ->getXApiStateClient( + $toolLaunch->getLrsUrl(), + $toolLaunch->getLrsAuthUsername(), + $toolLaunch->getLrsAuthPassword() + ) + ->getDocument($state); +} catch (NotFoundException $notFoundException) { + $stateDocument = null; +} catch (Exception $exception) { + Display::addFlash( + Display::return_message($exception->getMessage(), 'error') + ); + + header('Location: '.api_get_course_url()); + exit; +} + +$formTarget = $originIsLearnpath ? '_self' : '_blank'; + +$frmNewRegistration = new FormValidator( + 'launch_new', + 'post', + "launch.php?$cidReq", + '', + ['target' => $formTarget], + FormValidator::LAYOUT_INLINE +); +$frmNewRegistration->addHidden('attempt_id', Uuid::uuid4()); +$frmNewRegistration->addHidden('id', $toolLaunch->getId()); +$frmNewRegistration->addButton( + 'submit', + $plugin->get_lang('LaunchNewAttempt'), + 'external-link fa-fw', + 'success' +); + +if ($stateDocument) { + $row = 0; + + $table = new HTML_Table(['class' => 'table table-hover table-striped']); + $table->setHeaderContents($row, 0, $plugin->get_lang('ActivityFirstLaunch')); + $table->setHeaderContents($row, 1, $plugin->get_lang('ActivityLastLaunch')); + $table->setHeaderContents($row, 2, get_lang('Actions')); + + $row++; + + $langActivityLaunch = $plugin->get_lang('ActivityLaunch'); + + foreach ($stateDocument->getData()->getData() as $attemptId => $attempt) { + $firstLaunch = api_convert_and_format_date( + $attempt[XApiPlugin::STATE_FIRST_LAUNCH], + DATE_TIME_FORMAT_LONG + ); + $lastLaunch = api_convert_and_format_date( + $attempt[XApiPlugin::STATE_LAST_LAUNCH], + DATE_TIME_FORMAT_LONG + ); + + $frmLaunch = new FormValidator( + "launch_$row", + 'post', + "launch.php?$cidReq", + '', + ['target' => $formTarget], + FormValidator::LAYOUT_INLINE + ); + $frmLaunch->addHidden('attempt_id', $attemptId); + $frmLaunch->addHidden('id', $toolLaunch->getId()); + $frmLaunch->addButton( + 'submit', + $langActivityLaunch, + 'external-link fa-fw', + 'default' + ); + + $table->setCellContents($row, 0, $firstLaunch); + $table->setCellContents($row, 1, $lastLaunch); + $table->setCellContents($row, 2, $frmLaunch->returnForm()); + + $row++; + } + + $table->setColAttributes(0, ['class' => 'text-center']); + $table->setColAttributes(1, ['class' => 'text-center']); + $table->setColAttributes(2, ['class' => 'text-center']); +} + +$interbreadcrumb[] = ['url' => '../start.php', 'name' => $plugin->get_lang('ToolTinCan')]; + +$pageTitle = $toolLaunch->getTitle(); +$pageContent = ''; + +if ($toolLaunch->getDescription()) { + $pageContent .= PHP_EOL; + $pageContent .= "

    {$toolLaunch->getDescription()}

    "; +} + +if ($toolLaunch->isAllowMultipleAttempts() + || empty($stateDocument) +) { + $pageContent .= Display::div( + $frmNewRegistration->returnForm(), + ['class' => 'exercise_overview_options'] + ); +} + +if ($stateDocument) { + $pageContent .= $table->toHtml(); +} + +$actions = ''; + +if (!$originIsLearnpath) { + $actions = Display::url( + Display::return_icon('back.png', get_lang('Back'), [], ICON_SIZE_MEDIUM), + '../start.php?'.api_get_cidreq() + ); +} + +$view = new Template($pageTitle); +$view->assign('header', $pageTitle); + +if ($actions) { + $view->assign( + 'actions', + Display::toolbarAction( + 'xapi_actions', + [$actions] + ) + ); +} +$view->assign('content', $pageContent); +$view->display_one_col_template(); diff --git a/public/plugin/xapi/tool_delete.php b/public/plugin/xapi/tool_delete.php new file mode 100644 index 00000000000..910e4a76552 --- /dev/null +++ b/public/plugin/xapi/tool_delete.php @@ -0,0 +1,39 @@ +find( + ToolLaunch::class, + $request->query->getInt('delete') +); + +if (null === $toolLaunch + || $toolLaunch->getCourse()->getId() !== api_get_course_entity()->getId() +) { + api_not_allowed(true); +} + +$plugin = XApiPlugin::create(); + +$em = Database::getManager(); +$em->remove($toolLaunch); +$em->flush(); + +Display::addFlash( + Display::return_message($plugin->get_lang('ActivityDeleted'), 'success') +); + +header('Location: '.api_get_course_url()); +exit; diff --git a/public/plugin/xapi/tool_edit.php b/public/plugin/xapi/tool_edit.php new file mode 100644 index 00000000000..fec53f7b3e8 --- /dev/null +++ b/public/plugin/xapi/tool_edit.php @@ -0,0 +1,145 @@ +find( + ToolLaunch::class, + $request->query->getInt('edit') +); + +if (null === $toolLaunch) { + header('Location: '.api_get_course_url()); + exit; +} + +$course = api_get_course_entity(); +$session = api_get_session_entity(); + +$cidReq = api_get_cidreq(); + +$plugin = XApiPlugin::create(); + +$toolIsCmi5 = 'cmi5' === $toolLaunch->getActivityType(); +$toolIsTinCan = !$toolIsCmi5; + +$langEditActivity = $plugin->get_lang('EditActivity'); + +$frmActivity = new FormValidator('frm_activity', 'post', api_get_self()."?$cidReq&edit={$toolLaunch->getId()}"); +$frmActivity->addText('title', get_lang('Title')); +$frmActivity->addTextarea('description', get_lang('Description')); + +if ($toolIsTinCan) { + $frmActivity->addButtonAdvancedSettings('advanced_params'); + $frmActivity->addHtml(''); +} + +$frmActivity->addButtonAdvancedSettings('lrs_params', $plugin->get_lang('LrsConfiguration')); +$frmActivity->addHtml(''); +$frmActivity->addButtonUpdate(get_lang('Update')); +$frmActivity->applyFilter('title', 'trim'); +$frmActivity->applyFilter('description', 'trim'); +$frmActivity->applyFilter('lrs_url', 'trim'); +$frmActivity->applyFilter('lrs_auth', 'trim'); + +if ($frmActivity->validate()) { + $values = $frmActivity->exportValues(); + + $toolLaunch + ->setTitle($values['title']) + ->setDescription(empty($values['description']) ? null : $values['description']); + + if ($toolIsTinCan && isset($values['allow_multiple_attempts'])) { + $toolLaunch->setAllowMultipleAttempts(true); + } + + if (!empty($values['lrs_url']) + && !empty($values['lrs_auth_username']) + && !empty($values['lrs_auth_password']) + ) { + $toolLaunch + ->setLrsUrl($values['lrs_url']) + ->setLrsAuthUsername($values['lrs_auth_username']) + ->setLrsAuthPassword($values['lrs_auth_password']); + } + + $em->persist($toolLaunch); + $em->flush(); + + Display::addFlash( + Display::return_message($plugin->get_lang('ActivityUpdated'), 'success') + ); + + header('Location: '.api_get_course_url()); + exit; +} + +$frmActivity->setDefaults( + [ + 'title' => $toolLaunch->getTitle(), + 'description' => $toolLaunch->getDescription(), + 'allow_multiple_attempts' => $toolLaunch->isAllowMultipleAttempts(), + 'lrs_url' => $toolLaunch->getLrsUrl(), + 'lrs_auth_username' => $toolLaunch->getLrsAuthUsername(), + 'lrs_auth_password' => $toolLaunch->getLrsAuthPassword(), + ] +); + +$actions = Display::url( + Display::return_icon('back.png', get_lang('Back'), [], ICON_SIZE_MEDIUM), + 'start.php?'.api_get_cidreq() +); + +$pageContent = $frmActivity->returnForm(); + +$interbreadcrumb[] = ['url' => 'start.php', 'name' => $plugin->get_lang('ToolTinCan')]; + +$view = new Template($langEditActivity); +$view->assign('header', $langEditActivity); +$view->assign( + 'actions', + Display::toolbarAction( + 'xapi_actions', + [$actions] + ) +); +$view->assign('content', $pageContent); +$view->display_one_col_template(); diff --git a/public/plugin/xapi/tool_import.php b/public/plugin/xapi/tool_import.php new file mode 100644 index 00000000000..7c9fedc598e --- /dev/null +++ b/public/plugin/xapi/tool_import.php @@ -0,0 +1,177 @@ +query->has('lp_id')) { + $lp = new learnpath('', $httpRequest->query->getInt('lp_id'), $userId); + + if (!empty($lp->lp_id)) { + $pluginIndex = api_get_path(WEB_CODE_PATH)."lp/lp_controller.php?$cidReq&" + .http_build_query(['action' => 'add_item', 'type' => 'step', 'lp' => $lp->lp_id, 'lp_build_selected' => 8]); + } +} + +$langAddActivity = $plugin->get_lang('AddActivity'); + +$formAction = api_get_self()."?$cidReq&".($lp ? http_build_query(['lp_id' => $lp->lp_id]) : ''); + +$frmActivity = new FormValidator('frm_activity', 'post', $formAction); +$frmActivity->addFile('file', $plugin->get_lang('XApiPackage')); +$frmActivity->addButtonAdvancedSettings('advanced_params'); +$frmActivity->addHtml(''); +$frmActivity->addButtonAdvancedSettings('lrs_params', $plugin->get_lang('LrsConfiguration')); +$frmActivity->addHtml(''); +$frmActivity->addButtonImport(get_lang('Import')); +$frmActivity->addRule('file', get_lang('ThisFileIsRequired'), 'required'); +$frmActivity->addRule( + 'file', + $plugin->get_lang('OnlyZipOrXmlAllowed'), + 'filetype', + ['zip', 'xml'] +); +$frmActivity->applyFilter('title', 'trim'); +$frmActivity->applyFilter('description', 'trim'); +$frmActivity->applyFilter('lrs_url', 'trim'); +$frmActivity->applyFilter('lrs_auth', 'trim'); + +if ($frmActivity->validate()) { + $values = $frmActivity->exportValues(); + $zipFileInfo = $_FILES['file']; + + try { + $importer = PackageImporter::create($zipFileInfo, $course); + $packageFile = $importer->import(); + + $parser = PackageParser::create( + $importer->getPackageType(), + $packageFile, + $course, + $session + ); + $toolLaunch = $parser->parse(); + } catch (Exception $e) { + Display::addFlash( + Display::return_message($e->getMessage(), 'error') + ); + + header("Location: $pluginIndex"); + exit; + } + + if ('tincan' === $importer->getPackageType() && isset($values['allow_multiple_attempts'])) { + $toolLaunch->setAllowMultipleAttempts(true); + } + + if (!empty($values['title'])) { + $toolLaunch->setTitle($values['title']); + } + + if (!empty($values['description'])) { + $toolLaunch->setDescription($values['description']); + } + + if (!empty($values['lrs_url']) + && !empty($values['lrs_auth_username']) + && !empty($values['lrs_auth_password']) + ) { + $toolLaunch + ->setLrsUrl($values['lrs_url']) + ->setLrsAuthUsername($values['lrs_auth_username']) + ->setLrsAuthPassword($values['lrs_auth_password']); + } + + $em = Database::getManager(); + $em->persist($toolLaunch); + $em->flush(); + + Display::addFlash( + Display::return_message($plugin->get_lang('ActivityImported'), 'success') + ); + + header("Location: $pluginIndex"); + exit; +} + +$frmActivity->setDefaults(['allow_multiple_attempts' => true]); + +$actions = Display::url( + Display::return_icon('back.png', get_lang('Back'), [], ICON_SIZE_MEDIUM), + $pluginIndex +); + +$pageContent = $frmActivity->returnForm(); + +if ($lp) { + $interbreadcrumb[] = [ + 'url' => api_get_path(WEB_CODE_PATH).'lp/lp_controller.php?action=list&'.api_get_cidreq(), + 'name' => get_lang('LearningPaths'), + ]; + $interbreadcrumb[] = [ + 'url' => $pluginIndex, + 'name' => $lp->getNameNoTags(), + ]; +} else { + $interbreadcrumb[] = ['url' => $pluginIndex, 'name' => $plugin->get_lang('ToolTinCan')]; +} + +$view = new Template($langAddActivity); +$view->assign('header', $langAddActivity); +$view->assign( + 'actions', + Display::toolbarAction( + 'xapi_actions', + [$actions] + ) +); +$view->assign('content', $pageContent); +$view->display_one_col_template(); diff --git a/public/plugin/xapi/uninstall.php b/public/plugin/xapi/uninstall.php new file mode 100644 index 00000000000..3bcf5724756 --- /dev/null +++ b/public/plugin/xapi/uninstall.php @@ -0,0 +1,5 @@ +uninstall(); diff --git a/public/plugin/xapi/views/cmi5_launch.twig b/public/plugin/xapi/views/cmi5_launch.twig new file mode 100644 index 00000000000..8aa5f8359fe --- /dev/null +++ b/public/plugin/xapi/views/cmi5_launch.twig @@ -0,0 +1,15 @@ +
    +
    +
    +

    {{ tool.title }}

    +

    {{ tool.description }}

    + +
    + + {{ toc_html }} +
    +
    + +
    +
    +
    From 3bc565e0bbf9c413d5ca5d8c59fdc786ec6c2008 Mon Sep 17 00:00:00 2001 From: Angel Fernando Quiroz Campos <1697880+AngelFQC@users.noreply.github.com> Date: Wed, 3 Jul 2024 12:58:53 -0500 Subject: [PATCH 2/4] Minor: Format code --- public/plugin/xapi/admin.php | 28 ++++-- public/plugin/xapi/cmi5/launch.php | 16 ++-- public/plugin/xapi/cmi5/token.php | 2 + public/plugin/xapi/cmi5/view.php | 6 +- public/plugin/xapi/cron/send_statements.php | 22 +++-- public/plugin/xapi/install.php | 2 + public/plugin/xapi/lang/english.php | 2 + public/plugin/xapi/lang/french.php | 2 + public/plugin/xapi/lang/spanish.php | 2 + public/plugin/xapi/lrs.php | 2 + .../src/Controller/StatementGetController.php | 28 ++++-- .../Controller/StatementHeadController.php | 6 +- .../Controller/StatementPostController.php | 2 + .../src/Controller/StatementPutController.php | 5 +- .../src/DependencyInjection/Configuration.php | 26 ++--- .../DependencyInjection/XApiLrsExtension.php | 8 +- .../AlternateRequestSyntaxListener.php | 6 +- .../src/EventListener/ExceptionListener.php | 6 +- .../src/EventListener/SerializerListener.php | 5 +- .../src/EventListener/VersionListener.php | 6 +- .../src/Model/StatementsFilterFactory.php | 9 +- .../src/Response/AttachmentResponse.php | 26 ++--- .../src/Response/MultipartResponse.php | 22 ++--- .../php-xapi/lrs-bundle/src/XApiLrsBundle.php | 2 + .../src/StatementRepository.php | 13 +-- public/plugin/xapi/plugin.php | 2 + .../xapi/src/Entity/ActivityProfile.php | 18 ++-- .../plugin/xapi/src/Entity/ActivityState.php | 21 ++-- public/plugin/xapi/src/Entity/Cmi5Item.php | 95 +++++++++++-------- public/plugin/xapi/src/Entity/InternalLog.php | 39 +++++--- public/plugin/xapi/src/Entity/LrsAuth.php | 24 +++-- .../Repository/ToolLaunchRepository.php | 21 ++-- .../xapi/src/Entity/SharedStatement.php | 17 +++- .../src/Hook/XApiCreateCourseHookObserver.php | 13 +-- .../Hook/XApiLearningPathEndHookObserver.php | 7 +- ...XApiLearningPathItemViewedHookObserver.php | 8 +- ...XApiPortfolioCommentEditedHookObserver.php | 4 +- ...XApiPortfolioCommentScoredHookObserver.php | 4 +- .../XApiPortfolioDownloadedHookObserver.php | 4 +- .../XApiPortfolioItemAddedHookObserver.php | 10 +- ...XApiPortfolioItemCommentedHookObserver.php | 10 +- .../XApiPortfolioItemEditedHookObserver.php | 4 +- ...piPortfolioItemHighlightedHookObserver.php | 4 +- .../XApiPortfolioItemScoredHookObserver.php | 4 +- .../XApiPortfolioItemViewedHookObserver.php | 4 +- .../xapi/src/Hook/XApiQuizEndHookObserver.php | 10 +- .../XApiQuizQuestionAnsweredHookObserver.php | 10 +- .../xapi/src/Importer/PackageImporter.php | 17 ++-- .../xapi/src/Importer/XmlPackageImporter.php | 9 +- .../xapi/src/Importer/ZipPackageImporter.php | 13 +-- .../plugin/xapi/src/Lrs/AboutController.php | 4 +- .../src/Lrs/ActivitiesProfileController.php | 12 ++- .../src/Lrs/ActivitiesStateController.php | 7 +- public/plugin/xapi/src/Lrs/BaseController.php | 9 +- public/plugin/xapi/src/Lrs/LrsRequest.php | 35 ++++--- .../xapi/src/Lrs/StatementsController.php | 12 +-- .../xapi/src/Lrs/Utils/InternalLogUtil.php | 10 +- public/plugin/xapi/src/Parser/Cmi5Parser.php | 49 +++++----- .../plugin/xapi/src/Parser/PackageParser.php | 24 +++-- .../plugin/xapi/src/Parser/TinCanParser.php | 12 +-- .../src/ToolExperience/Activity/Course.php | 4 +- .../ToolExperience/Activity/LearningPath.php | 6 +- .../Activity/LearningPathItem.php | 6 +- .../src/ToolExperience/Activity/Portfolio.php | 6 +- .../Activity/PortfolioCategory.php | 6 +- .../Activity/PortfolioComment.php | 6 +- .../ToolExperience/Activity/PortfolioItem.php | 6 +- .../xapi/src/ToolExperience/Activity/Quiz.php | 6 +- .../ToolExperience/Activity/QuizQuestion.php | 14 ++- .../xapi/src/ToolExperience/Activity/Site.php | 4 +- .../src/ToolExperience/Actor/BaseActor.php | 4 +- .../xapi/src/ToolExperience/Actor/User.php | 4 +- .../Statement/BaseStatement.php | 7 +- .../Statement/LearningPathCompleted.php | 11 ++- .../Statement/LearningPathItemViewed.php | 15 +-- .../Statement/PortfolioAttachmentsTrait.php | 7 +- .../Statement/PortfolioComment.php | 16 +++- .../Statement/PortfolioCommentEdited.php | 2 + .../Statement/PortfolioCommentScored.php | 2 + .../Statement/PortfolioDownloaded.php | 6 +- .../Statement/PortfolioItem.php | 2 + .../Statement/PortfolioItemCommented.php | 34 ++++--- .../Statement/PortfolioItemEdited.php | 2 + .../Statement/PortfolioItemHighlighted.php | 2 + .../Statement/PortfolioItemScored.php | 2 + .../Statement/PortfolioItemShared.php | 4 +- .../Statement/PortfolioItemViewed.php | 2 + .../Statement/QuizCompleted.php | 9 +- .../Statement/QuizQuestionAnswered.php | 15 +-- .../xapi/src/ToolExperience/Verb/Answered.php | 4 +- .../xapi/src/ToolExperience/Verb/BaseVerb.php | 5 +- .../src/ToolExperience/Verb/Commented.php | 4 +- .../src/ToolExperience/Verb/Completed.php | 4 +- .../src/ToolExperience/Verb/Downloaded.php | 2 + .../xapi/src/ToolExperience/Verb/Edited.php | 4 +- .../src/ToolExperience/Verb/Highlighted.php | 2 + .../xapi/src/ToolExperience/Verb/Replied.php | 4 +- .../xapi/src/ToolExperience/Verb/Scored.php | 8 +- .../xapi/src/ToolExperience/Verb/Shared.php | 4 +- .../xapi/src/ToolExperience/Verb/Viewed.php | 4 +- public/plugin/xapi/start.php | 9 +- public/plugin/xapi/tincan/launch.php | 12 ++- public/plugin/xapi/tincan/stats.php | 3 + .../xapi/tincan/stats_attempts.ajax.php | 7 ++ .../xapi/tincan/stats_statements.ajax.php | 10 +- public/plugin/xapi/tincan/view.php | 10 +- public/plugin/xapi/tool_delete.php | 3 + public/plugin/xapi/tool_edit.php | 10 +- public/plugin/xapi/tool_import.php | 10 +- public/plugin/xapi/uninstall.php | 2 + 110 files changed, 676 insertions(+), 455 deletions(-) diff --git a/public/plugin/xapi/admin.php b/public/plugin/xapi/admin.php index 4affc59fd0f..9bacdc9e5be 100644 --- a/public/plugin/xapi/admin.php +++ b/public/plugin/xapi/admin.php @@ -1,8 +1,11 @@ setEnabled(isset($values['enabled'])) ->setCreatedAt( api_get_utc_datetime(null, false, true) - ); + ) + ; $em->persist($auth); $em->flush(); @@ -77,6 +81,7 @@ function createForm(LrsAuth $auth = null) ); header('Location: '.$pageBaseUrl); + exit; } @@ -85,7 +90,9 @@ function createForm(LrsAuth $auth = null) $pageBaseUrl ); $pageContent = $form->returnForm(); + break; + case 'edit': $auth = $em->find(LrsAuth::class, $request->query->getInt('id')); @@ -104,7 +111,8 @@ function createForm(LrsAuth $auth = null) ->setEnabled(isset($values['enabled'])) ->setCreatedAt( api_get_utc_datetime(null, false, true) - ); + ) + ; $em->persist($auth); $em->flush(); @@ -114,6 +122,7 @@ function createForm(LrsAuth $auth = null) ); header('Location: '.$pageBaseUrl); + exit; } @@ -122,7 +131,9 @@ function createForm(LrsAuth $auth = null) $pageBaseUrl ); $pageContent = $form->returnForm(); + break; + case 'delete': $auth = $em->find(LrsAuth::class, $request->query->getInt('id')); @@ -138,7 +149,9 @@ function createForm(LrsAuth $auth = null) ); header('Location: '.$pageBaseUrl); + exit; + case 'list': default: $pageActions = Display::url( @@ -177,11 +190,12 @@ function createForm(LrsAuth $auth = null) $table->setCellContents($row, 1, $auth->getPassword()); $table->setCellContents($row, 2, $auth->isEnabled() ? get_lang('Yes') : get_lang('No')); $table->setCellContents($row, 3, api_convert_and_format_date($auth->getCreatedAt())); - $table->setCellContents($row, 4, implode(PHP_EOL, $actions)); + $table->setCellContents($row, 4, implode(\PHP_EOL, $actions)); } $pageContent = $table->toHtml(); } + break; } diff --git a/public/plugin/xapi/cmi5/launch.php b/public/plugin/xapi/cmi5/launch.php index 9c9c65536b0..466e83ebb97 100644 --- a/public/plugin/xapi/cmi5/launch.php +++ b/public/plugin/xapi/cmi5/launch.php @@ -1,5 +1,7 @@ withLanguage(api_get_language_isocode()) - ->withRegistration($registration); + ->withRegistration($registration) +; $statementUuid = Uuid::uuid5( $plugin->get(XApiPlugin::SETTING_UUID_NAMESPACE), @@ -95,17 +98,17 @@ $statementClient = XApiPlugin::create()->getXApiStatementClient(); -//try { +// try { // $statementClient->storeStatement($statement); -//} catch (ConflictException $e) { +// } catch (ConflictException $e) { // echo Display::return_message($e->getMessage(), 'error'); // // exit; -//} catch (XApiException $e) { +// } catch (XApiException $e) { // echo Display::return_message($e->getMessage(), 'error'); // // exit; -//} +// } $viewSessionId = (string) Uuid::uuid4(); @@ -146,7 +149,8 @@ ->getXApiStateClient() ->createOrReplaceDocument( new StateDocument($state, $documentData) - ); + ) + ; } catch (Exception $exception) { echo Display::return_message($exception->getMessage(), 'error'); diff --git a/public/plugin/xapi/cmi5/token.php b/public/plugin/xapi/cmi5/token.php index be04b7e189e..d3228da2f09 100644 --- a/public/plugin/xapi/cmi5/token.php +++ b/public/plugin/xapi/cmi5/token.php @@ -1,5 +1,7 @@ getActivityType() ) { header('Location: '.api_get_course_url()); + exit; } @@ -42,7 +45,8 @@ ->from(Cmi5Item::class, 'item') ->where('item.tool = :tool') ->setParameter('tool', $toolLaunch->getId()) - ->getQuery(); + ->getQuery() +; $tocHtml = $itemsRepo->buildTree( $query->getArrayResult(), diff --git a/public/plugin/xapi/cron/send_statements.php b/public/plugin/xapi/cron/send_statements.php index 21cfc2bcbce..f19e3be941a 100644 --- a/public/plugin/xapi/cron/send_statements.php +++ b/public/plugin/xapi/cron/send_statements.php @@ -1,5 +1,7 @@ null, 'sent' => false], null, 100 - ); + ) +; $countNotSent = count($notSentSharedStatements); if ($countNotSent > 0) { - echo '['.time().'] Trying to send '.$countNotSent.' statements to LRS'.PHP_EOL; + echo '['.time().'] Trying to send '.$countNotSent.' statements to LRS'.\PHP_EOL; $client = XApiPlugin::create()->getXapiStatementCronClient(); @@ -55,25 +58,26 @@ echo "\t\tStatement ID received: \"{$sentStatement->getId()->getValue()}\""; } catch (ConflictException $e) { - echo $e->getMessage().PHP_EOL; + echo $e->getMessage().\PHP_EOL; continue; } catch (XApiException $e) { - echo $e->getMessage().PHP_EOL; + echo $e->getMessage().\PHP_EOL; continue; } $notSentSharedStatement ->setUuid($sentStatement->getId()->getValue()) - ->setSent(true); + ->setSent(true) + ; $em->persist($notSentSharedStatement); - echo "\t\tShared statement updated".PHP_EOL; + echo "\t\tShared statement updated".\PHP_EOL; } $em->flush(); } else { - echo 'No statements to process.'.PHP_EOL; + echo 'No statements to process.'.\PHP_EOL; } diff --git a/public/plugin/xapi/install.php b/public/plugin/xapi/install.php index c939bf5325f..96ac5bb5d57 100644 --- a/public/plugin/xapi/install.php +++ b/public/plugin/xapi/install.php @@ -1,5 +1,7 @@ install(); diff --git a/public/plugin/xapi/lang/english.php b/public/plugin/xapi/lang/english.php index bee4d9a2b8e..53ca13e3e0b 100644 --- a/public/plugin/xapi/lang/english.php +++ b/public/plugin/xapi/lang/english.php @@ -1,5 +1,7 @@ */ class StatementGetController { - protected static $getParameters = [ + protected static array $getParameters = [ 'statementId' => true, 'voidedStatementId' => true, 'agent' => true, @@ -65,17 +71,18 @@ public function __construct(StatementRepositoryInterface $repository, StatementS } /** - * @throws BadRequestHttpException if the query parameters does not comply with xAPI specification - * * @return Response + * + * @throws BadRequestHttpException if the query parameters does not comply with xAPI specification */ public function getStatement(Request $request) { - $query = new ParameterBag(\array_intersect_key($request->query->all(), self::$getParameters)); + $query = new ParameterBag(array_intersect_key($request->query->all(), self::$getParameters)); $this->validate($query); $includeAttachments = $query->filter('attachments', false, FILTER_VALIDATE_BOOLEAN); + try { if (($statementId = $query->get('statementId')) !== null) { $statement = $this->repository->findStatementById(StatementId::fromString($statementId)); @@ -92,14 +99,15 @@ public function getStatement(Request $request) } } catch (NotFoundException $e) { $response = $this->buildMultiStatementsResponse([], $query) - ->setStatusCode(Response::HTTP_NOT_FOUND) - ->setContent(''); - } catch (\Exception $exception) { + ->setStatusCode(Response::HTTP_NOT_FOUND) + ->setContent('') + ; + } catch (Exception $exception) { $response = Response::create('', Response::HTTP_BAD_REQUEST); } - $now = new \DateTime(); - $response->headers->set('X-Experience-API-Consistent-Through', $now->format(\DateTime::ATOM)); + $now = new DateTime(); + $response->headers->set('X-Experience-API-Consistent-Through', $now->format(DateTime::ATOM)); $response->headers->set('Content-Type', 'application/json'); return $response; @@ -171,7 +179,7 @@ protected function buildMultipartResponse(JsonResponse $statementResponse, array * * @throws BadRequestHttpException if the parameters does not comply with the xAPI specification */ - protected function validate(ParameterBag $query) + protected function validate(ParameterBag $query): void { $hasStatementId = $query->has('statementId'); $hasVoidedStatementId = $query->has('voidedStatementId'); diff --git a/public/plugin/xapi/php-xapi/lrs-bundle/src/Controller/StatementHeadController.php b/public/plugin/xapi/php-xapi/lrs-bundle/src/Controller/StatementHeadController.php index 224a441850a..b3b931bd8db 100644 --- a/public/plugin/xapi/php-xapi/lrs-bundle/src/Controller/StatementHeadController.php +++ b/public/plugin/xapi/php-xapi/lrs-bundle/src/Controller/StatementHeadController.php @@ -1,5 +1,7 @@ root('xapi_lrs') - ->beforeNormalization() - ->ifTrue(function ($v) { return isset($v['type']) && in_array($v['type'], ['mongodb', 'orm']) && !isset($v['object_manager_service']); }) - ->thenInvalid('You need to configure the object manager service when the repository type is "mongodb" or orm".') - ->end() - ->children() - ->enumNode('type') - ->isRequired() - ->values(['in_memory', 'mongodb', 'orm']) - ->end() - ->scalarNode('object_manager_service')->end() - ->end() + ->beforeNormalization() + ->ifTrue(function ($v) { + return isset($v['type']) && \in_array($v['type'], ['mongodb', 'orm']) && !isset($v['object_manager_service']); + }) + ->thenInvalid('You need to configure the object manager service when the repository type is "mongodb" or orm".') + ->end() + ->children() + ->enumNode('type') + ->isRequired() + ->values(['in_memory', 'mongodb', 'orm']) + ->end() + ->scalarNode('object_manager_service')->end() + ->end() ->end() ; diff --git a/public/plugin/xapi/php-xapi/lrs-bundle/src/DependencyInjection/XApiLrsExtension.php b/public/plugin/xapi/php-xapi/lrs-bundle/src/DependencyInjection/XApiLrsExtension.php index e2ff9e6d190..baa5b83bfbe 100644 --- a/public/plugin/xapi/php-xapi/lrs-bundle/src/DependencyInjection/XApiLrsExtension.php +++ b/public/plugin/xapi/php-xapi/lrs-bundle/src/DependencyInjection/XApiLrsExtension.php @@ -1,5 +1,7 @@ processConfiguration($configuration, $configs); @@ -36,19 +38,23 @@ public function load(array $configs, ContainerBuilder $container) switch ($config['type']) { case 'in_memory': break; + case 'mongodb': $loader->load('doctrine.xml'); $loader->load('mongodb.xml'); $container->setAlias('xapi_lrs.doctrine.object_manager', $config['object_manager_service']); $container->setAlias('xapi_lrs.repository.statement', 'xapi_lrs.repository.statement.doctrine'); + break; + case 'orm': $loader->load('doctrine.xml'); $loader->load('orm.xml'); $container->setAlias('xapi_lrs.doctrine.object_manager', $config['object_manager_service']); $container->setAlias('xapi_lrs.repository.statement', 'xapi_lrs.repository.statement.doctrine'); + break; } } diff --git a/public/plugin/xapi/php-xapi/lrs-bundle/src/EventListener/AlternateRequestSyntaxListener.php b/public/plugin/xapi/php-xapi/lrs-bundle/src/EventListener/AlternateRequestSyntaxListener.php index ef4a5a1eacf..22ecdfdb9de 100644 --- a/public/plugin/xapi/php-xapi/lrs-bundle/src/EventListener/AlternateRequestSyntaxListener.php +++ b/public/plugin/xapi/php-xapi/lrs-bundle/src/EventListener/AlternateRequestSyntaxListener.php @@ -1,5 +1,7 @@ isMasterRequest()) { return; @@ -61,7 +63,7 @@ public function onKernelRequest(GetResponseEvent $event) } foreach ($request->request as $key => $value) { - if (in_array($key, ['Authorization', 'X-Experience-API-Version', 'Content-Type', 'Content-Length', 'If-Match', 'If-None-Match'], true)) { + if (\in_array($key, ['Authorization', 'X-Experience-API-Version', 'Content-Type', 'Content-Length', 'If-Match', 'If-None-Match'], true)) { $request->headers->set($key, $value); } else { $request->query->set($key, $value); diff --git a/public/plugin/xapi/php-xapi/lrs-bundle/src/EventListener/ExceptionListener.php b/public/plugin/xapi/php-xapi/lrs-bundle/src/EventListener/ExceptionListener.php index 956f2f27e98..34b013972b3 100644 --- a/public/plugin/xapi/php-xapi/lrs-bundle/src/EventListener/ExceptionListener.php +++ b/public/plugin/xapi/php-xapi/lrs-bundle/src/EventListener/ExceptionListener.php @@ -1,5 +1,7 @@ statementSerializer = $statementSerializer; } - public function onKernelRequest(GetResponseEvent $event) + public function onKernelRequest(GetResponseEvent $event): void { $request = $event->getRequest(); @@ -31,6 +33,7 @@ public function onKernelRequest(GetResponseEvent $event) switch ($request->attributes->get('xapi_serializer')) { case 'statement': $request->attributes->set('statement', $this->statementSerializer->deserializeStatement($request->getContent())); + break; } } catch (BaseSerializerException $e) { diff --git a/public/plugin/xapi/php-xapi/lrs-bundle/src/EventListener/VersionListener.php b/public/plugin/xapi/php-xapi/lrs-bundle/src/EventListener/VersionListener.php index f82af7595ed..7b2cbe29ba2 100644 --- a/public/plugin/xapi/php-xapi/lrs-bundle/src/EventListener/VersionListener.php +++ b/public/plugin/xapi/php-xapi/lrs-bundle/src/EventListener/VersionListener.php @@ -1,5 +1,7 @@ isMasterRequest()) { return; @@ -47,7 +49,7 @@ public function onKernelRequest(GetResponseEvent $event) throw new BadRequestHttpException(sprintf('xAPI version "%s" is not supported.', $version)); } - public function onKernelResponse(FilterResponseEvent $event) + public function onKernelResponse(FilterResponseEvent $event): void { if (!$event->isMasterRequest()) { return; diff --git a/public/plugin/xapi/php-xapi/lrs-bundle/src/Model/StatementsFilterFactory.php b/public/plugin/xapi/php-xapi/lrs-bundle/src/Model/StatementsFilterFactory.php index 7e1ba67f46c..ba0e2c43c1a 100644 --- a/public/plugin/xapi/php-xapi/lrs-bundle/src/Model/StatementsFilterFactory.php +++ b/public/plugin/xapi/php-xapi/lrs-bundle/src/Model/StatementsFilterFactory.php @@ -1,5 +1,7 @@ */ @@ -66,11 +71,11 @@ public function createFromParameterBag(ParameterBag $parameters) } if (($since = $parameters->get('since')) !== null) { - $filter->since(\DateTime::createFromFormat(\DateTime::ATOM, $since)); + $filter->since(DateTime::createFromFormat(DateTime::ATOM, $since)); } if (($until = $parameters->get('until')) !== null) { - $filter->until(\DateTime::createFromFormat(\DateTime::ATOM, $until)); + $filter->until(DateTime::createFromFormat(DateTime::ATOM, $until)); } if ($parameters->filter('ascending', false, FILTER_VALIDATE_BOOLEAN)) { diff --git a/public/plugin/xapi/php-xapi/lrs-bundle/src/Response/AttachmentResponse.php b/public/plugin/xapi/php-xapi/lrs-bundle/src/Response/AttachmentResponse.php index 860de13f0fa..ea01fafbac3 100644 --- a/public/plugin/xapi/php-xapi/lrs-bundle/src/Response/AttachmentResponse.php +++ b/public/plugin/xapi/php-xapi/lrs-bundle/src/Response/AttachmentResponse.php @@ -1,5 +1,7 @@ attachment = $attachment; } - /** - * {@inheritdoc} - */ - public function prepare(Request $request) + public function prepare(Request $request): void { if (!$this->headers->has('Content-Type')) { $this->headers->set('Content-Type', $this->attachment->getContentType()); @@ -43,30 +43,24 @@ public function prepare(Request $request) } /** - * {@inheritdoc} - * - * @throws \LogicException + * @throws LogicException */ - public function sendContent() + public function sendContent(): void { - throw new \LogicException('An AttachmentResponse is only meant to be part of a multipart Response.'); + throw new LogicException('An AttachmentResponse is only meant to be part of a multipart Response.'); } /** - * {@inheritdoc} - * - * @throws \LogicException when the content is not null + * @throws LogicException when the content is not null */ - public function setContent($content) + public function setContent($content): void { if (null !== $content) { - throw new \LogicException('The content cannot be set on an AttachmentResponse instance.'); + throw new LogicException('The content cannot be set on an AttachmentResponse instance.'); } } /** - * {@inheritdoc} - * * @return string|null */ public function getContent() diff --git a/public/plugin/xapi/php-xapi/lrs-bundle/src/Response/MultipartResponse.php b/public/plugin/xapi/php-xapi/lrs-bundle/src/Response/MultipartResponse.php index a5cb9e1f5a1..2509f0e4406 100644 --- a/public/plugin/xapi/php-xapi/lrs-bundle/src/Response/MultipartResponse.php +++ b/public/plugin/xapi/php-xapi/lrs-bundle/src/Response/MultipartResponse.php @@ -1,5 +1,7 @@ getContent() !== null) { + if (null !== $part->getContent()) { $this->parts[] = $part; } @@ -76,9 +80,6 @@ public function setAttachmentsParts(array $attachmentsParts) return $this; } - /** - * {@inheritdoc} - */ public function prepare(Request $request) { foreach ($this->parts as $part) { @@ -91,9 +92,6 @@ public function prepare(Request $request) return parent::prepare($request); } - /** - * {@inheritdoc} - */ public function sendContent() { $content = ''; @@ -112,20 +110,16 @@ public function sendContent() } /** - * {@inheritdoc} - * - * @throws \LogicException when the content is not null + * @throws LogicException when the content is not null */ - public function setContent($content) + public function setContent($content): void { if (null !== $content) { - throw new \LogicException('The content cannot be set on a MultipartResponse instance.'); + throw new LogicException('The content cannot be set on a MultipartResponse instance.'); } } /** - * {@inheritdoc} - * * @return false */ public function getContent() diff --git a/public/plugin/xapi/php-xapi/lrs-bundle/src/XApiLrsBundle.php b/public/plugin/xapi/php-xapi/lrs-bundle/src/XApiLrsBundle.php index 33d7775ff18..ff6af63e1fb 100644 --- a/public/plugin/xapi/php-xapi/lrs-bundle/src/XApiLrsBundle.php +++ b/public/plugin/xapi/php-xapi/lrs-bundle/src/XApiLrsBundle.php @@ -1,5 +1,7 @@ 'ASC']); } - /** - * {@inheritdoc} - */ - public function storeStatement(Statement $mappedStatement, $flush = true) + public function storeStatement(Statement $mappedStatement, $flush = true): void { $this->_em->persist($mappedStatement); diff --git a/public/plugin/xapi/plugin.php b/public/plugin/xapi/plugin.php index 0e2770b43bc..bc7d829d927 100644 --- a/public/plugin/xapi/plugin.php +++ b/public/plugin/xapi/plugin.php @@ -1,5 +1,7 @@ get_info(); diff --git a/public/plugin/xapi/src/Entity/ActivityProfile.php b/public/plugin/xapi/src/Entity/ActivityProfile.php index 82f07523cc9..b4801dc1330 100644 --- a/public/plugin/xapi/src/Entity/ActivityProfile.php +++ b/public/plugin/xapi/src/Entity/ActivityProfile.php @@ -1,5 +1,7 @@ id; } - public function setId(int $id): ActivityProfile + public function setId(int $id): self { $this->id = $id; @@ -60,7 +66,7 @@ public function getProfileId(): string return $this->profileId; } - public function setProfileId(string $profileId): ActivityProfile + public function setProfileId(string $profileId): self { $this->profileId = $profileId; @@ -72,7 +78,7 @@ public function getActivityId(): string return $this->activityId; } - public function setActivityId(string $activityId): ActivityProfile + public function setActivityId(string $activityId): self { $this->activityId = $activityId; @@ -84,7 +90,7 @@ public function getDocumentData(): array return $this->documentData; } - public function setDocumentData(array $documentData): ActivityProfile + public function setDocumentData(array $documentData): self { $this->documentData = $documentData; diff --git a/public/plugin/xapi/src/Entity/ActivityState.php b/public/plugin/xapi/src/Entity/ActivityState.php index f63d03b66d4..f466ab6c70b 100644 --- a/public/plugin/xapi/src/Entity/ActivityState.php +++ b/public/plugin/xapi/src/Entity/ActivityState.php @@ -1,5 +1,7 @@ id; } - public function setId(int $id): ActivityState + public function setId(int $id): self { $this->id = $id; @@ -66,7 +73,7 @@ public function getStateId(): string return $this->stateId; } - public function setStateId(string $stateId): ActivityState + public function setStateId(string $stateId): self { $this->stateId = $stateId; @@ -78,7 +85,7 @@ public function getActivityId(): string return $this->activityId; } - public function setActivityId(string $activityId): ActivityState + public function setActivityId(string $activityId): self { $this->activityId = $activityId; @@ -90,7 +97,7 @@ public function getAgent(): array return $this->agent; } - public function setAgent(array $agent): ActivityState + public function setAgent(array $agent): self { $this->agent = $agent; @@ -102,7 +109,7 @@ public function getDocumentData(): array return $this->documentData; } - public function setDocumentData(array $documentData): ActivityState + public function setDocumentData(array $documentData): self { $this->documentData = $documentData; diff --git a/public/plugin/xapi/src/Entity/Cmi5Item.php b/public/plugin/xapi/src/Entity/Cmi5Item.php index c3f86deea04..4401b39ce57 100644 --- a/public/plugin/xapi/src/Entity/Cmi5Item.php +++ b/public/plugin/xapi/src/Entity/Cmi5Item.php @@ -1,5 +1,7 @@ children = new ArrayCollection(); @@ -163,7 +193,7 @@ public function getId(): int return $this->id; } - public function setId(int $id): Cmi5Item + public function setId(int $id): self { $this->id = $id; @@ -175,7 +205,7 @@ public function getIdentifier(): string return $this->identifier; } - public function setIdentifier(string $identifier): Cmi5Item + public function setIdentifier(string $identifier): self { $this->identifier = $identifier; @@ -187,7 +217,7 @@ public function getType(): string return $this->type; } - public function setType(string $type): Cmi5Item + public function setType(string $type): self { $this->type = $type; @@ -199,7 +229,7 @@ public function getTitle(): array return $this->title; } - public function setTitle(array $title): Cmi5Item + public function setTitle(array $title): self { $this->title = $title; @@ -211,7 +241,7 @@ public function getDescription(): array return $this->description; } - public function setDescription(array $description): Cmi5Item + public function setDescription(array $description): self { $this->description = $description; @@ -223,7 +253,7 @@ public function getUrl(): ?string return $this->url; } - public function setUrl(?string $url): Cmi5Item + public function setUrl(?string $url): self { $this->url = $url; @@ -235,7 +265,7 @@ public function getActivityType(): ?string return $this->activityType; } - public function setActivityType(?string $activityType): Cmi5Item + public function setActivityType(?string $activityType): self { $this->activityType = $activityType; @@ -247,7 +277,7 @@ public function getLaunchMethod(): ?string return $this->launchMethod; } - public function setLaunchMethod(?string $launchMethod): Cmi5Item + public function setLaunchMethod(?string $launchMethod): self { $this->launchMethod = $launchMethod; @@ -259,7 +289,7 @@ public function getMoveOn(): ?string return $this->moveOn; } - public function setMoveOn(?string $moveOn): Cmi5Item + public function setMoveOn(?string $moveOn): self { $this->moveOn = $moveOn; @@ -271,7 +301,7 @@ public function getMasteryScore(): ?float return $this->masteryScore; } - public function setMasteryScore(?float $masteryScore): Cmi5Item + public function setMasteryScore(?float $masteryScore): self { $this->masteryScore = $masteryScore; @@ -283,7 +313,7 @@ public function getLaunchParameters(): ?string return $this->launchParameters; } - public function setLaunchParameters(?string $launchParameters): Cmi5Item + public function setLaunchParameters(?string $launchParameters): self { $this->launchParameters = $launchParameters; @@ -295,25 +325,19 @@ public function getEntitlementKey(): ?string return $this->entitlementKey; } - public function setEntitlementKey(?string $entitlementKey): Cmi5Item + public function setEntitlementKey(?string $entitlementKey): self { $this->entitlementKey = $entitlementKey; return $this; } - /** - * @return \Chamilo\PluginBundle\Entity\XApi\Cmi5Item|null - */ - public function getParent(): ?Cmi5Item + public function getParent(): ?self { return $this->parent; } - /** - * @param \Chamilo\PluginBundle\Entity\XApi\Cmi5Item|null $parent - */ - public function setParent(?Cmi5Item $parent): Cmi5Item + public function setParent(?self $parent): self { $this->parent = $parent; @@ -325,7 +349,7 @@ public function getChildren(): ArrayCollection return $this->children; } - public function setChildren(ArrayCollection $children): Cmi5Item + public function setChildren(ArrayCollection $children): self { $this->children = $children; @@ -337,35 +361,26 @@ public function getStatus(): ?string return $this->status; } - public function setStatus(?string $status): Cmi5Item + public function setStatus(?string $status): self { $this->status = $status; return $this; } - /** - * @return \Chamilo\PluginBundle\Entity\XApi\ToolLaunch - */ public function getTool(): ToolLaunch { return $this->tool; } - /** - * @param \Chamilo\PluginBundle\Entity\XApi\ToolLaunch $tool - */ - public function setTool(ToolLaunch $tool): Cmi5Item + public function setTool(ToolLaunch $tool): self { $this->tool = $tool; return $this; } - /** - * @return \Chamilo\PluginBundle\Entity\XApi\Cmi5Item - */ - public function getRoot(): Cmi5Item + public function getRoot(): self { return $this->root; } diff --git a/public/plugin/xapi/src/Entity/InternalLog.php b/public/plugin/xapi/src/Entity/InternalLog.php index b9c807f94da..8e43a3a3020 100644 --- a/public/plugin/xapi/src/Entity/InternalLog.php +++ b/public/plugin/xapi/src/Entity/InternalLog.php @@ -1,5 +1,7 @@ user; } - public function setUser(User $user): InternalLog + public function setUser(User $user): self { $this->user = $user; @@ -110,7 +127,7 @@ public function getStatementId(): string return $this->statementId; } - public function setStatementId(string $statementId): InternalLog + public function setStatementId(string $statementId): self { $this->statementId = $statementId; @@ -122,7 +139,7 @@ public function getVerb(): string return $this->verb; } - public function setVerb(string $verb): InternalLog + public function setVerb(string $verb): self { $this->verb = $verb; @@ -134,7 +151,7 @@ public function getObjectId(): string return $this->objectId; } - public function setObjectId(string $objectId): InternalLog + public function setObjectId(string $objectId): self { $this->objectId = $objectId; @@ -146,7 +163,7 @@ public function getActivityName(): ?string return $this->activityName; } - public function setActivityName(?string $activityName): InternalLog + public function setActivityName(?string $activityName): self { $this->activityName = $activityName; @@ -158,7 +175,7 @@ public function getActivityDescription(): ?string return $this->activityDescription; } - public function setActivityDescription(?string $activityDescription): InternalLog + public function setActivityDescription(?string $activityDescription): self { $this->activityDescription = $activityDescription; @@ -170,7 +187,7 @@ public function getScoreScaled(): ?float return $this->scoreScaled; } - public function setScoreScaled(?float $scoreScaled): InternalLog + public function setScoreScaled(?float $scoreScaled): self { $this->scoreScaled = $scoreScaled; @@ -182,7 +199,7 @@ public function getScoreRaw(): ?float return $this->scoreRaw; } - public function setScoreRaw(?float $scoreRaw): InternalLog + public function setScoreRaw(?float $scoreRaw): self { $this->scoreRaw = $scoreRaw; @@ -194,7 +211,7 @@ public function getScoreMin(): ?float return $this->scoreMin; } - public function setScoreMin(?float $scoreMin): InternalLog + public function setScoreMin(?float $scoreMin): self { $this->scoreMin = $scoreMin; @@ -206,7 +223,7 @@ public function getScoreMax(): ?float return $this->scoreMax; } - public function setScoreMax(?float $scoreMax): InternalLog + public function setScoreMax(?float $scoreMax): self { $this->scoreMax = $scoreMax; @@ -218,7 +235,7 @@ public function getCreatedAt(): ?DateTime return $this->createdAt; } - public function setCreatedAt(?DateTime $createdAt): InternalLog + public function setCreatedAt(?DateTime $createdAt): self { $this->createdAt = $createdAt; diff --git a/public/plugin/xapi/src/Entity/LrsAuth.php b/public/plugin/xapi/src/Entity/LrsAuth.php index 69cc839ea4f..03b15d416eb 100644 --- a/public/plugin/xapi/src/Entity/LrsAuth.php +++ b/public/plugin/xapi/src/Entity/LrsAuth.php @@ -1,17 +1,19 @@ username; } - public function setUsername(string $username): LrsAuth + public function setUsername(string $username): self { $this->username = $username; @@ -71,7 +79,7 @@ public function getPassword(): string return $this->password; } - public function setPassword(string $password): LrsAuth + public function setPassword(string $password): self { $this->password = $password; @@ -83,19 +91,19 @@ public function isEnabled(): bool return $this->enabled; } - public function setEnabled(bool $enabled): LrsAuth + public function setEnabled(bool $enabled): self { $this->enabled = $enabled; return $this; } - public function getCreatedAt(): \DateTime + public function getCreatedAt(): DateTime { return $this->createdAt; } - public function setCreatedAt(\DateTime $createdAt): LrsAuth + public function setCreatedAt(DateTime $createdAt): self { $this->createdAt = $createdAt; diff --git a/public/plugin/xapi/src/Entity/Repository/ToolLaunchRepository.php b/public/plugin/xapi/src/Entity/Repository/ToolLaunchRepository.php index 0446c9fc093..e9431f3e23a 100644 --- a/public/plugin/xapi/src/Entity/Repository/ToolLaunchRepository.php +++ b/public/plugin/xapi/src/Entity/Repository/ToolLaunchRepository.php @@ -1,5 +1,7 @@ $course, @@ -38,16 +38,18 @@ public function findByCourseAndSession( return $this->findBy($criteria, $orderBy, $limit, $start); } - public function countByCourseAndSession(Course $course, Session $session = null, $filteredForStudent = false): int + public function countByCourseAndSession(Course $course, ?Session $session = null, $filteredForStudent = false): int { $qb = $this->createQueryBuilder('tl'); $qb->select($qb->expr()->count('tl')) ->where($qb->expr()->eq('tl.course', ':course')) - ->setParameter('course', $course); + ->setParameter('course', $course) + ; if ($session) { $qb->andWhere($qb->expr()->eq('tl.session', ':session')) - ->setParameter('session', $session); + ->setParameter('session', $session) + ; } else { $qb->andWhere($qb->expr()->isNull('tl.session')); } @@ -60,7 +62,8 @@ public function countByCourseAndSession(Course $course, Session $session = null, Join::WITH, "tl.id = lpi.path AND tl.course = lpi.cId AND lpi.itemType = 'xapi'" ) - ->andWhere($qb->expr()->isNull('lpi.path')); + ->andWhere($qb->expr()->isNull('lpi.path')) + ; } $query = $qb->getQuery(); diff --git a/public/plugin/xapi/src/Entity/SharedStatement.php b/public/plugin/xapi/src/Entity/SharedStatement.php index 5c4d81caad0..9475e3a2c13 100644 --- a/public/plugin/xapi/src/Entity/SharedStatement.php +++ b/public/plugin/xapi/src/Entity/SharedStatement.php @@ -1,5 +1,7 @@ uuid; } - public function setUuid(?string $uuid): SharedStatement + public function setUuid(?string $uuid): self { $this->uuid = $uuid; @@ -84,7 +91,7 @@ public function getStatement(): array return $this->statement; } - public function setStatement(array $statement): SharedStatement + public function setStatement(array $statement): self { $this->statement = $statement; @@ -96,7 +103,7 @@ public function isSent(): bool return $this->sent; } - public function setSent(bool $sent): SharedStatement + public function setSent(bool $sent): self { $this->sent = $sent; diff --git a/public/plugin/xapi/src/Hook/XApiCreateCourseHookObserver.php b/public/plugin/xapi/src/Hook/XApiCreateCourseHookObserver.php index dd5ca328b99..ebdbd0241d3 100644 --- a/public/plugin/xapi/src/Hook/XApiCreateCourseHookObserver.php +++ b/public/plugin/xapi/src/Hook/XApiCreateCourseHookObserver.php @@ -1,15 +1,11 @@ getEventData(); diff --git a/public/plugin/xapi/src/Hook/XApiLearningPathEndHookObserver.php b/public/plugin/xapi/src/Hook/XApiLearningPathEndHookObserver.php index 5993cfacc26..358ca3c1b89 100644 --- a/public/plugin/xapi/src/Hook/XApiLearningPathEndHookObserver.php +++ b/public/plugin/xapi/src/Hook/XApiLearningPathEndHookObserver.php @@ -1,15 +1,14 @@ getEventData(); $em = Database::getManager(); diff --git a/public/plugin/xapi/src/Hook/XApiLearningPathItemViewedHookObserver.php b/public/plugin/xapi/src/Hook/XApiLearningPathItemViewedHookObserver.php index 676b504c3d0..b9eb6d296da 100644 --- a/public/plugin/xapi/src/Hook/XApiLearningPathItemViewedHookObserver.php +++ b/public/plugin/xapi/src/Hook/XApiLearningPathItemViewedHookObserver.php @@ -1,17 +1,13 @@ getEventData(); diff --git a/public/plugin/xapi/src/Hook/XApiPortfolioCommentEditedHookObserver.php b/public/plugin/xapi/src/Hook/XApiPortfolioCommentEditedHookObserver.php index 4bd43bfd0c6..8f296c23182 100644 --- a/public/plugin/xapi/src/Hook/XApiPortfolioCommentEditedHookObserver.php +++ b/public/plugin/xapi/src/Hook/XApiPortfolioCommentEditedHookObserver.php @@ -1,5 +1,7 @@ getEventData()['comment']; diff --git a/public/plugin/xapi/src/Hook/XApiPortfolioCommentScoredHookObserver.php b/public/plugin/xapi/src/Hook/XApiPortfolioCommentScoredHookObserver.php index 3dcd0de5db5..ef0e0ecdbec 100644 --- a/public/plugin/xapi/src/Hook/XApiPortfolioCommentScoredHookObserver.php +++ b/public/plugin/xapi/src/Hook/XApiPortfolioCommentScoredHookObserver.php @@ -1,5 +1,7 @@ getEventData()['comment']; diff --git a/public/plugin/xapi/src/Hook/XApiPortfolioDownloadedHookObserver.php b/public/plugin/xapi/src/Hook/XApiPortfolioDownloadedHookObserver.php index b992006c1e6..18e4cd7a36e 100644 --- a/public/plugin/xapi/src/Hook/XApiPortfolioDownloadedHookObserver.php +++ b/public/plugin/xapi/src/Hook/XApiPortfolioDownloadedHookObserver.php @@ -1,11 +1,13 @@ getEventData()['owner']; diff --git a/public/plugin/xapi/src/Hook/XApiPortfolioItemAddedHookObserver.php b/public/plugin/xapi/src/Hook/XApiPortfolioItemAddedHookObserver.php index 5738a1f4fb3..55d26ad650f 100644 --- a/public/plugin/xapi/src/Hook/XApiPortfolioItemAddedHookObserver.php +++ b/public/plugin/xapi/src/Hook/XApiPortfolioItemAddedHookObserver.php @@ -1,18 +1,14 @@ getEventData()['portfolio']; diff --git a/public/plugin/xapi/src/Hook/XApiPortfolioItemCommentedHookObserver.php b/public/plugin/xapi/src/Hook/XApiPortfolioItemCommentedHookObserver.php index 32bc4a8d8a8..3b5bbc974b9 100644 --- a/public/plugin/xapi/src/Hook/XApiPortfolioItemCommentedHookObserver.php +++ b/public/plugin/xapi/src/Hook/XApiPortfolioItemCommentedHookObserver.php @@ -1,18 +1,14 @@ getEventData()['comment']; diff --git a/public/plugin/xapi/src/Hook/XApiPortfolioItemEditedHookObserver.php b/public/plugin/xapi/src/Hook/XApiPortfolioItemEditedHookObserver.php index a5535c2a27a..91db05f08a5 100644 --- a/public/plugin/xapi/src/Hook/XApiPortfolioItemEditedHookObserver.php +++ b/public/plugin/xapi/src/Hook/XApiPortfolioItemEditedHookObserver.php @@ -1,5 +1,7 @@ getEventData()['item']; diff --git a/public/plugin/xapi/src/Hook/XApiPortfolioItemHighlightedHookObserver.php b/public/plugin/xapi/src/Hook/XApiPortfolioItemHighlightedHookObserver.php index 9a7092329f8..df9f813cbdb 100644 --- a/public/plugin/xapi/src/Hook/XApiPortfolioItemHighlightedHookObserver.php +++ b/public/plugin/xapi/src/Hook/XApiPortfolioItemHighlightedHookObserver.php @@ -1,5 +1,7 @@ getEventData()['item']; diff --git a/public/plugin/xapi/src/Hook/XApiPortfolioItemScoredHookObserver.php b/public/plugin/xapi/src/Hook/XApiPortfolioItemScoredHookObserver.php index bed2dfb9427..d70e1bf509a 100644 --- a/public/plugin/xapi/src/Hook/XApiPortfolioItemScoredHookObserver.php +++ b/public/plugin/xapi/src/Hook/XApiPortfolioItemScoredHookObserver.php @@ -1,5 +1,7 @@ getEventData()['item']; diff --git a/public/plugin/xapi/src/Hook/XApiPortfolioItemViewedHookObserver.php b/public/plugin/xapi/src/Hook/XApiPortfolioItemViewedHookObserver.php index fbe52506644..f0a50c7cb24 100644 --- a/public/plugin/xapi/src/Hook/XApiPortfolioItemViewedHookObserver.php +++ b/public/plugin/xapi/src/Hook/XApiPortfolioItemViewedHookObserver.php @@ -1,5 +1,7 @@ getEventData()['portfolio']; diff --git a/public/plugin/xapi/src/Hook/XApiQuizEndHookObserver.php b/public/plugin/xapi/src/Hook/XApiQuizEndHookObserver.php index c8540795343..c52e3c9f665 100644 --- a/public/plugin/xapi/src/Hook/XApiQuizEndHookObserver.php +++ b/public/plugin/xapi/src/Hook/XApiQuizEndHookObserver.php @@ -1,18 +1,14 @@ getEventData(); $em = Database::getManager(); diff --git a/public/plugin/xapi/src/Hook/XApiQuizQuestionAnsweredHookObserver.php b/public/plugin/xapi/src/Hook/XApiQuizQuestionAnsweredHookObserver.php index f1645425591..217fb5e434e 100644 --- a/public/plugin/xapi/src/Hook/XApiQuizQuestionAnsweredHookObserver.php +++ b/public/plugin/xapi/src/Hook/XApiQuizQuestionAnsweredHookObserver.php @@ -1,5 +1,7 @@ getEventData(); diff --git a/public/plugin/xapi/src/Importer/PackageImporter.php b/public/plugin/xapi/src/Importer/PackageImporter.php index 25a8b4b1f60..90ca238323c 100644 --- a/public/plugin/xapi/src/Importer/PackageImporter.php +++ b/public/plugin/xapi/src/Importer/PackageImporter.php @@ -1,38 +1,39 @@ packageFileInfo = $fileInfo; @@ -54,9 +55,9 @@ public static function create(array $fileInfo, Course $course) } /** - * @throws \Exception - * * @return mixed + * + * @throws Exception */ abstract public function import(): string; diff --git a/public/plugin/xapi/src/Importer/XmlPackageImporter.php b/public/plugin/xapi/src/Importer/XmlPackageImporter.php index 2a48fddb375..c41384aba98 100644 --- a/public/plugin/xapi/src/Importer/XmlPackageImporter.php +++ b/public/plugin/xapi/src/Importer/XmlPackageImporter.php @@ -1,5 +1,7 @@ packageFileInfo['name'], ['tincan.xml', 'cmi5.xml'])) { + if (!\in_array($this->packageFileInfo['name'], ['tincan.xml', 'cmi5.xml'])) { throw new Exception('Invalid package'); } diff --git a/public/plugin/xapi/src/Importer/ZipPackageImporter.php b/public/plugin/xapi/src/Importer/ZipPackageImporter.php index ae5be37a286..0ddef299a2e 100644 --- a/public/plugin/xapi/src/Importer/ZipPackageImporter.php +++ b/public/plugin/xapi/src/Importer/ZipPackageImporter.php @@ -1,5 +1,7 @@ packageFileInfo['tmp_name']); @@ -31,7 +28,7 @@ function ($accumulator, $zipEntry) { throw new Exception("File \"{$zipEntry['filename']}\" contains a PHP script"); } - if (in_array($zipEntry['filename'], ['tincan.xml', 'cmi5.xml'])) { + if (\in_array($zipEntry['filename'], ['tincan.xml', 'cmi5.xml'])) { $this->packageType = explode('.', $zipEntry['filename'], 2)[0]; } @@ -55,9 +52,9 @@ function ($accumulator, $zipEntry) { } /** - * @throws \Exception + * @throws Exception */ - protected function validateEnoughSpace(int $packageSize) + protected function validateEnoughSpace(int $packageSize): void { $courseSpaceQuota = DocumentManager::get_course_quota($this->course->getCode()); diff --git a/public/plugin/xapi/src/Lrs/AboutController.php b/public/plugin/xapi/src/Lrs/AboutController.php index 084f2ddae68..df05b336af0 100644 --- a/public/plugin/xapi/src/Lrs/AboutController.php +++ b/public/plugin/xapi/src/Lrs/AboutController.php @@ -1,5 +1,7 @@ httpRequest->query->get('profileId'); $activityId = $this->httpRequest->query->get('activityId'); - $em = \Database::getManager(); + $em = Database::getManager(); $profileRepo = $em->getRepository(ActivityProfile::class); /** @var ActivityProfile $activityProfile */ @@ -50,7 +51,7 @@ public function put(): Response $activityId = $this->httpRequest->query->get('activityId'); $documentData = $this->httpRequest->getContent(); - $em = \Database::getManager(); + $em = Database::getManager(); $profileRepo = $em->getRepository(ActivityProfile::class); /** @var ActivityProfile $activityProfile */ @@ -65,7 +66,8 @@ public function put(): Response $activityProfile = new ActivityProfile(); $activityProfile ->setProfileId($profileId) - ->setActivityId($activityId); + ->setActivityId($activityId) + ; } $activityProfile->setDocumentData(json_decode($documentData, true)); diff --git a/public/plugin/xapi/src/Lrs/ActivitiesStateController.php b/public/plugin/xapi/src/Lrs/ActivitiesStateController.php index 6d05fc7b865..56c3398e390 100644 --- a/public/plugin/xapi/src/Lrs/ActivitiesStateController.php +++ b/public/plugin/xapi/src/Lrs/ActivitiesStateController.php @@ -1,4 +1,6 @@ setActivityId($activityId) ->setAgent(json_decode($agent, true)) - ->setStateId($stateId); + ->setStateId($stateId) + ; } else { $state = $em->find(ActivityState::class, $state['id']); } diff --git a/public/plugin/xapi/src/Lrs/BaseController.php b/public/plugin/xapi/src/Lrs/BaseController.php index 865b2b71b68..353cc4c68e5 100644 --- a/public/plugin/xapi/src/Lrs/BaseController.php +++ b/public/plugin/xapi/src/Lrs/BaseController.php @@ -1,5 +1,7 @@ httpRequest = $httpRequest; diff --git a/public/plugin/xapi/src/Lrs/LrsRequest.php b/public/plugin/xapi/src/Lrs/LrsRequest.php index f409f61943b..8b550e66c78 100644 --- a/public/plugin/xapi/src/Lrs/LrsRequest.php +++ b/public/plugin/xapi/src/Lrs/LrsRequest.php @@ -1,11 +1,14 @@ request = HttpRequest::createFromGlobals(); } - public function send() + public function send(): void { try { $this->alternateRequestSyntax(); @@ -50,7 +48,7 @@ public function send() $httpException->getMessage(), $httpException->getStatusCode() ); - } catch (\Exception $exception) { + } catch (Exception $exception) { $response = HttpResponse::create($exception->getMessage(), HttpResponse::HTTP_BAD_REQUEST); } @@ -60,7 +58,7 @@ public function send() } /** - * @throws \Xabbuh\XApi\Common\Exception\AccessDeniedException + * @throws AccessDeniedException */ private function validateAuth(): bool { @@ -80,7 +78,7 @@ private function validateAuth(): bool $parts = explode(':', $authDecoded, 2); - if (empty($parts) || count($parts) !== 2) { + if (empty($parts) || 2 !== \count($parts)) { throw new AccessDeniedException(); } @@ -90,7 +88,8 @@ private function validateAuth(): bool ->getRepository(LrsAuth::class) ->findOneBy( ['username' => $username, 'password' => $password, 'enabled' => true] - ); + ) + ; if (null == $auth) { throw new AccessDeniedException(); @@ -99,7 +98,7 @@ private function validateAuth(): bool return true; } - private function validateVersion() + private function validateVersion(): void { $version = $this->request->headers->get('X-Experience-API-Version'); @@ -131,7 +130,7 @@ private function getControllerName(): ?string $segments = array_map('ucfirst', $segments); $controllerName = implode('', $segments).'Controller'; - return "Chamilo\\PluginBundle\\XApi\Lrs\\$controllerName"; + return "Chamilo\\PluginBundle\\XApi\\Lrs\\$controllerName"; } private function getMethodName(): string @@ -142,7 +141,7 @@ private function getMethodName(): string } /** - * @throws \Xabbuh\XApi\Common\Exception\AccessDeniedException + * @throws AccessDeniedException */ private function generateResponse(string $controllerName, string $methodName): HttpResponse { @@ -152,23 +151,21 @@ private function generateResponse(string $controllerName, string $methodName): H throw new NotFoundHttpException(); } - if ($controllerName !== AboutController::class) { + if (AboutController::class !== $controllerName) { $this->validateAuth(); $this->validateVersion(); } /** @var HttpResponse $response */ - $response = call_user_func( + return \call_user_func( [ new $controllerName($this->request), $methodName, ] ); - - return $response; } - private function alternateRequestSyntax() + private function alternateRequestSyntax(): void { if ('POST' !== $this->request->getMethod()) { return; @@ -209,7 +206,7 @@ private function alternateRequestSyntax() ]; foreach ($this->request->request as $key => $value) { - if (in_array($key, $headerNames, true)) { + if (\in_array($key, $headerNames, true)) { $this->request->headers->set($key, $value); } else { $this->request->query->set($key, $value); diff --git a/public/plugin/xapi/src/Lrs/StatementsController.php b/public/plugin/xapi/src/Lrs/StatementsController.php index 86dc548b0ce..f212dd174c4 100644 --- a/public/plugin/xapi/src/Lrs/StatementsController.php +++ b/public/plugin/xapi/src/Lrs/StatementsController.php @@ -1,5 +1,7 @@ httpRequest->getContent(); - if (substr($content, 0, 1) !== '[') { + if ('[' !== substr($content, 0, 1)) { $content = "[$content]"; } @@ -128,10 +130,8 @@ public function post(): Response /** * @param array $statementsId - * - * @return void */ - private function saveLog(array $statementsId) + private function saveLog(array $statementsId): void { foreach ($statementsId as $statementId) { try { diff --git a/public/plugin/xapi/src/Lrs/Utils/InternalLogUtil.php b/public/plugin/xapi/src/Lrs/Utils/InternalLogUtil.php index dc9a1cbcd14..6f5518f51e5 100644 --- a/public/plugin/xapi/src/Lrs/Utils/InternalLogUtil.php +++ b/public/plugin/xapi/src/Lrs/Utils/InternalLogUtil.php @@ -1,5 +1,7 @@ getActor())) { return; @@ -35,7 +37,8 @@ public static function saveStatementForInternalLog(Statement $statement) $internalLog ->setUser($user) ->setVerb($statementVerbString) - ->setObjectId($statementObject->getId()->getValue()); + ->setObjectId($statementObject->getId()->getValue()) + ; if (null !== $statementId = $statement->getId()) { $internalLog->setStatementId($statementId->getValue()); @@ -69,7 +72,8 @@ public static function saveStatementForInternalLog(Statement $statement) ) ->setScoreMax( $score->getMax() - ); + ) + ; } } diff --git a/public/plugin/xapi/src/Parser/Cmi5Parser.php b/public/plugin/xapi/src/Parser/Cmi5Parser.php index 0d7cc96c340..a869edc378c 100644 --- a/public/plugin/xapi/src/Parser/Cmi5Parser.php +++ b/public/plugin/xapi/src/Parser/Cmi5Parser.php @@ -1,5 +1,7 @@ filePath); @@ -47,7 +44,8 @@ public function parse(): ToolLaunch ->setAllowMultipleAttempts(false) ->setCreatedAt(api_get_utc_datetime(null, false, true)) ->setCourse($this->course) - ->setSession($this->session); + ->setSession($this->session) + ; $toc = $this->generateToC($xml); @@ -87,7 +85,7 @@ private function generateToC(Crawler $xml) ->filterXPath('//*') ->reduce( function (Crawler $node, $i) { - return in_array($node->nodeName(), ['au', 'block']); + return \in_array($node->nodeName(), ['au', 'block']); } ) ->each( @@ -109,28 +107,30 @@ function (Crawler $node, $i) use (&$blocksMap) { $this->getLanguageStrings( $node->filterXPath('//description') ) - ); + ) + ; if ('au' === $node->nodeName()) { $launchParametersNode = $node->filterXPath('//launchParameters'); $entitlementKeyNode = $node->filterXPath('//entitlementKey'); $url = $item - ->setUrl( - $this->parseLaunchUrl( - trim($node->filterXPath('//url')->text()) + ->setUrl( + $this->parseLaunchUrl( + trim($node->filterXPath('//url')->text()) + ) ) - ) - ->setActivityType($activityType ?: null) - ->setLaunchMethod($launchMethod ?: null) - ->setMoveOn($moveOn ?: 'NotApplicable') - ->setMasteryScore((float) $masteryMode ?: null) - ->setLaunchParameters( - $launchParametersNode->count() > 0 ? trim($launchParametersNode->text()) : null - ) - ->setEntitlementKey( - $entitlementKeyNode->count() > 0 ? trim($entitlementKeyNode->text()) : null - ); + ->setActivityType($activityType ?: null) + ->setLaunchMethod($launchMethod ?: null) + ->setMoveOn($moveOn ?: 'NotApplicable') + ->setMasteryScore((float) $masteryMode ?: null) + ->setLaunchParameters( + $launchParametersNode->count() > 0 ? trim($launchParametersNode->text()) : null + ) + ->setEntitlementKey( + $entitlementKeyNode->count() > 0 ? trim($entitlementKeyNode->text()) : null + ) + ; } $parentNode = $node->parents()->first(); @@ -141,7 +141,8 @@ function (Crawler $node, $i) use (&$blocksMap) { return $item; } - ); + ) + ; foreach ($blocksMap as $itemPos => $parentIdentifier) { foreach ($items as $item) { @@ -167,7 +168,7 @@ private function parseLaunchUrl($url) $baseUrl = str_replace( api_get_path(SYS_COURSE_PATH), api_get_path(WEB_COURSE_PATH), - dirname($this->filePath) + \dirname($this->filePath) ); return "$baseUrl/$url"; diff --git a/public/plugin/xapi/src/Parser/PackageParser.php b/public/plugin/xapi/src/Parser/PackageParser.php index 0454eca522a..bca1575acad 100644 --- a/public/plugin/xapi/src/Parser/PackageParser.php +++ b/public/plugin/xapi/src/Parser/PackageParser.php @@ -1,16 +1,18 @@ filePath = $filePath; $this->course = $course; @@ -40,21 +42,23 @@ protected function __construct($filePath, Course $course, Session $session = nul } /** - * @throws \Exception - * * @return mixed + * + * @throws Exception */ - public static function create(string $packageType, string $filePath, Course $course, Session $session = null) + public static function create(string $packageType, string $filePath, Course $course, ?Session $session = null) { switch ($packageType) { case 'tincan': return new TinCanParser($filePath, $course, $session); + case 'cmi5': return new Cmi5Parser($filePath, $course, $session); + default: - throw new \Exception('Invalid package.'); + throw new Exception('Invalid package.'); } } - abstract public function parse(): \Chamilo\PluginBundle\Entity\XApi\ToolLaunch; + abstract public function parse(): ToolLaunch; } diff --git a/public/plugin/xapi/src/Parser/TinCanParser.php b/public/plugin/xapi/src/Parser/TinCanParser.php index c8de58dc137..af5b568cd4d 100644 --- a/public/plugin/xapi/src/Parser/TinCanParser.php +++ b/public/plugin/xapi/src/Parser/TinCanParser.php @@ -1,5 +1,7 @@ filePath); @@ -35,7 +32,8 @@ public function parse(): ToolLaunch ->setCreatedAt(api_get_utc_datetime(null, false, true)) ->setActivityId($activityNode->attr('id')) ->setActivityType($activityNode->attr('type')) - ->setLaunchUrl($this->parseLaunchUrl($nodeLaunch)); + ->setLaunchUrl($this->parseLaunchUrl($nodeLaunch)) + ; if ($nodeName) { $toolLaunch->setTitle($nodeName->text()); @@ -58,7 +56,7 @@ private function parseLaunchUrl(Crawler $launchNode): string $baseUrl = str_replace( api_get_path(SYS_COURSE_PATH), api_get_path(WEB_COURSE_PATH), - dirname($this->filePath) + \dirname($this->filePath) ); return "$baseUrl/$launchUrl"; diff --git a/public/plugin/xapi/src/ToolExperience/Activity/Course.php b/public/plugin/xapi/src/ToolExperience/Activity/Course.php index 63ce10e34ab..c9faaf817fa 100644 --- a/public/plugin/xapi/src/ToolExperience/Activity/Course.php +++ b/public/plugin/xapi/src/ToolExperience/Activity/Course.php @@ -1,5 +1,7 @@ autoId, 0, $objAnswer->nbrAnswers / 2); + $correctResponsesPattern = \array_slice($objAnswer->autoId, 0, $objAnswer->nbrAnswers / 2); return new SequencingInteractionDefinition( $titleMap, @@ -116,10 +118,12 @@ private function generateActivityDefinitionFromQuestionType() [implode('[,]', $correctResponsesPattern)], $choices ); + case MATCHING: case MATCHING_DRAGGABLE: /** @var array|InteractionComponent[] $source */ $source = []; + /** @var array|InteractionComponent[] $source */ $target = []; $correctResponsesPattern = []; @@ -149,8 +153,10 @@ private function generateActivityDefinitionFromQuestionType() $source, $target ); + case FREE_ANSWER: return new LongFillInInteractionDefinition($titleMap, $descriptionMap, $type); + case FILL_IN_BLANKS: case HOT_SPOT: case HOT_SPOT_DELINEATION: diff --git a/public/plugin/xapi/src/ToolExperience/Activity/Site.php b/public/plugin/xapi/src/ToolExperience/Activity/Site.php index c074df33f36..c757b10e046 100644 --- a/public/plugin/xapi/src/ToolExperience/Activity/Site.php +++ b/public/plugin/xapi/src/ToolExperience/Activity/Site.php @@ -1,5 +1,7 @@ withLanguage(api_get_language_isocode()) ->withContextActivities( new ContextActivities(null, $groupingActivities) - ); + ) + ; } } diff --git a/public/plugin/xapi/src/ToolExperience/Statement/LearningPathCompleted.php b/public/plugin/xapi/src/ToolExperience/Statement/LearningPathCompleted.php index 7629a9a8ff4..88f6ce4a1d8 100644 --- a/public/plugin/xapi/src/ToolExperience/Statement/LearningPathCompleted.php +++ b/public/plugin/xapi/src/ToolExperience/Statement/LearningPathCompleted.php @@ -1,10 +1,14 @@ generateContext(); $contextActivities = $context ->getContextActivities() - ->withAddedGroupingActivity($lpActivity->generate()); + ->withAddedGroupingActivity($lpActivity->generate()) + ; return new Statement( $this->generateStatementId('learning-path-item'), diff --git a/public/plugin/xapi/src/ToolExperience/Statement/PortfolioAttachmentsTrait.php b/public/plugin/xapi/src/ToolExperience/Statement/PortfolioAttachmentsTrait.php index 4e66182f01d..4b488d9dda5 100644 --- a/public/plugin/xapi/src/ToolExperience/Statement/PortfolioAttachmentsTrait.php +++ b/public/plugin/xapi/src/ToolExperience/Statement/PortfolioAttachmentsTrait.php @@ -1,5 +1,7 @@ getRepository(PortfolioAttachment::class) ->findFromItem($item) ; @@ -79,7 +82,7 @@ protected function generateAttachmentsForItem(Portfolio $item): array protected function generateAttachmentsForComment(PortfolioCommentEntity $comment): array { - $commentAttachments = \Database::getManager() + $commentAttachments = Database::getManager() ->getRepository(PortfolioAttachment::class) ->findFromComment($this->comment) ; diff --git a/public/plugin/xapi/src/ToolExperience/Statement/PortfolioComment.php b/public/plugin/xapi/src/ToolExperience/Statement/PortfolioComment.php index f967d6a1c10..9b2057a9314 100644 --- a/public/plugin/xapi/src/ToolExperience/Statement/PortfolioComment.php +++ b/public/plugin/xapi/src/ToolExperience/Statement/PortfolioComment.php @@ -1,5 +1,7 @@ generateContext(); - $em = \Database::getManager(); + $em = Database::getManager(); $commentAttachments = $em->getRepository(PortfolioAttachment::class)->findFromComment($this->comment); $attachments = $this->generateAttachments( @@ -42,7 +45,8 @@ public function generate(): Statement $contextActivities = $context ->getContextActivities() - ->withAddedGroupingActivity($itemActivity->generate()); + ->withAddedGroupingActivity($itemActivity->generate()) + ; return new Statement( $statementId, @@ -56,19 +60,19 @@ public function generate(): Statement $context->withContextActivities($contextActivities), $attachments ); - } else { - $itemShared = new PortfolioItemShared($this->item); - - $commentedVerb = new CommentedVerb(); - - return $itemShared->generate() - ->withId($statementId) - ->withActor($userActor->generate()) - ->withVerb($commentedVerb->generate()) - ->withStored($this->comment->getDate()) - ->withResult($statementResult) - ->withContext($context) - ->withAttachments($attachments); } + $itemShared = new PortfolioItemShared($this->item); + + $commentedVerb = new CommentedVerb(); + + return $itemShared->generate() + ->withId($statementId) + ->withActor($userActor->generate()) + ->withVerb($commentedVerb->generate()) + ->withStored($this->comment->getDate()) + ->withResult($statementResult) + ->withContext($context) + ->withAttachments($attachments) + ; } } diff --git a/public/plugin/xapi/src/ToolExperience/Statement/PortfolioItemEdited.php b/public/plugin/xapi/src/ToolExperience/Statement/PortfolioItemEdited.php index f8ed7ae2e0c..4cf0e4bf958 100644 --- a/public/plugin/xapi/src/ToolExperience/Statement/PortfolioItemEdited.php +++ b/public/plugin/xapi/src/ToolExperience/Statement/PortfolioItemEdited.php @@ -1,5 +1,7 @@ generateContext(); $contextActivities = $context ->getContextActivities() - ->withAddedGroupingActivity($quizActivity->generate()); + ->withAddedGroupingActivity($quizActivity->generate()) + ; return new Statement( $this->generateStatementId('exercise-question'), diff --git a/public/plugin/xapi/src/ToolExperience/Verb/Answered.php b/public/plugin/xapi/src/ToolExperience/Verb/Answered.php index 4007892f389..c92b4fe205c 100644 --- a/public/plugin/xapi/src/ToolExperience/Verb/Answered.php +++ b/public/plugin/xapi/src/ToolExperience/Verb/Answered.php @@ -1,13 +1,13 @@ getRepository(ToolLaunch::class) - ->countByCourseAndSession($course, $session, !$isAllowedToEdit); + ->countByCourseAndSession($course, $session, !$isAllowedToEdit) + ; }, function ($start, $limit, $orderBy, $orderDir) use ($toolLaunchRepo, $course, $session, $isAllowedToEdit) { $tools = $toolLaunchRepo->findByCourseAndSession($course, $session, ['title' => $orderDir], $limit, $start); @@ -79,7 +82,7 @@ function (array $toolInfo) use ($cidReq, $session, $userInfo, $plugin) { ); if ($description) { - $data .= PHP_EOL.Display::tag('small', $description, ['class' => 'text-muted']); + $data .= \PHP_EOL.Display::tag('small', $description, ['class' => 'text-muted']); } if ($wasAddedInLp) { @@ -117,7 +120,7 @@ function ($id) use ($cidReq, $isAllowedToEdit) { ); } - return implode(PHP_EOL, $actions); + return implode(\PHP_EOL, $actions); } ); } diff --git a/public/plugin/xapi/tincan/launch.php b/public/plugin/xapi/tincan/launch.php index b61eee3bdeb..459721ca2c4 100644 --- a/public/plugin/xapi/tincan/launch.php +++ b/public/plugin/xapi/tincan/launch.php @@ -1,5 +1,7 @@ getLrsAuthUsername(), $toolLaunch->getLrsAuthPassword() ) - ->getDocument($state); + ->getDocument($state) + ; $data = $stateDocument->getData()->getData(); @@ -103,6 +106,7 @@ function ($attemptA, $attemptB) { ); header('Location: '.api_get_course_url()); + exit; } @@ -111,13 +115,15 @@ function ($attemptA, $attemptB) { ->getXApiStateClient() ->createOrReplaceDocument( new StateDocument($state, $documentData) - ); + ) + ; } catch (Exception $exception) { Display::addFlash( Display::return_message($exception->getMessage(), 'error') ); header('Location: '.api_get_course_url()); + exit; } @@ -136,7 +142,7 @@ function ($attemptA, $attemptB) { ], null, '&', - PHP_QUERY_RFC3986 + \PHP_QUERY_RFC3986 ); header("Location: $activityLaunchUrl"); diff --git a/public/plugin/xapi/tincan/stats.php b/public/plugin/xapi/tincan/stats.php index b019db5d914..fd0b98d84ba 100644 --- a/public/plugin/xapi/tincan/stats.php +++ b/public/plugin/xapi/tincan/stats.php @@ -1,4 +1,6 @@ getMessage(), 'error'); + exit; } diff --git a/public/plugin/xapi/tincan/stats_statements.ajax.php b/public/plugin/xapi/tincan/stats_statements.ajax.php index 6d14bf046a9..d3edcc3152a 100644 --- a/public/plugin/xapi/tincan/stats_statements.ajax.php +++ b/public/plugin/xapi/tincan/stats_statements.ajax.php @@ -1,5 +1,7 @@ byRegistration($attempt); + ->byRegistration($attempt) +; try { $result = $xapiStatementClient->getStatements($filter); } catch (XApiException $xApiException) { echo Display::return_message($xApiException->getMessage(), 'error'); + exit; } catch (Exception $exception) { echo Display::return_message($exception->getMessage(), 'error'); + exit; } @@ -65,6 +72,7 @@ if (count($statements) <= 0) { echo Display::return_message(get_lang('NoResults'), 'warning'); + exit; } diff --git a/public/plugin/xapi/tincan/view.php b/public/plugin/xapi/tincan/view.php index 9c259d7f377..e571a020996 100644 --- a/public/plugin/xapi/tincan/view.php +++ b/public/plugin/xapi/tincan/view.php @@ -1,5 +1,7 @@ getLrsAuthUsername(), $toolLaunch->getLrsAuthPassword() ) - ->getDocument($state); + ->getDocument($state) + ; } catch (NotFoundException $notFoundException) { $stateDocument = null; } catch (Exception $exception) { @@ -71,6 +74,7 @@ ); header('Location: '.api_get_course_url()); + exit; } @@ -150,7 +154,7 @@ $pageContent = ''; if ($toolLaunch->getDescription()) { - $pageContent .= PHP_EOL; + $pageContent .= \PHP_EOL; $pageContent .= "

    {$toolLaunch->getDescription()}

    "; } diff --git a/public/plugin/xapi/tool_delete.php b/public/plugin/xapi/tool_delete.php index 910e4a76552..086bbaba78c 100644 --- a/public/plugin/xapi/tool_delete.php +++ b/public/plugin/xapi/tool_delete.php @@ -1,5 +1,7 @@ setTitle($values['title']) - ->setDescription(empty($values['description']) ? null : $values['description']); + ->setDescription(empty($values['description']) ? null : $values['description']) + ; if ($toolIsTinCan && isset($values['allow_multiple_attempts'])) { $toolLaunch->setAllowMultipleAttempts(true); @@ -98,7 +102,8 @@ $toolLaunch ->setLrsUrl($values['lrs_url']) ->setLrsAuthUsername($values['lrs_auth_username']) - ->setLrsAuthPassword($values['lrs_auth_password']); + ->setLrsAuthPassword($values['lrs_auth_password']) + ; } $em->persist($toolLaunch); @@ -109,6 +114,7 @@ ); header('Location: '.api_get_course_url()); + exit; } diff --git a/public/plugin/xapi/tool_import.php b/public/plugin/xapi/tool_import.php index 7c9fedc598e..67dd6497526 100644 --- a/public/plugin/xapi/tool_import.php +++ b/public/plugin/xapi/tool_import.php @@ -1,16 +1,19 @@ setLrsUrl($values['lrs_url']) ->setLrsAuthUsername($values['lrs_auth_username']) - ->setLrsAuthPassword($values['lrs_auth_password']); + ->setLrsAuthPassword($values['lrs_auth_password']) + ; } $em = Database::getManager(); @@ -139,6 +144,7 @@ ); header("Location: $pluginIndex"); + exit; } diff --git a/public/plugin/xapi/uninstall.php b/public/plugin/xapi/uninstall.php index 3bcf5724756..a13bd5e1cf9 100644 --- a/public/plugin/xapi/uninstall.php +++ b/public/plugin/xapi/uninstall.php @@ -1,5 +1,7 @@ uninstall(); From 4e2f0e60a1f62906753ecbdb88d2465ddd90bbba Mon Sep 17 00:00:00 2001 From: Angel Fernando Quiroz Campos <1697880+AngelFQC@users.noreply.github.com> Date: Wed, 3 Jul 2024 16:23:11 -0500 Subject: [PATCH 3/4] Plugin: XApi: Use core entities instead of plugin entities See #3943 --- public/plugin/xapi/admin.php | 12 +- public/plugin/xapi/cmi5/launch.php | 4 +- public/plugin/xapi/cmi5/view.php | 18 +- public/plugin/xapi/cron/send_statements.php | 5 +- public/plugin/xapi/install.php | 7 - .../src/Controller/StatementGetController.php | 18 +- .../xapi/src/Entity/ActivityProfile.php | 99 ----- .../plugin/xapi/src/Entity/ActivityState.php | 118 ------ public/plugin/xapi/src/Entity/Cmi5Item.php | 387 ------------------ public/plugin/xapi/src/Entity/InternalLog.php | 244 ----------- public/plugin/xapi/src/Entity/LrsAuth.php | 112 ----- .../Repository/ToolLaunchRepository.php | 88 ---- .../xapi/src/Entity/SharedStatement.php | 112 ----- public/plugin/xapi/src/Entity/ToolLaunch.php | 295 ------------- .../src/Hook/XApiActivityHookObserver.php | 8 +- .../src/Lrs/ActivitiesProfileController.php | 12 +- .../src/Lrs/ActivitiesStateController.php | 6 +- public/plugin/xapi/src/Lrs/LrsRequest.php | 4 +- .../xapi/src/Lrs/Utils/InternalLogUtil.php | 4 +- public/plugin/xapi/src/Parser/Cmi5Parser.php | 16 +- .../plugin/xapi/src/Parser/PackageParser.php | 4 +- .../plugin/xapi/src/Parser/TinCanParser.php | 6 +- public/plugin/xapi/src/XApiPlugin.php | 114 +----- public/plugin/xapi/start.php | 7 +- public/plugin/xapi/tincan/launch.php | 4 +- public/plugin/xapi/tincan/stats.php | 4 +- .../xapi/tincan/stats_attempts.ajax.php | 4 +- .../xapi/tincan/stats_statements.ajax.php | 4 +- public/plugin/xapi/tincan/view.php | 4 +- public/plugin/xapi/tool_delete.php | 4 +- public/plugin/xapi/tool_edit.php | 4 +- public/plugin/xapi/uninstall.php | 7 - src/CoreBundle/Entity/XApiSharedStatement.php | 7 + src/CoreBundle/Entity/XApiToolLaunch.php | 2 +- 34 files changed, 82 insertions(+), 1662 deletions(-) delete mode 100644 public/plugin/xapi/install.php delete mode 100644 public/plugin/xapi/src/Entity/ActivityProfile.php delete mode 100644 public/plugin/xapi/src/Entity/ActivityState.php delete mode 100644 public/plugin/xapi/src/Entity/Cmi5Item.php delete mode 100644 public/plugin/xapi/src/Entity/InternalLog.php delete mode 100644 public/plugin/xapi/src/Entity/LrsAuth.php delete mode 100644 public/plugin/xapi/src/Entity/Repository/ToolLaunchRepository.php delete mode 100644 public/plugin/xapi/src/Entity/SharedStatement.php delete mode 100644 public/plugin/xapi/src/Entity/ToolLaunch.php delete mode 100644 public/plugin/xapi/uninstall.php diff --git a/public/plugin/xapi/admin.php b/public/plugin/xapi/admin.php index 9bacdc9e5be..f83f55b4fd9 100644 --- a/public/plugin/xapi/admin.php +++ b/public/plugin/xapi/admin.php @@ -4,7 +4,7 @@ /* For licensing terms, see /license.txt */ -use Chamilo\PluginBundle\Entity\XApi\LrsAuth; +use Chamilo\CoreBundle\Entity\XApiLrsAuth; use Symfony\Component\HttpFoundation\Request; $cidReset = true; @@ -26,7 +26,7 @@ * * @throws Exception */ -function createForm(?LrsAuth $auth = null) +function createForm(?XApiLrsAuth $auth = null) { $pageBaseUrl = api_get_self(); @@ -63,7 +63,7 @@ function createForm(?LrsAuth $auth = null) if ($form->validate()) { $values = $form->exportValues(); - $auth = new LrsAuth(); + $auth = new XApiLrsAuth(); $auth ->setUsername($values['username']) ->setPassword($values['password']) @@ -94,7 +94,7 @@ function createForm(?LrsAuth $auth = null) break; case 'edit': - $auth = $em->find(LrsAuth::class, $request->query->getInt('id')); + $auth = $em->find(XApiLrsAuth::class, $request->query->getInt('id')); if (null == $auth) { api_not_allowed(true); @@ -135,7 +135,7 @@ function createForm(?LrsAuth $auth = null) break; case 'delete': - $auth = $em->find(LrsAuth::class, $request->query->getInt('id')); + $auth = $em->find(XApiLrsAuth::class, $request->query->getInt('id')); if (null == $auth) { api_not_allowed(true); @@ -160,7 +160,7 @@ function createForm(?LrsAuth $auth = null) ); $pageContent = Display::return_message(get_lang('NoData'), 'warning'); - $auths = $em->getRepository(LrsAuth::class)->findAll(); + $auths = $em->getRepository(XApiLrsAuth::class)->findAll(); if (count($auths) > 0) { $row = 0; diff --git a/public/plugin/xapi/cmi5/launch.php b/public/plugin/xapi/cmi5/launch.php index 466e83ebb97..0291a15be9e 100644 --- a/public/plugin/xapi/cmi5/launch.php +++ b/public/plugin/xapi/cmi5/launch.php @@ -4,7 +4,7 @@ /* For licensing terms, see /license.txt */ -use Chamilo\PluginBundle\Entity\XApi\Cmi5Item; +use Chamilo\CoreBundle\Entity\XApiCmi5Item; use Symfony\Component\HttpFoundation\Request as HttpRequest; use Xabbuh\XApi\Model\Account; use Xabbuh\XApi\Model\Activity; @@ -32,7 +32,7 @@ $em = Database::getManager(); -$item = $em->find(Cmi5Item::class, $request->query->getInt('id')); +$item = $em->find(XApiCmi5Item::class, $request->query->getInt('id')); $toolLaunch = $item->getTool(); if ($toolLaunch->getId() !== $request->query->getInt('tool')) { diff --git a/public/plugin/xapi/cmi5/view.php b/public/plugin/xapi/cmi5/view.php index 955edc4fb3c..61feb914e3d 100644 --- a/public/plugin/xapi/cmi5/view.php +++ b/public/plugin/xapi/cmi5/view.php @@ -4,8 +4,8 @@ /* For licensing terms, see /license.txt */ -use Chamilo\PluginBundle\Entity\XApi\Cmi5Item; -use Chamilo\PluginBundle\Entity\XApi\ToolLaunch; +use Chamilo\CoreBundle\Entity\XApiCmi5Item; +use Chamilo\CoreBundle\Entity\XApiToolLaunch; use Symfony\Component\HttpFoundation\Request as HttpRequest; use Xabbuh\XApi\Model\LanguageMap; @@ -19,7 +19,7 @@ $em = Database::getManager(); $toolLaunch = $em->find( - ToolLaunch::class, + XApiToolLaunch::class, $request->query->getInt('id') ); @@ -38,18 +38,16 @@ $user = api_get_user_entity(api_get_user_id()); $interfaceLanguage = api_get_interface_language(); -$itemsRepo = $em->getRepository(Cmi5Item::class); +$itemsRepo = $em->getRepository(XApiCmi5Item::class); -$query = $em->createQueryBuilder() - ->select('item') - ->from(Cmi5Item::class, 'item') - ->where('item.tool = :tool') +$query = $itemsRepo->createQueryBuilder('item'); +$query + ->where($query->expr()->eq('item.tool', ':tool')) ->setParameter('tool', $toolLaunch->getId()) - ->getQuery() ; $tocHtml = $itemsRepo->buildTree( - $query->getArrayResult(), + $query->getQuery()->getArrayResult(), [ 'decorate' => true, 'rootOpen' => '
      ', diff --git a/public/plugin/xapi/cron/send_statements.php b/public/plugin/xapi/cron/send_statements.php index f19e3be941a..24feaae4b23 100644 --- a/public/plugin/xapi/cron/send_statements.php +++ b/public/plugin/xapi/cron/send_statements.php @@ -4,7 +4,7 @@ /* For licensing terms, see /license.txt */ -use Chamilo\PluginBundle\Entity\XApi\SharedStatement; +use Chamilo\CoreBundle\Entity\XApiSharedStatement; use Xabbuh\XApi\Common\Exception\ConflictException; use Xabbuh\XApi\Common\Exception\XApiException; use Xabbuh\XApi\Model\StatementId; @@ -25,7 +25,7 @@ $statementSerializer = new StatementSerializer($serializer); $notSentSharedStatements = $em - ->getRepository(SharedStatement::class) + ->getRepository(XApiSharedStatement::class) ->findBy( ['uuid' => null, 'sent' => false], null, @@ -39,7 +39,6 @@ $client = XApiPlugin::create()->getXapiStatementCronClient(); - /** @var SharedStatement $notSentSharedStatement */ foreach ($notSentSharedStatements as $notSentSharedStatement) { $notSentStatement = $statementSerializer->deserializeStatement( json_encode($notSentSharedStatement->getStatement()) diff --git a/public/plugin/xapi/install.php b/public/plugin/xapi/install.php deleted file mode 100644 index 96ac5bb5d57..00000000000 --- a/public/plugin/xapi/install.php +++ /dev/null @@ -1,7 +0,0 @@ -install(); diff --git a/public/plugin/xapi/php-xapi/lrs-bundle/src/Controller/StatementGetController.php b/public/plugin/xapi/php-xapi/lrs-bundle/src/Controller/StatementGetController.php index f19447b262e..8a98f3f8298 100644 --- a/public/plugin/xapi/php-xapi/lrs-bundle/src/Controller/StatementGetController.php +++ b/public/plugin/xapi/php-xapi/lrs-bundle/src/Controller/StatementGetController.php @@ -57,18 +57,12 @@ class StatementGetController 'cursor' => true, ]; - protected $repository; - protected $statementSerializer; - protected $statementResultSerializer; - protected $statementsFilterFactory; - - public function __construct(StatementRepositoryInterface $repository, StatementSerializerInterface $statementSerializer, StatementResultSerializerInterface $statementResultSerializer, StatementsFilterFactory $statementsFilterFactory) - { - $this->repository = $repository; - $this->statementSerializer = $statementSerializer; - $this->statementResultSerializer = $statementResultSerializer; - $this->statementsFilterFactory = $statementsFilterFactory; - } + public function __construct( + protected readonly StatementRepositoryInterface $repository, + protected readonly StatementSerializerInterface $statementSerializer, + protected readonly StatementResultSerializerInterface $statementResultSerializer, + protected readonly StatementsFilterFactory $statementsFilterFactory + ) {} /** * @return Response diff --git a/public/plugin/xapi/src/Entity/ActivityProfile.php b/public/plugin/xapi/src/Entity/ActivityProfile.php deleted file mode 100644 index b4801dc1330..00000000000 --- a/public/plugin/xapi/src/Entity/ActivityProfile.php +++ /dev/null @@ -1,99 +0,0 @@ -id; - } - - public function setId(int $id): self - { - $this->id = $id; - - return $this; - } - - public function getProfileId(): string - { - return $this->profileId; - } - - public function setProfileId(string $profileId): self - { - $this->profileId = $profileId; - - return $this; - } - - public function getActivityId(): string - { - return $this->activityId; - } - - public function setActivityId(string $activityId): self - { - $this->activityId = $activityId; - - return $this; - } - - public function getDocumentData(): array - { - return $this->documentData; - } - - public function setDocumentData(array $documentData): self - { - $this->documentData = $documentData; - - return $this; - } -} diff --git a/public/plugin/xapi/src/Entity/ActivityState.php b/public/plugin/xapi/src/Entity/ActivityState.php deleted file mode 100644 index f466ab6c70b..00000000000 --- a/public/plugin/xapi/src/Entity/ActivityState.php +++ /dev/null @@ -1,118 +0,0 @@ -id; - } - - public function setId(int $id): self - { - $this->id = $id; - - return $this; - } - - public function getStateId(): string - { - return $this->stateId; - } - - public function setStateId(string $stateId): self - { - $this->stateId = $stateId; - - return $this; - } - - public function getActivityId(): string - { - return $this->activityId; - } - - public function setActivityId(string $activityId): self - { - $this->activityId = $activityId; - - return $this; - } - - public function getAgent(): array - { - return $this->agent; - } - - public function setAgent(array $agent): self - { - $this->agent = $agent; - - return $this; - } - - public function getDocumentData(): array - { - return $this->documentData; - } - - public function setDocumentData(array $documentData): self - { - $this->documentData = $documentData; - - return $this; - } -} diff --git a/public/plugin/xapi/src/Entity/Cmi5Item.php b/public/plugin/xapi/src/Entity/Cmi5Item.php deleted file mode 100644 index 4401b39ce57..00000000000 --- a/public/plugin/xapi/src/Entity/Cmi5Item.php +++ /dev/null @@ -1,387 +0,0 @@ -children = new ArrayCollection(); - } - - public function getId(): int - { - return $this->id; - } - - public function setId(int $id): self - { - $this->id = $id; - - return $this; - } - - public function getIdentifier(): string - { - return $this->identifier; - } - - public function setIdentifier(string $identifier): self - { - $this->identifier = $identifier; - - return $this; - } - - public function getType(): string - { - return $this->type; - } - - public function setType(string $type): self - { - $this->type = $type; - - return $this; - } - - public function getTitle(): array - { - return $this->title; - } - - public function setTitle(array $title): self - { - $this->title = $title; - - return $this; - } - - public function getDescription(): array - { - return $this->description; - } - - public function setDescription(array $description): self - { - $this->description = $description; - - return $this; - } - - public function getUrl(): ?string - { - return $this->url; - } - - public function setUrl(?string $url): self - { - $this->url = $url; - - return $this; - } - - public function getActivityType(): ?string - { - return $this->activityType; - } - - public function setActivityType(?string $activityType): self - { - $this->activityType = $activityType; - - return $this; - } - - public function getLaunchMethod(): ?string - { - return $this->launchMethod; - } - - public function setLaunchMethod(?string $launchMethod): self - { - $this->launchMethod = $launchMethod; - - return $this; - } - - public function getMoveOn(): ?string - { - return $this->moveOn; - } - - public function setMoveOn(?string $moveOn): self - { - $this->moveOn = $moveOn; - - return $this; - } - - public function getMasteryScore(): ?float - { - return $this->masteryScore; - } - - public function setMasteryScore(?float $masteryScore): self - { - $this->masteryScore = $masteryScore; - - return $this; - } - - public function getLaunchParameters(): ?string - { - return $this->launchParameters; - } - - public function setLaunchParameters(?string $launchParameters): self - { - $this->launchParameters = $launchParameters; - - return $this; - } - - public function getEntitlementKey(): ?string - { - return $this->entitlementKey; - } - - public function setEntitlementKey(?string $entitlementKey): self - { - $this->entitlementKey = $entitlementKey; - - return $this; - } - - public function getParent(): ?self - { - return $this->parent; - } - - public function setParent(?self $parent): self - { - $this->parent = $parent; - - return $this; - } - - public function getChildren(): ArrayCollection - { - return $this->children; - } - - public function setChildren(ArrayCollection $children): self - { - $this->children = $children; - - return $this; - } - - public function getStatus(): ?string - { - return $this->status; - } - - public function setStatus(?string $status): self - { - $this->status = $status; - - return $this; - } - - public function getTool(): ToolLaunch - { - return $this->tool; - } - - public function setTool(ToolLaunch $tool): self - { - $this->tool = $tool; - - return $this; - } - - public function getRoot(): self - { - return $this->root; - } -} diff --git a/public/plugin/xapi/src/Entity/InternalLog.php b/public/plugin/xapi/src/Entity/InternalLog.php deleted file mode 100644 index 8e43a3a3020..00000000000 --- a/public/plugin/xapi/src/Entity/InternalLog.php +++ /dev/null @@ -1,244 +0,0 @@ -id; - } - - public function getUser(): User - { - return $this->user; - } - - public function setUser(User $user): self - { - $this->user = $user; - - return $this; - } - - public function getStatementId(): string - { - return $this->statementId; - } - - public function setStatementId(string $statementId): self - { - $this->statementId = $statementId; - - return $this; - } - - public function getVerb(): string - { - return $this->verb; - } - - public function setVerb(string $verb): self - { - $this->verb = $verb; - - return $this; - } - - public function getObjectId(): string - { - return $this->objectId; - } - - public function setObjectId(string $objectId): self - { - $this->objectId = $objectId; - - return $this; - } - - public function getActivityName(): ?string - { - return $this->activityName; - } - - public function setActivityName(?string $activityName): self - { - $this->activityName = $activityName; - - return $this; - } - - public function getActivityDescription(): ?string - { - return $this->activityDescription; - } - - public function setActivityDescription(?string $activityDescription): self - { - $this->activityDescription = $activityDescription; - - return $this; - } - - public function getScoreScaled(): ?float - { - return $this->scoreScaled; - } - - public function setScoreScaled(?float $scoreScaled): self - { - $this->scoreScaled = $scoreScaled; - - return $this; - } - - public function getScoreRaw(): ?float - { - return $this->scoreRaw; - } - - public function setScoreRaw(?float $scoreRaw): self - { - $this->scoreRaw = $scoreRaw; - - return $this; - } - - public function getScoreMin(): ?float - { - return $this->scoreMin; - } - - public function setScoreMin(?float $scoreMin): self - { - $this->scoreMin = $scoreMin; - - return $this; - } - - public function getScoreMax(): ?float - { - return $this->scoreMax; - } - - public function setScoreMax(?float $scoreMax): self - { - $this->scoreMax = $scoreMax; - - return $this; - } - - public function getCreatedAt(): ?DateTime - { - return $this->createdAt; - } - - public function setCreatedAt(?DateTime $createdAt): self - { - $this->createdAt = $createdAt; - - return $this; - } -} diff --git a/public/plugin/xapi/src/Entity/LrsAuth.php b/public/plugin/xapi/src/Entity/LrsAuth.php deleted file mode 100644 index 03b15d416eb..00000000000 --- a/public/plugin/xapi/src/Entity/LrsAuth.php +++ /dev/null @@ -1,112 +0,0 @@ -id; - } - - public function getUsername(): string - { - return $this->username; - } - - public function setUsername(string $username): self - { - $this->username = $username; - - return $this; - } - - public function getPassword(): string - { - return $this->password; - } - - public function setPassword(string $password): self - { - $this->password = $password; - - return $this; - } - - public function isEnabled(): bool - { - return $this->enabled; - } - - public function setEnabled(bool $enabled): self - { - $this->enabled = $enabled; - - return $this; - } - - public function getCreatedAt(): DateTime - { - return $this->createdAt; - } - - public function setCreatedAt(DateTime $createdAt): self - { - $this->createdAt = $createdAt; - - return $this; - } -} diff --git a/public/plugin/xapi/src/Entity/Repository/ToolLaunchRepository.php b/public/plugin/xapi/src/Entity/Repository/ToolLaunchRepository.php deleted file mode 100644 index e9431f3e23a..00000000000 --- a/public/plugin/xapi/src/Entity/Repository/ToolLaunchRepository.php +++ /dev/null @@ -1,88 +0,0 @@ - $course, - 'session' => null, - ]; - - if ($session) { - $criteria['session'] = $session; - } - - return $this->findBy($criteria, $orderBy, $limit, $start); - } - - public function countByCourseAndSession(Course $course, ?Session $session = null, $filteredForStudent = false): int - { - $qb = $this->createQueryBuilder('tl'); - $qb->select($qb->expr()->count('tl')) - ->where($qb->expr()->eq('tl.course', ':course')) - ->setParameter('course', $course) - ; - - if ($session) { - $qb->andWhere($qb->expr()->eq('tl.session', ':session')) - ->setParameter('session', $session) - ; - } else { - $qb->andWhere($qb->expr()->isNull('tl.session')); - } - - if ($filteredForStudent) { - $qb - ->leftJoin( - CLpItem::class, - 'lpi', - Join::WITH, - "tl.id = lpi.path AND tl.course = lpi.cId AND lpi.itemType = 'xapi'" - ) - ->andWhere($qb->expr()->isNull('lpi.path')) - ; - } - - $query = $qb->getQuery(); - - return (int) $query->getSingleScalarResult(); - } - - public function wasAddedInLp(ToolLaunch $toolLaunch): int - { - $qb = $this->getEntityManager()->createQueryBuilder(); - - return (int) $qb->select($qb->expr()->count('lp')) - ->from(CLp::class, 'lp') - ->innerJoin(CLpItem::class, 'lpi', Join::WITH, 'lp.id = lpi.lpId') - ->where('lpi.itemType = :type') - ->andWhere('lpi.path = :tool_id') - ->setParameter('type', TOOL_XAPI) - ->setParameter('tool_id', $toolLaunch->getId()) - ->getQuery() - ->getSingleScalarResult(); - } -} diff --git a/public/plugin/xapi/src/Entity/SharedStatement.php b/public/plugin/xapi/src/Entity/SharedStatement.php deleted file mode 100644 index 9475e3a2c13..00000000000 --- a/public/plugin/xapi/src/Entity/SharedStatement.php +++ /dev/null @@ -1,112 +0,0 @@ -statement = $statement; - $this->uuid = $uuid; - $this->sent = $sent; - } - - public function getId(): int - { - return $this->id; - } - - public function getUuid(): ?string - { - return $this->uuid; - } - - public function setUuid(?string $uuid): self - { - $this->uuid = $uuid; - - return $this; - } - - public function getStatement(): array - { - return $this->statement; - } - - public function setStatement(array $statement): self - { - $this->statement = $statement; - - return $this; - } - - public function isSent(): bool - { - return $this->sent; - } - - public function setSent(bool $sent): self - { - $this->sent = $sent; - - return $this; - } -} diff --git a/public/plugin/xapi/src/Entity/ToolLaunch.php b/public/plugin/xapi/src/Entity/ToolLaunch.php deleted file mode 100644 index 6d9d4f5b8ea..00000000000 --- a/public/plugin/xapi/src/Entity/ToolLaunch.php +++ /dev/null @@ -1,295 +0,0 @@ -allowMultipleAttempts = true; - $this->items = new ArrayCollection(); - } - - public function getId(): int - { - return $this->id; - } - - public function setId(int $id): ToolLaunch - { - $this->id = $id; - - return $this; - } - - public function getTitle(): string - { - return $this->title; - } - - public function setTitle(string $title): ToolLaunch - { - $this->title = $title; - - return $this; - } - - public function getDescription(): ?string - { - return $this->description; - } - - public function setDescription(?string $description): ToolLaunch - { - $this->description = $description; - - return $this; - } - - public function getCourse(): Course - { - return $this->course; - } - - public function setCourse(Course $course): ToolLaunch - { - $this->course = $course; - - return $this; - } - - public function getSession(): ?Session - { - return $this->session; - } - - public function setSession(?Session $session): ToolLaunch - { - $this->session = $session; - - return $this; - } - - public function getLaunchUrl(): string - { - return $this->launchUrl; - } - - public function setLaunchUrl(string $launchUrl): ToolLaunch - { - $this->launchUrl = $launchUrl; - - return $this; - } - - public function getActivityId(): ?string - { - return $this->activityId; - } - - public function setActivityId(?string $activityId): ToolLaunch - { - $this->activityId = $activityId; - - return $this; - } - - public function getCreatedAt(): DateTime - { - return $this->createdAt; - } - - public function setCreatedAt(DateTime $createdAt): ToolLaunch - { - $this->createdAt = $createdAt; - - return $this; - } - - public function getActivityType(): ?string - { - return $this->activityType; - } - - public function setActivityType(?string $activityType): ToolLaunch - { - $this->activityType = $activityType; - - return $this; - } - - public function isAllowMultipleAttempts(): bool - { - return $this->allowMultipleAttempts; - } - - public function setAllowMultipleAttempts(bool $allowMultipleAttempts): ToolLaunch - { - $this->allowMultipleAttempts = $allowMultipleAttempts; - - return $this; - } - - public function getLrsUrl(): ?string - { - return $this->lrsUrl; - } - - public function setLrsUrl(?string $lrsUrl): ToolLaunch - { - $this->lrsUrl = $lrsUrl; - - return $this; - } - - public function getLrsAuthUsername(): ?string - { - return $this->lrsAuthUsername; - } - - public function setLrsAuthUsername(?string $lrsAuthUsername): ToolLaunch - { - $this->lrsAuthUsername = $lrsAuthUsername; - - return $this; - } - - public function getLrsAuthPassword(): ?string - { - return $this->lrsAuthPassword; - } - - public function setLrsAuthPassword(?string $lrsAuthPassword): ToolLaunch - { - $this->lrsAuthPassword = $lrsAuthPassword; - - return $this; - } - - public function getItems(): ArrayCollection - { - return $this->items; - } - - /** - * @param \Chamilo\PluginBundle\Entity\XApi\Cmi5Item $cmi5Item - * - * @return $this - */ - public function addItem(Cmi5Item $cmi5Item) - { - $cmi5Item->setTool($this); - - $this->items->add($cmi5Item); - - return $this; - } -} diff --git a/public/plugin/xapi/src/Hook/XApiActivityHookObserver.php b/public/plugin/xapi/src/Hook/XApiActivityHookObserver.php index c622e5eeecc..15f2ba24b03 100644 --- a/public/plugin/xapi/src/Hook/XApiActivityHookObserver.php +++ b/public/plugin/xapi/src/Hook/XApiActivityHookObserver.php @@ -2,7 +2,7 @@ /* For licensing terms, see /license.txt */ -use Chamilo\PluginBundle\Entity\XApi\SharedStatement; +use Chamilo\CoreBundle\Entity\XApiSharedStatement; use Xabbuh\XApi\Model\Statement; use Xabbuh\XApi\Serializer\Symfony\Serializer; use Xabbuh\XApi\Serializer\Symfony\StatementSerializer; @@ -33,14 +33,12 @@ protected function __construct() /** * @throws \Doctrine\ORM\ORMException * @throws \Doctrine\ORM\OptimisticLockException - * - * @return \Chamilo\PluginBundle\Entity\XApi\SharedStatement|null */ - protected function saveSharedStatement(Statement $statement) + protected function saveSharedStatement(Statement $statement): XApiSharedStatement { $statementSerialized = $this->serializeStatement($statement); - $sharedStmt = new SharedStatement( + $sharedStmt = new XApiSharedStatement( json_decode($statementSerialized, true) ); diff --git a/public/plugin/xapi/src/Lrs/ActivitiesProfileController.php b/public/plugin/xapi/src/Lrs/ActivitiesProfileController.php index 6ea1ed6e840..391750b15fb 100644 --- a/public/plugin/xapi/src/Lrs/ActivitiesProfileController.php +++ b/public/plugin/xapi/src/Lrs/ActivitiesProfileController.php @@ -6,7 +6,7 @@ namespace Chamilo\PluginBundle\XApi\Lrs; -use Chamilo\PluginBundle\Entity\XApi\ActivityProfile; +use Chamilo\CoreBundle\Entity\XApiActivityProfile; use Database; use Symfony\Component\HttpFoundation\Response; @@ -21,9 +21,9 @@ public function get(): Response $activityId = $this->httpRequest->query->get('activityId'); $em = Database::getManager(); - $profileRepo = $em->getRepository(ActivityProfile::class); + $profileRepo = $em->getRepository(XApiActivityProfile::class); - /** @var ActivityProfile $activityProfile */ + /** @var XApiActivityProfile $activityProfile */ $activityProfile = $profileRepo->findOneBy( [ 'profileId' => $profileId, @@ -52,9 +52,9 @@ public function put(): Response $documentData = $this->httpRequest->getContent(); $em = Database::getManager(); - $profileRepo = $em->getRepository(ActivityProfile::class); + $profileRepo = $em->getRepository(XApiActivityProfile::class); - /** @var ActivityProfile $activityProfile */ + /** @var XApiActivityProfile $activityProfile */ $activityProfile = $profileRepo->findOneBy( [ 'profileId' => $profileId, @@ -63,7 +63,7 @@ public function put(): Response ); if (empty($activityProfile)) { - $activityProfile = new ActivityProfile(); + $activityProfile = new XApiActivityProfile(); $activityProfile ->setProfileId($profileId) ->setActivityId($activityId) diff --git a/public/plugin/xapi/src/Lrs/ActivitiesStateController.php b/public/plugin/xapi/src/Lrs/ActivitiesStateController.php index 56c3398e390..b980fb1a71e 100644 --- a/public/plugin/xapi/src/Lrs/ActivitiesStateController.php +++ b/public/plugin/xapi/src/Lrs/ActivitiesStateController.php @@ -5,7 +5,7 @@ namespace Chamilo\PluginBundle\XApi\Lrs; -use Chamilo\PluginBundle\Entity\XApi\ActivityState; +use Chamilo\CoreBundle\Entity\XApiActivityState; use Database; use Symfony\Component\HttpFoundation\JsonResponse; use Symfony\Component\HttpFoundation\Response; @@ -101,14 +101,14 @@ public function put(): Response $em = Database::getManager(); if (empty($state)) { - $state = new ActivityState(); + $state = new XApiActivityState(); $state ->setActivityId($activityId) ->setAgent(json_decode($agent, true)) ->setStateId($stateId) ; } else { - $state = $em->find(ActivityState::class, $state['id']); + $state = $em->find(XApiActivityState::class, $state['id']); } $state->setDocumentData(json_decode($documentData, true)); diff --git a/public/plugin/xapi/src/Lrs/LrsRequest.php b/public/plugin/xapi/src/Lrs/LrsRequest.php index 8b550e66c78..b4f4389def6 100644 --- a/public/plugin/xapi/src/Lrs/LrsRequest.php +++ b/public/plugin/xapi/src/Lrs/LrsRequest.php @@ -6,7 +6,7 @@ namespace Chamilo\PluginBundle\XApi\Lrs; -use Chamilo\PluginBundle\Entity\XApi\LrsAuth; +use Chamilo\CoreBundle\Entity\XApiLrsAuth; use Database; use Exception; use Symfony\Component\HttpFoundation\Request as HttpRequest; @@ -85,7 +85,7 @@ private function validateAuth(): bool list($username, $password) = $parts; $auth = Database::getManager() - ->getRepository(LrsAuth::class) + ->getRepository(XApiLrsAuth::class) ->findOneBy( ['username' => $username, 'password' => $password, 'enabled' => true] ) diff --git a/public/plugin/xapi/src/Lrs/Utils/InternalLogUtil.php b/public/plugin/xapi/src/Lrs/Utils/InternalLogUtil.php index 6f5518f51e5..24b4fa364b0 100644 --- a/public/plugin/xapi/src/Lrs/Utils/InternalLogUtil.php +++ b/public/plugin/xapi/src/Lrs/Utils/InternalLogUtil.php @@ -6,7 +6,7 @@ namespace Chamilo\PluginBundle\XApi\Lrs\Util; -use Chamilo\PluginBundle\Entity\XApi\InternalLog; +use Chamilo\CoreBundle\Entity\XApiInternalLog; use Database; use UserManager; use Xabbuh\XApi\Model\Activity; @@ -33,7 +33,7 @@ public static function saveStatementForInternalLog(Statement $statement): void $languageIso = api_get_language_isocode(); $statementVerbString = XApiPlugin::extractVerbInLanguage($statement->getVerb()->getDisplay(), $languageIso); - $internalLog = new InternalLog(); + $internalLog = new XApiInternalLog(); $internalLog ->setUser($user) ->setVerb($statementVerbString) diff --git a/public/plugin/xapi/src/Parser/Cmi5Parser.php b/public/plugin/xapi/src/Parser/Cmi5Parser.php index a869edc378c..e939ff5bb15 100644 --- a/public/plugin/xapi/src/Parser/Cmi5Parser.php +++ b/public/plugin/xapi/src/Parser/Cmi5Parser.php @@ -6,8 +6,8 @@ namespace Chamilo\PluginBundle\XApi\Parser; -use Chamilo\PluginBundle\Entity\XApi\Cmi5Item; -use Chamilo\PluginBundle\Entity\XApi\ToolLaunch; +use Chamilo\CoreBundle\Entity\XApiCmi5Item; +use Chamilo\CoreBundle\Entity\XApiToolLaunch; use Symfony\Component\DomCrawler\Crawler; /** @@ -15,14 +15,14 @@ */ class Cmi5Parser extends PackageParser { - public function parse(): ToolLaunch + public function parse(): XApiToolLaunch { $content = file_get_contents($this->filePath); $xml = new Crawler($content); $courseNode = $xml->filterXPath('//courseStructure/course'); - $toolLaunch = new ToolLaunch(); + $toolLaunch = new XApiToolLaunch(); $toolLaunch ->setTitle( current( @@ -74,13 +74,13 @@ private function getLanguageStrings(Crawler $node) } /** - * @return array|\Chamilo\PluginBundle\Entity\XApi\Cmi5Item[] + * @return array */ - private function generateToC(Crawler $xml) + private function generateToC(Crawler $xml): array { $blocksMap = []; - /** @var array|Cmi5Item[] $items */ + /** @var array|XApiCmi5Item[] $items */ $items = $xml ->filterXPath('//*') ->reduce( @@ -94,7 +94,7 @@ function (Crawler $node, $i) use (&$blocksMap) { list($id, $activityType, $launchMethod, $moveOn, $masteryMode) = $node->extract($attributes)[0]; - $item = new Cmi5Item(); + $item = new XApiCmi5Item(); $item ->setIdentifier($id) ->setType($node->nodeName()) diff --git a/public/plugin/xapi/src/Parser/PackageParser.php b/public/plugin/xapi/src/Parser/PackageParser.php index bca1575acad..15a0f0d5457 100644 --- a/public/plugin/xapi/src/Parser/PackageParser.php +++ b/public/plugin/xapi/src/Parser/PackageParser.php @@ -8,7 +8,7 @@ use Chamilo\CoreBundle\Entity\Course; use Chamilo\CoreBundle\Entity\Session; -use Chamilo\PluginBundle\Entity\XApi\ToolLaunch; +use Chamilo\CoreBundle\Entity\XApiToolLaunch; use Exception; /** @@ -60,5 +60,5 @@ public static function create(string $packageType, string $filePath, Course $cou } } - abstract public function parse(): ToolLaunch; + abstract public function parse(): XApiToolLaunch; } diff --git a/public/plugin/xapi/src/Parser/TinCanParser.php b/public/plugin/xapi/src/Parser/TinCanParser.php index af5b568cd4d..aa3e876a1be 100644 --- a/public/plugin/xapi/src/Parser/TinCanParser.php +++ b/public/plugin/xapi/src/Parser/TinCanParser.php @@ -6,7 +6,7 @@ namespace Chamilo\PluginBundle\XApi\Parser; -use Chamilo\PluginBundle\Entity\XApi\ToolLaunch; +use Chamilo\CoreBundle\Entity\XApiToolLaunch; use Symfony\Component\DomCrawler\Crawler; /** @@ -14,7 +14,7 @@ */ class TinCanParser extends PackageParser { - public function parse(): ToolLaunch + public function parse(): XApiToolLaunch { $content = file_get_contents($this->filePath); @@ -25,7 +25,7 @@ public function parse(): ToolLaunch $nodeDescription = $activityNode->filter('description'); $nodeLaunch = $activityNode->filter('launch'); - $toolLaunch = new ToolLaunch(); + $toolLaunch = new XApiToolLaunch(); $toolLaunch ->setCourse($this->course) ->setSession($this->session) diff --git a/public/plugin/xapi/src/XApiPlugin.php b/public/plugin/xapi/src/XApiPlugin.php index b58094aaf47..9d034c57904 100644 --- a/public/plugin/xapi/src/XApiPlugin.php +++ b/public/plugin/xapi/src/XApiPlugin.php @@ -2,21 +2,14 @@ /* For licensing terms, see /license.txt */ -use Chamilo\PluginBundle\Entity\XApi\ActivityProfile; -use Chamilo\PluginBundle\Entity\XApi\ActivityState; -use Chamilo\PluginBundle\Entity\XApi\Cmi5Item; -use Chamilo\PluginBundle\Entity\XApi\InternalLog; -use Chamilo\PluginBundle\Entity\XApi\LrsAuth; -use Chamilo\PluginBundle\Entity\XApi\SharedStatement; -use Chamilo\PluginBundle\Entity\XApi\ToolLaunch; +use Chamilo\CoreBundle\Entity\XApiToolLaunch; use Doctrine\ORM\EntityManager; use Doctrine\ORM\Mapping\Driver\SimplifiedXmlDriver; use Doctrine\ORM\ORMException; -use Doctrine\ORM\Tools\SchemaTool; use GuzzleHttp\RequestOptions; use Http\Adapter\Guzzle6\Client; use Http\Message\MessageFactory\GuzzleMessageFactory; -use Ramsey\Uuid\Uuid; +use Symfony\Component\Uid\Uuid; use Xabbuh\XApi\Client\Api\StatementsApiClientInterface; use Xabbuh\XApi\Client\XApiClientBuilder; use Xabbuh\XApi\Model\Agent; @@ -93,34 +86,6 @@ public static function create() */ public function install() { - $em = Database::getManager(); - - $tablesExists = $em->getConnection()->getSchemaManager()->tablesExist( - [ - 'xapi_shared_statement', - 'xapi_tool_launch', - 'xapi_lrs_auth', - 'xapi_cmi5_item', - 'xapi_activity_state', - 'xapi_activity_profile', - 'xapi_internal_log', - - 'xapi_attachment', - 'xapi_object', - 'xapi_result', - 'xapi_verb', - 'xapi_extensions', - 'xapi_context', - 'xapi_actor', - 'xapi_statement', - ] - ); - - if ($tablesExists) { - return; - } - - $this->installPluginDbTables(); $this->installInitialConfig(); $this->addCourseTools(); $this->installHook(); @@ -132,7 +97,6 @@ public function install() public function uninstall() { $this->uninstallHook(); - $this->uninstallPluginDbTables(); $this->deleteCourseTools(); } @@ -172,39 +136,6 @@ public function uninstallHook() return 1; } - public function uninstallPluginDbTables() - { - $em = Database::getManager(); - $pluginEm = self::getEntityManager(); - - $schemaTool = new SchemaTool($em); - $schemaTool->dropSchema( - [ - $em->getClassMetadata(ActivityProfile::class), - $em->getClassMetadata(ActivityState::class), - $em->getClassMetadata(SharedStatement::class), - $em->getClassMetadata(ToolLaunch::class), - $em->getClassMetadata(LrsAuth::class), - $em->getClassMetadata(Cmi5Item::class), - $em->getClassMetadata(InternalLog::class), - ] - ); - - $pluginSchemaTool = new SchemaTool($pluginEm); - $pluginSchemaTool->dropSchema( - [ - $pluginEm->getClassMetadata(\XApi\Repository\Doctrine\Mapping\Attachment::class), - $pluginEm->getClassMetadata(\XApi\Repository\Doctrine\Mapping\StatementObject::class), - $pluginEm->getClassMetadata(\XApi\Repository\Doctrine\Mapping\Result::class), - $pluginEm->getClassMetadata(\XApi\Repository\Doctrine\Mapping\Verb::class), - $pluginEm->getClassMetadata(\XApi\Repository\Doctrine\Mapping\Extensions::class), - $pluginEm->getClassMetadata(\XApi\Repository\Doctrine\Mapping\Context::class), - $pluginEm->getClassMetadata(\XApi\Repository\Doctrine\Mapping\Actor::class), - $pluginEm->getClassMetadata(\XApi\Repository\Doctrine\Mapping\Statement::class), - ] - ); - } - /** * @param string|null $lrsUrl * @param string|null $lrsAuthUsername @@ -507,7 +438,7 @@ public function getLpResourceBlock(int $lpId) $session = api_get_session_entity(); $tools = Database::getManager() - ->getRepository(ToolLaunch::class) + ->getRepository(XApiToolLaunch::class) ->findByCourseAndSession($course, $session); $importIcon = Display::return_icon('import_scorm.png'); @@ -525,7 +456,6 @@ public function getLpResourceBlock(int $lpId) ) .''; - /** @var ToolLaunch $tool */ foreach ($tools as $tool) { $toolAnchor = Display::url( Security::remove_XSS($tool->getTitle()), @@ -553,48 +483,12 @@ public function getLpResourceBlock(int $lpId) return $return; } - /** - * @throws \Doctrine\ORM\Tools\ToolsException - */ - private function installPluginDbTables() - { - $em = Database::getManager(); - $pluginEm = self::getEntityManager(); - - $schemaTool = new SchemaTool($em); - $schemaTool->createSchema( - [ - $em->getClassMetadata(SharedStatement::class), - $em->getClassMetadata(ToolLaunch::class), - $em->getClassMetadata(LrsAuth::class), - $em->getClassMetadata(Cmi5Item::class), - $em->getClassMetadata(ActivityState::class), - $em->getClassMetadata(ActivityProfile::class), - $em->getClassMetadata(InternalLog::class), - ] - ); - - $pluginSchemaTool = new SchemaTool($pluginEm); - $pluginSchemaTool->createSchema( - [ - $pluginEm->getClassMetadata(\XApi\Repository\Doctrine\Mapping\Attachment::class), - $pluginEm->getClassMetadata(\XApi\Repository\Doctrine\Mapping\StatementObject::class), - $pluginEm->getClassMetadata(\XApi\Repository\Doctrine\Mapping\Result::class), - $pluginEm->getClassMetadata(\XApi\Repository\Doctrine\Mapping\Verb::class), - $pluginEm->getClassMetadata(\XApi\Repository\Doctrine\Mapping\Extensions::class), - $pluginEm->getClassMetadata(\XApi\Repository\Doctrine\Mapping\Context::class), - $pluginEm->getClassMetadata(\XApi\Repository\Doctrine\Mapping\Actor::class), - $pluginEm->getClassMetadata(\XApi\Repository\Doctrine\Mapping\Statement::class), - ] - ); - } - /** * @throws \Exception */ private function installInitialConfig() { - $uuidNamespace = Uuid::uuid1(); + $uuidNamespace = Uuid::v1()->toRfc4122(); $pluginName = $this->get_name(); $urlId = api_get_current_access_url_id(); diff --git a/public/plugin/xapi/start.php b/public/plugin/xapi/start.php index 3c8438fa35d..d50db19a27b 100644 --- a/public/plugin/xapi/start.php +++ b/public/plugin/xapi/start.php @@ -4,7 +4,7 @@ /* For licensing terms, see /license.txt */ -use Chamilo\PluginBundle\Entity\XApi\ToolLaunch; +use Chamilo\CoreBundle\Entity\XApiToolLaunch; require_once __DIR__.'/../../main/inc/global.inc.php'; @@ -16,7 +16,7 @@ $isAllowedToEdit = api_is_allowed_to_edit(); $em = Database::getManager(); -$toolLaunchRepo = $em->getRepository(ToolLaunch::class); +$toolLaunchRepo = $em->getRepository(XApiToolLaunch::class); $course = api_get_course_entity(); $session = api_get_session_entity(); @@ -27,7 +27,7 @@ $table = new SortableTable( 'tbl_xapi', function () use ($em, $course, $session, $isAllowedToEdit) { - return $em->getRepository(ToolLaunch::class) + return $em->getRepository(XApiToolLaunch::class) ->countByCourseAndSession($course, $session, !$isAllowedToEdit) ; }, @@ -36,7 +36,6 @@ function ($start, $limit, $orderBy, $orderDir) use ($toolLaunchRepo, $course, $s $data = []; - /** @var ToolLaunch $toolLaunch */ foreach ($tools as $toolLaunch) { $wasAddedInLp = $toolLaunchRepo->wasAddedInLp($toolLaunch); diff --git a/public/plugin/xapi/tincan/launch.php b/public/plugin/xapi/tincan/launch.php index 459721ca2c4..d1d0d3f7035 100644 --- a/public/plugin/xapi/tincan/launch.php +++ b/public/plugin/xapi/tincan/launch.php @@ -4,7 +4,7 @@ /* For licensing terms, see /license.txt */ -use Chamilo\PluginBundle\Entity\XApi\ToolLaunch; +use Chamilo\CoreBundle\Entity\XApiToolLaunch; use Symfony\Component\HttpFoundation\Request as HttpRequest; use Xabbuh\XApi\Common\Exception\NotFoundException; use Xabbuh\XApi\Model\Activity; @@ -29,7 +29,7 @@ $attemptId = $request->request->get('attempt_id'); $toolLaunch = $em->find( - ToolLaunch::class, + XApiToolLaunch::class, $request->request->getInt('id') ); diff --git a/public/plugin/xapi/tincan/stats.php b/public/plugin/xapi/tincan/stats.php index fd0b98d84ba..5abad6f01a0 100644 --- a/public/plugin/xapi/tincan/stats.php +++ b/public/plugin/xapi/tincan/stats.php @@ -3,7 +3,7 @@ declare(strict_types=1); /* For licensing terms, see /license.txt */ -use Chamilo\PluginBundle\Entity\XApi\ToolLaunch; +use Chamilo\CoreBundle\Entity\XApiToolLaunch; use Knp\Component\Pager\Paginator; use Symfony\Component\HttpFoundation\Request as HttpRequest; @@ -17,7 +17,7 @@ $em = Database::getManager(); $toolLaunch = $em->find( - ToolLaunch::class, + XApiToolLaunch::class, $request->query->getInt('id') ); diff --git a/public/plugin/xapi/tincan/stats_attempts.ajax.php b/public/plugin/xapi/tincan/stats_attempts.ajax.php index 2376d494edc..88b1ac6625f 100644 --- a/public/plugin/xapi/tincan/stats_attempts.ajax.php +++ b/public/plugin/xapi/tincan/stats_attempts.ajax.php @@ -4,7 +4,7 @@ /* For licensing terms, see /license.txt */ -use Chamilo\PluginBundle\Entity\XApi\ToolLaunch; +use Chamilo\CoreBundle\Entity\XApiToolLaunch; use Symfony\Component\HttpFoundation\Request as HttpRequest; use Xabbuh\XApi\Common\Exception\NotFoundException; use Xabbuh\XApi\Common\Exception\XApiException; @@ -34,7 +34,7 @@ $em = Database::getManager(); $toolLaunch = $em->find( - ToolLaunch::class, + XApiToolLaunch::class, $request->request->getInt('tool') ); diff --git a/public/plugin/xapi/tincan/stats_statements.ajax.php b/public/plugin/xapi/tincan/stats_statements.ajax.php index d3edcc3152a..2185ce155fa 100644 --- a/public/plugin/xapi/tincan/stats_statements.ajax.php +++ b/public/plugin/xapi/tincan/stats_statements.ajax.php @@ -4,7 +4,7 @@ /* For licensing terms, see /license.txt */ -use Chamilo\PluginBundle\Entity\XApi\ToolLaunch; +use Chamilo\CoreBundle\Entity\XApiToolLaunch; use Symfony\Component\HttpFoundation\Request as HttpRequest; use Xabbuh\XApi\Common\Exception\XApiException; use Xabbuh\XApi\Model\Activity; @@ -31,7 +31,7 @@ $em = Database::getManager(); $toolLaunch = $em->find( - ToolLaunch::class, + XApiToolLaunch::class, $request->request->getInt('tool') ); diff --git a/public/plugin/xapi/tincan/view.php b/public/plugin/xapi/tincan/view.php index e571a020996..2765161e1f0 100644 --- a/public/plugin/xapi/tincan/view.php +++ b/public/plugin/xapi/tincan/view.php @@ -4,7 +4,7 @@ /* For licensing terms, see /license.txt */ -use Chamilo\PluginBundle\Entity\XApi\ToolLaunch; +use Chamilo\CoreBundle\Entity\XApiToolLaunch; use Symfony\Component\HttpFoundation\Request as HttpRequest; use Xabbuh\XApi\Common\Exception\NotFoundException; use Xabbuh\XApi\Model\Activity; @@ -28,7 +28,7 @@ $em = Database::getManager(); $toolLaunch = $em->find( - ToolLaunch::class, + XApiToolLaunch::class, $request->query->getInt('id') ); diff --git a/public/plugin/xapi/tool_delete.php b/public/plugin/xapi/tool_delete.php index 086bbaba78c..3776965fb48 100644 --- a/public/plugin/xapi/tool_delete.php +++ b/public/plugin/xapi/tool_delete.php @@ -4,7 +4,7 @@ /* For licensing terms, see /license.txt */ -use Chamilo\PluginBundle\Entity\XApi\ToolLaunch; +use Chamilo\CoreBundle\Entity\XApiToolLaunch; use Symfony\Component\HttpFoundation\Request as HttpRequest; require_once __DIR__.'/../../main/inc/global.inc.php'; @@ -17,7 +17,7 @@ $em = Database::getManager(); $toolLaunch = $em->find( - ToolLaunch::class, + XApiToolLaunch::class, $request->query->getInt('delete') ); diff --git a/public/plugin/xapi/tool_edit.php b/public/plugin/xapi/tool_edit.php index d5747a1be06..c357d184a48 100644 --- a/public/plugin/xapi/tool_edit.php +++ b/public/plugin/xapi/tool_edit.php @@ -4,7 +4,7 @@ /* For licensing terms, see /license.txt */ -use Chamilo\PluginBundle\Entity\XApi\ToolLaunch; +use Chamilo\CoreBundle\Entity\XApiToolLaunch; use Symfony\Component\HttpFoundation\Request as HttpRequest; require_once __DIR__.'/../../main/inc/global.inc.php'; @@ -17,7 +17,7 @@ $em = Database::getManager(); $toolLaunch = $em->find( - ToolLaunch::class, + XApiToolLaunch::class, $request->query->getInt('edit') ); diff --git a/public/plugin/xapi/uninstall.php b/public/plugin/xapi/uninstall.php deleted file mode 100644 index a13bd5e1cf9..00000000000 --- a/public/plugin/xapi/uninstall.php +++ /dev/null @@ -1,7 +0,0 @@ -uninstall(); diff --git a/src/CoreBundle/Entity/XApiSharedStatement.php b/src/CoreBundle/Entity/XApiSharedStatement.php index 48fba8e14fd..8138c9a8a3d 100644 --- a/src/CoreBundle/Entity/XApiSharedStatement.php +++ b/src/CoreBundle/Entity/XApiSharedStatement.php @@ -28,6 +28,13 @@ class XApiSharedStatement #[ORM\Column] private ?bool $sent = null; + public function __construct(array $statement, string $uuid = null, bool $sent = false) + { + $this->statement = $statement; + $this->uuid = Uuid::fromString($uuid); + $this->sent = $sent; + } + public function getId(): ?int { return $this->id; diff --git a/src/CoreBundle/Entity/XApiToolLaunch.php b/src/CoreBundle/Entity/XApiToolLaunch.php index cbdd7761f83..f76032777d9 100644 --- a/src/CoreBundle/Entity/XApiToolLaunch.php +++ b/src/CoreBundle/Entity/XApiToolLaunch.php @@ -46,7 +46,7 @@ class XApiToolLaunch private ?string $activityType = null; #[ORM\Column] - private ?bool $allowMultipleAttempts = null; + private ?bool $allowMultipleAttempts = true; #[ORM\Column(length: 255, nullable: true)] private ?string $lrsUrl = null; From 8be4bfa5eb0e8455c3c3953d0e5d17373238550a Mon Sep 17 00:00:00 2001 From: Angel Fernando Quiroz Campos <1697880+AngelFQC@users.noreply.github.com> Date: Wed, 3 Jul 2024 18:19:00 -0500 Subject: [PATCH 4/4] Plugin: XApi: Get changes from fixes for 1.11.x See #3943 --- .../src/Model/StatementsFilterFactory.php | 1 + .../lrs-bundle/src/Resources/config/routing.xml | 2 +- .../src/StatementRepository.php | 14 ++++++++++++-- src/CoreBundle/Entity/XApiObject.php | 2 +- src/CoreBundle/Entity/XApiVerb.php | 8 ++++---- .../Schema/V200/Version20240515094800.php | 4 ++-- 6 files changed, 21 insertions(+), 10 deletions(-) diff --git a/public/plugin/xapi/php-xapi/lrs-bundle/src/Model/StatementsFilterFactory.php b/public/plugin/xapi/php-xapi/lrs-bundle/src/Model/StatementsFilterFactory.php index ba0e2c43c1a..7a5e35aef11 100644 --- a/public/plugin/xapi/php-xapi/lrs-bundle/src/Model/StatementsFilterFactory.php +++ b/public/plugin/xapi/php-xapi/lrs-bundle/src/Model/StatementsFilterFactory.php @@ -84,6 +84,7 @@ public function createFromParameterBag(ParameterBag $parameters) $filter->descending(); } + $filter->cursor($parameters->getInt('cursor')); $filter->limit($parameters->getInt('limit')); return $filter; diff --git a/public/plugin/xapi/php-xapi/lrs-bundle/src/Resources/config/routing.xml b/public/plugin/xapi/php-xapi/lrs-bundle/src/Resources/config/routing.xml index 27c4d2bc101..f0e75024fb3 100644 --- a/public/plugin/xapi/php-xapi/lrs-bundle/src/Resources/config/routing.xml +++ b/public/plugin/xapi/php-xapi/lrs-bundle/src/Resources/config/routing.xml @@ -13,7 +13,7 @@ - xapi_lrs.controller.statement.post:postStatement + xapi_lrs.controller.statement.post:postStatements statement true diff --git a/public/plugin/xapi/php-xapi/repository-doctrine-orm/src/StatementRepository.php b/public/plugin/xapi/php-xapi/repository-doctrine-orm/src/StatementRepository.php index 4efa0036a51..a32e2d47c4f 100644 --- a/public/plugin/xapi/php-xapi/repository-doctrine-orm/src/StatementRepository.php +++ b/public/plugin/xapi/php-xapi/repository-doctrine-orm/src/StatementRepository.php @@ -38,15 +38,25 @@ public function findStatements(array $criteria) $criteria['context'] = $contexts; } + if (!empty($criteria['verb'])) { + $verbs = $this->_em->getRepository(Verb::class)->findBy(['id' => $criteria['verb']]); + + $criteria['verb'] = $verbs; + } + unset( $criteria['registration'], $criteria['related_activities'], $criteria['related_agents'], $criteria['ascending'], - $criteria['limit'] ); - return parent::findBy($criteria, ['created' => 'ASC']); + return parent::findBy( + $criteria, + ['created' => 'ASC'], + $criteria['limit'] ?? null, + $criteria['cursor'] ?? null + ); } public function storeStatement(Statement $mappedStatement, $flush = true): void diff --git a/src/CoreBundle/Entity/XApiObject.php b/src/CoreBundle/Entity/XApiObject.php index 719dc24f92e..16615605997 100644 --- a/src/CoreBundle/Entity/XApiObject.php +++ b/src/CoreBundle/Entity/XApiObject.php @@ -90,7 +90,7 @@ class XApiObject /** * @var Collection */ - #[ORM\OneToMany(mappedBy: 'group', targetEntity: self::class)] + #[ORM\OneToMany(mappedBy: 'group', targetEntity: self::class, cascade: ['persist', 'remove'])] #[ORM\JoinColumn(referencedColumnName: 'identifier')] private Collection $members; diff --git a/src/CoreBundle/Entity/XApiVerb.php b/src/CoreBundle/Entity/XApiVerb.php index 79094d26e33..a348571512b 100644 --- a/src/CoreBundle/Entity/XApiVerb.php +++ b/src/CoreBundle/Entity/XApiVerb.php @@ -20,8 +20,8 @@ class XApiVerb #[ORM\Column(length: 255)] private ?string $id = null; - #[ORM\Column] - private array $display = []; + #[ORM\Column(nullable: true)] + private ?array $display = null; public function getIdentifier(): ?int { @@ -40,12 +40,12 @@ public function setId(string $id): static return $this; } - public function getDisplay(): array + public function getDisplay(): ?array { return $this->display; } - public function setDisplay(array $display): static + public function setDisplay(?array $display): static { $this->display = $display; diff --git a/src/CoreBundle/Migrations/Schema/V200/Version20240515094800.php b/src/CoreBundle/Migrations/Schema/V200/Version20240515094800.php index 5f65b2a296d..dcede6cc807 100644 --- a/src/CoreBundle/Migrations/Schema/V200/Version20240515094800.php +++ b/src/CoreBundle/Migrations/Schema/V200/Version20240515094800.php @@ -52,9 +52,9 @@ public function up(Schema $schema): void } if ($hasTblVerb) { - $this->addSql("ALTER TABLE xapi_verb CHANGE display display LONGTEXT NOT NULL COMMENT '(DC2Type:json)'"); + $this->addSql("ALTER TABLE xapi_verb CHANGE display display LONGTEXT NULL COMMENT '(DC2Type:json)'"); } else { - $this->addSql("CREATE TABLE xapi_verb (identifier INT AUTO_INCREMENT NOT NULL, id VARCHAR(255) NOT NULL, display LONGTEXT NOT NULL COMMENT '(DC2Type:json)', PRIMARY KEY(identifier)) DEFAULT CHARACTER SET utf8mb4 COLLATE `utf8mb4_unicode_ci` ENGINE = InnoDB ROW_FORMAT = DYNAMIC"); + $this->addSql("CREATE TABLE xapi_verb (identifier INT AUTO_INCREMENT NOT NULL, id VARCHAR(255) NOT NULL, display LONGTEXT NULL COMMENT '(DC2Type:json)', PRIMARY KEY(identifier)) DEFAULT CHARACTER SET utf8mb4 COLLATE `utf8mb4_unicode_ci` ENGINE = InnoDB ROW_FORMAT = DYNAMIC"); } if ($hasTblObject) {