Merge branch 'MDL-68746-master' of git://github.com/sarjona/moodle
authorJake Dallimore <jake@moodle.com>
Thu, 28 May 2020 03:56:23 +0000 (11:56 +0800)
committerJake Dallimore <jake@moodle.com>
Thu, 28 May 2020 03:56:23 +0000 (11:56 +0800)
665 files changed:
.eslintignore
.stylelintignore
admin/settings/courses.php
admin/settings/license.php [new file with mode: 0644]
admin/settings/plugins.php
admin/settings/top.php
admin/tool/dataprivacy/tests/behat/manage_categories.feature
admin/tool/dataprivacy/tests/behat/manage_purposes.feature
admin/tool/licensemanager/amd/build/delete_license.min.js [new file with mode: 0644]
admin/tool/licensemanager/amd/build/delete_license.min.js.map [new file with mode: 0644]
admin/tool/licensemanager/amd/src/delete_license.js [new file with mode: 0644]
admin/tool/licensemanager/classes/form/edit_license.php [new file with mode: 0644]
admin/tool/licensemanager/classes/helper.php [new file with mode: 0644]
admin/tool/licensemanager/classes/manager.php [new file with mode: 0644]
admin/tool/licensemanager/classes/output/renderer.php [new file with mode: 0644]
admin/tool/licensemanager/classes/output/table.php [new file with mode: 0644]
admin/tool/licensemanager/classes/privacy/provider.php [new file with mode: 0644]
admin/tool/licensemanager/index.php [moved from admin/licenses.php with 55% similarity]
admin/tool/licensemanager/lang/en/tool_licensemanager.php [new file with mode: 0644]
admin/tool/licensemanager/settings.php [new file with mode: 0644]
admin/tool/licensemanager/tests/behat/delete_license.feature [new file with mode: 0644]
admin/tool/licensemanager/tests/behat/edit_license.feature [new file with mode: 0644]
admin/tool/licensemanager/tests/behat/license_manager.feature [new file with mode: 0644]
admin/tool/licensemanager/tests/helper_test.php [new file with mode: 0644]
admin/tool/licensemanager/tests/manager_test.php [new file with mode: 0644]
admin/tool/licensemanager/version.php [new file with mode: 0644]
admin/tool/usertours/lang/en/tool_usertours.php
analytics/tests/behat/manage_models.feature
backup/util/helper/tests/async_helper_test.php
badges/amd/build/backpackactions.min.js [new file with mode: 0644]
badges/amd/build/backpackactions.min.js.map [new file with mode: 0644]
badges/amd/build/selectors.min.js [new file with mode: 0644]
badges/amd/build/selectors.min.js.map [new file with mode: 0644]
badges/amd/src/backpackactions.js [new file with mode: 0644]
badges/amd/src/selectors.js [new file with mode: 0644]
badges/backpack-connect.php [new file with mode: 0644]
badges/backpack-export.php [new file with mode: 0644]
badges/backpacks.php
badges/classes/backpack_api2p1.php [new file with mode: 0644]
badges/classes/backpack_api2p1_mapping.php [new file with mode: 0644]
badges/classes/form/backpack.php
badges/classes/form/external_backpack.php
badges/classes/helper.php [new file with mode: 0644]
badges/classes/oauth2/auth.php [new file with mode: 0644]
badges/classes/oauth2/badge_backpack_oauth2.php [new file with mode: 0644]
badges/classes/oauth2/client.php [new file with mode: 0644]
badges/classes/output/external_backpacks_page.php
badges/classes/privacy/provider.php
badges/mybackpack.php
badges/oauth2callback.php [new file with mode: 0644]
badges/renderer.php
badges/templates/external_backpacks_page.mustache
badges/tests/badgeslib_test.php
badges/tests/behat/backpack.feature [new file with mode: 0644]
badges/tests/privacy_test.php
blocks/myoverview/classes/output/main.php
blocks/myoverview/lang/en/block_myoverview.php
blocks/myoverview/lib.php
blocks/myoverview/templates/nav-sort-selector.mustache
blocks/myoverview/templates/view-list.mustache
blocks/myoverview/templates/view-summary.mustache
blocks/myoverview/tests/behat/block_myoverview_dashboard.feature
blocks/myoverview/tests/privacy_test.php
blocks/site_main_menu/tests/behat/add_url.feature
blocks/timeline/templates/event-list-item.mustache
calendar/amd/build/selectors.min.js
calendar/amd/build/selectors.min.js.map
calendar/amd/build/view_manager.min.js
calendar/amd/build/view_manager.min.js.map
calendar/amd/src/selectors.js
calendar/amd/src/view_manager.js
calendar/classes/external/calendar_event_exporter.php
calendar/classes/external/event_action_exporter.php
calendar/classes/external/event_exporter.php
calendar/classes/external/event_exporter_base.php
calendar/classes/external/event_icon_exporter.php
calendar/classes/local/event/container.php
calendar/classes/local/event/data_access/event_vault.php
calendar/classes/local/event/entities/action_event.php
calendar/classes/local/event/entities/event.php
calendar/classes/local/event/entities/event_interface.php
calendar/classes/local/event/factories/event_abstract_factory.php
calendar/classes/local/event/mappers/event_mapper.php
calendar/externallib.php
calendar/lib.php
calendar/renderer.php
calendar/templates/event_details.mustache
calendar/templates/event_item.mustache
calendar/templates/event_summary_modal.mustache
calendar/templates/header.mustache
calendar/templates/minicalendar_day_link.mustache
calendar/templates/month_detailed.mustache
calendar/tests/action_event_test.php
calendar/tests/event_mapper_test.php
calendar/tests/event_test.php
calendar/tests/helpers.php
calendar/tests/repeat_event_collection_test.php
calendar/upgrade.txt
config-dist.php
contentbank/amd/build/search.min.js
contentbank/amd/build/search.min.js.map
contentbank/amd/build/selectors.min.js
contentbank/amd/build/selectors.min.js.map
contentbank/amd/build/sort.min.js [new file with mode: 0644]
contentbank/amd/build/sort.min.js.map [new file with mode: 0644]
contentbank/amd/src/search.js
contentbank/amd/src/selectors.js
contentbank/amd/src/sort.js [new file with mode: 0644]
contentbank/classes/content.php
contentbank/classes/contenttype.php
contentbank/classes/output/bankcontent.php
contentbank/index.php
contentbank/templates/bankcontent.mustache
contentbank/templates/bankcontent/toolbar.mustache
contentbank/tests/behat/sort_content.feature [new file with mode: 0644]
contentbank/tests/fixtures/testable_contenttype.php
course/amd/build/activitychooser.min.js
course/amd/build/activitychooser.min.js.map
course/amd/build/local/activitychooser/dialogue.min.js
course/amd/build/local/activitychooser/dialogue.min.js.map
course/amd/build/local/activitychooser/selectors.min.js
course/amd/build/local/activitychooser/selectors.min.js.map
course/amd/src/activitychooser.js
course/amd/src/local/activitychooser/dialogue.js
course/amd/src/local/activitychooser/selectors.js
course/classes/category.php
course/classes/deletecategory_form.php
course/classes/management_renderer.php
course/lib.php
course/modlib.php
course/renderer.php
course/templates/activity_navigation.mustache
course/templates/activitychooser.mustache
course/templates/coursecard.mustache
course/templates/local/activitychooser/item.mustache
course/tests/behat/activity_chooser.feature
course/tests/behat/behat_course.php
course/tests/behat/recommend_activities.feature
course/tests/behat/restrict_available_activities.feature
course/tests/behat/search_recommended_activities.feature
course/tests/category_hooks_test.php [new file with mode: 0644]
course/tests/category_test.php
course/tests/courselib_test.php
course/tests/fixtures/mock_hooks.php [new file with mode: 0644]
course/upgrade.txt
dataformat/html/classes/writer.php
dataformat/json/classes/writer.php
dataformat/pdf/classes/writer.php
dataformat/pdf/tests/writer_test.php [new file with mode: 0644]
dataformat/upgrade.txt
enrol/manual/ajax.php
enrol/manual/amd/build/quickenrolment.min.js
enrol/manual/amd/build/quickenrolment.min.js.map
enrol/manual/amd/src/quickenrolment.js
enrol/manual/tests/behat/quickenrolment.feature
files/renderer.php
files/tests/behat/license_help_modal.feature [new file with mode: 0644]
grade/edit/letter/index.php
grade/edit/outcome/course_form.html
grade/lib.php
grade/report/grader/lib.php
grade/report/grader/styles.css
grade/report/lib.php
grade/report/singleview/classes/local/screen/grade.php
grade/report/singleview/classes/local/screen/tablelike.php
grade/report/singleview/classes/local/screen/user.php
grade/report/singleview/templates/text_attribute.mustache
grade/tests/report_graderlib_test.php
grade/tests/reportuserlib_test.php
h5p/classes/api.php
h5p/classes/helper.php
h5p/classes/player.php
h5p/tests/api_test.php
h5p/tests/external_test.php
h5p/tests/generator/lib.php
h5p/tests/helper_test.php
install/lang/eu/error.php
install/lang/eu/install.php
lang/en/admin.php
lang/en/badges.php
lang/en/cache.php
lang/en/calendar.php
lang/en/contentbank.php
lang/en/course.php
lang/en/deprecated.txt
lang/en/error.php
lang/en/license.php
lang/en/message.php
lang/en/moodle.php
lang/en/repository.php
lang/en/user.php
lib/adminlib.php
lib/amd/build/form-autocomplete.min.js
lib/amd/build/form-autocomplete.min.js.map
lib/amd/build/notification.min.js
lib/amd/build/notification.min.js.map
lib/amd/build/tag.min.js
lib/amd/build/tag.min.js.map
lib/amd/build/templates.min.js
lib/amd/build/templates.min.js.map
lib/amd/src/form-autocomplete.js
lib/amd/src/notification.js
lib/amd/src/tag.js
lib/amd/src/templates.js
lib/badgeslib.php
lib/behat/classes/behat_core_generator.php
lib/behat/classes/behat_generator_base.php
lib/behat/classes/partial_named_selector.php
lib/classes/dataformat/base.php
lib/classes/dataformat/spout_base.php
lib/classes/event/course_category_deleted.php
lib/classes/output/icon_system_fontawesome.php
lib/classes/plugin_manager.php
lib/classes/session/redis.php
lib/db/access.php
lib/db/caches.php
lib/db/install.xml
lib/db/upgrade.php
lib/db/upgradelib.php
lib/editor/atto/db/upgrade.php
lib/editor/atto/settings.php
lib/editor/atto/version.php
lib/filestorage/file_archive.php
lib/filestorage/zip_archive.php
lib/form/dateselector.php
lib/form/datetimeselector.php
lib/form/filemanager.js
lib/form/filemanager.php
lib/form/templates/element-group-inline.mustache
lib/gradelib.php
lib/grouplib.php
lib/licenselib.php
lib/moodlelib.php
lib/outputcomponents.php
lib/outputrenderers.php
lib/questionlib.php
lib/table/amd/build/dynamic.min.js
lib/table/amd/build/dynamic.min.js.map
lib/table/amd/build/local/dynamic/repository.min.js
lib/table/amd/build/local/dynamic/repository.min.js.map
lib/table/amd/src/dynamic.js
lib/table/amd/src/local/dynamic/repository.js
lib/table/classes/external/dynamic/fetch.php
lib/table/classes/local/filter/filter.php
lib/table/classes/local/filter/filterset.php
lib/table/tests/external/dynamic/fetch_test.php
lib/tablelib.php
lib/templates/action_menu_link.mustache
lib/templates/action_menu_trigger.mustache
lib/templates/filemanager_chooselicense.mustache [new file with mode: 0644]
lib/templates/filemanager_fileselect.mustache
lib/templates/filemanager_licenselinks.mustache [new file with mode: 0644]
lib/templates/filemanager_modal_generallayout.mustache
lib/templates/filemanager_page_generallayout.mustache
lib/templates/filemanager_selectlayout.mustache
lib/templates/filemanager_uploadform.mustache
lib/templates/form_autocomplete_input.mustache
lib/templates/form_autocomplete_layout.mustache [new file with mode: 0644]
lib/templates/full_header.mustache
lib/templates/inplace_editable.mustache
lib/templates/local/toast/message.mustache
lib/templates/loginform.mustache
lib/templates/navbar.mustache
lib/templates/preferences_groups.mustache
lib/testing/generator/data_generator.php
lib/testing/tests/generator_test.php
lib/tests/accesslib_test.php
lib/tests/behat/app_behat_runtime.js
lib/tests/behat/behat_app.php
lib/tests/event/contentbank_content_viewed_test.php
lib/tests/filestorage_zip_archive_test.php [new file with mode: 0644]
lib/tests/gradelib_test.php
lib/tests/grouplib_test.php
lib/tests/licenselib_test.php [new file with mode: 0644]
lib/tests/moodlelib_test.php
lib/tests/outputcomponents_test.php
lib/tests/questionlib_test.php
lib/tests/rsslib_test.php
lib/tests/upgradelib_test.php
lib/upgrade.txt
message/amd/build/message_drawer.min.js
message/amd/build/message_drawer.min.js.map
message/amd/build/message_drawer_view_conversation.min.js
message/amd/build/message_drawer_view_conversation.min.js.map
message/amd/src/message_drawer.js
message/amd/src/message_drawer_view_conversation.js
message/classes/api.php
message/templates/message_drawer.mustache
message/templates/message_drawer_view_conversation_footer.mustache
message/templates/message_drawer_view_conversation_footer_content.mustache
message/templates/message_drawer_view_conversation_header.mustache
message/templates/message_drawer_view_group_info_body_content.mustache
message/templates/message_drawer_view_overview_header.mustache
message/templates/message_drawer_view_overview_section.mustache
mod/assign/locallib.php
mod/assign/tests/locallib_test.php
mod/choice/report.php
mod/forum/export.php
mod/forum/report/summary/tests/behat/course_summary.feature
mod/forum/report/summary/tests/behat/summary_data_post_dates.feature
mod/h5pactivity/classes/external/get_attempts.php [new file with mode: 0644]
mod/h5pactivity/classes/external/get_h5pactivities_by_courses.php [new file with mode: 0644]
mod/h5pactivity/classes/external/get_results.php [new file with mode: 0644]
mod/h5pactivity/classes/external/h5pactivity_summary_exporter.php [new file with mode: 0644]
mod/h5pactivity/classes/local/manager.php
mod/h5pactivity/classes/local/report/results.php
mod/h5pactivity/db/services.php
mod/h5pactivity/lib.php
mod/h5pactivity/tests/behat/add_h5pactivity.feature
mod/h5pactivity/tests/external/get_attempts_test.php [new file with mode: 0644]
mod/h5pactivity/tests/external/get_h5pactivities_by_courses_test.php [new file with mode: 0644]
mod/h5pactivity/tests/external/get_results_test.php [new file with mode: 0644]
mod/h5pactivity/tests/generator/lib.php
mod/h5pactivity/tests/local/manager_test.php
mod/h5pactivity/version.php
mod/h5pactivity/view.php
mod/lti/amd/build/contentitem.min.js
mod/lti/amd/build/contentitem.min.js.map
mod/lti/amd/src/contentitem.js
mod/lti/classes/local/ltiservice/service_base.php
mod/lti/lib.php
mod/lti/locallib.php
mod/lti/mod_form.php
mod/lti/service/gradebookservices/backup/moodle2/backup_ltiservice_gradebookservices_subplugin.class.php
mod/lti/service/gradebookservices/backup/moodle2/restore_ltiservice_gradebookservices_subplugin.class.php
mod/lti/service/gradebookservices/classes/local/resources/lineitem.php
mod/lti/service/gradebookservices/classes/local/resources/lineitems.php
mod/lti/service/gradebookservices/classes/local/service/gradebookservices.php
mod/lti/service/gradebookservices/db/install.xml
mod/lti/service/gradebookservices/db/upgrade.php [new file with mode: 0644]
mod/lti/service/gradebookservices/tests/gradebookservices_test.php [new file with mode: 0644]
mod/lti/service/gradebookservices/version.php
mod/lti/tests/lib_test.php
mod/lti/version.php
mod/quiz/attemptlib.php
mod/quiz/comment.php
mod/quiz/editrandom.php
mod/quiz/locallib.php
mod/quiz/report/grading/report.php
mod/quiz/report/responses/first_or_all_responses_table.php
mod/quiz/styles.css
mod/quiz/tests/attempt_walkthrough_test.php
mod/quiz/tests/behat/editing_add_random.feature
mod/quiz/tests/behat/editing_edit_random.feature [new file with mode: 0644]
mod/wiki/parser/markups/html.php
mod/wiki/tests/wikiparser_test.php
mod/workshop/assessment.php
mod/workshop/classes/privacy/provider.php
mod/workshop/exassessment.php
mod/workshop/exsubmission.php
mod/workshop/lang/en/workshop.php
mod/workshop/renderer.php
mod/workshop/submission.php
mod/workshop/tests/behat/workshop_section_remembered.feature [new file with mode: 0644]
mod/workshop/version.php
mod/workshop/view.php
privacy/classes/local/request/moodle_content_writer.php
privacy/tests/moodle_content_writer_test.php
question/behaviour/behaviourbase.php
question/behaviour/interactive/tests/walkthrough_test.php
question/behaviour/missing/tests/missingbehaviour_test.php
question/behaviour/rendererbase.php
question/classes/bank/action_column_base.php
question/classes/bank/copy_action_column.php
question/classes/bank/edit_action_column.php
question/classes/bank/export_xml_action_column.php
question/classes/bank/preview_action_column.php
question/classes/privacy/provider.php
question/engine/bank.php
question/engine/datalib.php
question/engine/lib.php
question/engine/questionattempt.php
question/engine/questionusage.php
question/engine/renderer.php
question/engine/tests/helpers.php
question/engine/tests/questionattempt_db_test.php
question/engine/tests/questionattempt_test.php
question/engine/tests/questionusagebyactivity_data_test.php
question/engine/tests/questionusagebyactivity_test.php
question/engine/tests/unitofwork_test.php
question/engine/upgrade.txt
question/format/xml/format.php
question/format/xml/tests/xmlformat_test.php
question/lib.php
question/preview.php
question/previewlib.php
question/tests/behat/delete_questions.feature
question/tests/behat/edit_question_tags.feature [new file with mode: 0644]
question/tests/behat/edit_questions.feature
question/tests/generator/behat_core_question_generator.php [new file with mode: 0644]
question/tests/generator/lib.php
question/tests/privacy_helper.php
question/type/ddimageortext/amd/build/form.min.js
question/type/ddimageortext/amd/build/form.min.js.map
question/type/ddimageortext/amd/build/question.min.js
question/type/ddimageortext/amd/build/question.min.js.map
question/type/ddimageortext/amd/src/form.js
question/type/ddimageortext/amd/src/question.js
question/type/ddimageortext/edit_ddimageortext_form.php
question/type/ddimageortext/questiontype.php
question/type/ddimageortext/questiontypebase.php
question/type/ddimageortext/rendererbase.php
question/type/ddimageortext/styles.css
question/type/ddimageortext/tests/behat/behat_qtype_ddimageortext.php
question/type/ddmarker/amd/build/form.min.js
question/type/ddmarker/amd/build/form.min.js.map
question/type/ddmarker/amd/src/form.js
question/type/ddmarker/edit_ddmarker_form.php
question/type/ddmarker/question.php
question/type/ddmarker/questiontype.php
question/type/ddwtos/amd/build/ddwtos.min.js
question/type/ddwtos/amd/build/ddwtos.min.js.map
question/type/ddwtos/amd/src/ddwtos.js
question/type/ddwtos/styles.css
question/type/ddwtos/tests/helper.php
question/type/ddwtos/tests/questiontype_test.php
question/type/gapselect/questiontypebase.php
question/type/gapselect/tests/helper.php
question/type/gapselect/tests/question_test.php
question/type/gapselect/tests/questiontype_test.php
question/type/gapselect/tests/walkthrough_test.php
question/type/missingtype/tests/missingtype_test.php
question/type/multichoice/backup/moodle1/lib.php
question/type/multichoice/backup/moodle2/backup_qtype_multichoice_plugin.class.php
question/type/multichoice/db/install.xml
question/type/multichoice/db/upgrade.php
question/type/multichoice/edit_multichoice_form.php
question/type/multichoice/lang/en/qtype_multichoice.php
question/type/multichoice/question.php
question/type/multichoice/questiontype.php
question/type/multichoice/renderer.php
question/type/multichoice/tests/behat/export.feature
question/type/multichoice/tests/fixtures/testquestion.moodle.xml
question/type/multichoice/tests/helper.php
question/type/multichoice/tests/question_single_test.php
question/type/multichoice/tests/upgradelibnewqe_test.php
question/type/multichoice/tests/walkthrough_test.php
question/type/multichoice/version.php
question/type/questiontypebase.php
question/type/random/questiontype.php
question/upgrade.txt
repository/contentbank/tests/behat/select_content.feature
repository/draftfiles_ajax.php
repository/filepicker.js
repository/lib.php
repository/recent/lang/en/repository_recent.php
repository/recent/lib.php
repository/recent/tests/generator/lib.php
repository/recent/tests/lib_test.php [new file with mode: 0644]
repository/tests/behat/delete_files.feature
theme/boost/amd/build/alert.min.js [deleted file]
theme/boost/amd/build/alert.min.js.map [deleted file]
theme/boost/amd/build/bootstrap/alert.min.js [new file with mode: 0644]
theme/boost/amd/build/bootstrap/alert.min.js.map [new file with mode: 0644]
theme/boost/amd/build/bootstrap/button.min.js [new file with mode: 0644]
theme/boost/amd/build/bootstrap/button.min.js.map [new file with mode: 0644]
theme/boost/amd/build/bootstrap/carousel.min.js [new file with mode: 0644]
theme/boost/amd/build/bootstrap/carousel.min.js.map [new file with mode: 0644]
theme/boost/amd/build/bootstrap/collapse.min.js [new file with mode: 0644]
theme/boost/amd/build/bootstrap/collapse.min.js.map [new file with mode: 0644]
theme/boost/amd/build/bootstrap/dropdown.min.js [new file with mode: 0644]
theme/boost/amd/build/bootstrap/dropdown.min.js.map [new file with mode: 0644]
theme/boost/amd/build/bootstrap/index.min.js [new file with mode: 0644]
theme/boost/amd/build/bootstrap/index.min.js.map [new file with mode: 0644]
theme/boost/amd/build/bootstrap/modal.min.js [new file with mode: 0644]
theme/boost/amd/build/bootstrap/modal.min.js.map [new file with mode: 0644]
theme/boost/amd/build/bootstrap/popover.min.js [new file with mode: 0644]
theme/boost/amd/build/bootstrap/popover.min.js.map [new file with mode: 0644]
theme/boost/amd/build/bootstrap/scrollspy.min.js [new file with mode: 0644]
theme/boost/amd/build/bootstrap/scrollspy.min.js.map [new file with mode: 0644]
theme/boost/amd/build/bootstrap/tab.min.js [new file with mode: 0644]
theme/boost/amd/build/bootstrap/tab.min.js.map [new file with mode: 0644]
theme/boost/amd/build/bootstrap/toast.min.js [new file with mode: 0644]
theme/boost/amd/build/bootstrap/toast.min.js.map [new file with mode: 0644]
theme/boost/amd/build/bootstrap/tools/sanitizer.min.js [new file with mode: 0644]
theme/boost/amd/build/bootstrap/tools/sanitizer.min.js.map [new file with mode: 0644]
theme/boost/amd/build/bootstrap/tooltip.min.js [new file with mode: 0644]
theme/boost/amd/build/bootstrap/tooltip.min.js.map [new file with mode: 0644]
theme/boost/amd/build/bootstrap/util.min.js [new file with mode: 0644]
theme/boost/amd/build/bootstrap/util.min.js.map [new file with mode: 0644]
theme/boost/amd/build/button.min.js [deleted file]
theme/boost/amd/build/button.min.js.map [deleted file]
theme/boost/amd/build/carousel.min.js [deleted file]
theme/boost/amd/build/carousel.min.js.map [deleted file]
theme/boost/amd/build/collapse.min.js [deleted file]
theme/boost/amd/build/collapse.min.js.map [deleted file]
theme/boost/amd/build/dropdown.min.js [deleted file]
theme/boost/amd/build/dropdown.min.js.map [deleted file]
theme/boost/amd/build/index.min.js [deleted file]
theme/boost/amd/build/index.min.js.map [deleted file]
theme/boost/amd/build/loader.min.js
theme/boost/amd/build/loader.min.js.map
theme/boost/amd/build/modal.min.js [deleted file]
theme/boost/amd/build/modal.min.js.map [deleted file]
theme/boost/amd/build/popover.min.js
theme/boost/amd/build/popover.min.js.map
theme/boost/amd/build/sanitizer.min.js [deleted file]
theme/boost/amd/build/sanitizer.min.js.map [deleted file]
theme/boost/amd/build/scrollspy.min.js [deleted file]
theme/boost/amd/build/scrollspy.min.js.map [deleted file]
theme/boost/amd/build/tab.min.js [deleted file]
theme/boost/amd/build/tab.min.js.map [deleted file]
theme/boost/amd/build/tether.min.js [deleted file]
theme/boost/amd/build/tether.min.js.map [deleted file]
theme/boost/amd/build/toast.min.js
theme/boost/amd/build/toast.min.js.map
theme/boost/amd/build/tooltip.min.js [deleted file]
theme/boost/amd/build/tooltip.min.js.map [deleted file]
theme/boost/amd/build/util.min.js [deleted file]
theme/boost/amd/build/util.min.js.map [deleted file]
theme/boost/amd/src/alert.js [deleted file]
theme/boost/amd/src/bootstrap/alert.js [new file with mode: 0644]
theme/boost/amd/src/bootstrap/button.js [new file with mode: 0644]
theme/boost/amd/src/bootstrap/carousel.js [new file with mode: 0644]
theme/boost/amd/src/bootstrap/collapse.js [new file with mode: 0644]
theme/boost/amd/src/bootstrap/dropdown.js [new file with mode: 0644]
theme/boost/amd/src/bootstrap/index.js [new file with mode: 0644]
theme/boost/amd/src/bootstrap/modal.js [new file with mode: 0644]
theme/boost/amd/src/bootstrap/popover.js [new file with mode: 0644]
theme/boost/amd/src/bootstrap/scrollspy.js [new file with mode: 0644]
theme/boost/amd/src/bootstrap/tab.js [new file with mode: 0644]
theme/boost/amd/src/bootstrap/toast.js [new file with mode: 0644]
theme/boost/amd/src/bootstrap/tools/sanitizer.js [new file with mode: 0644]
theme/boost/amd/src/bootstrap/tooltip.js [new file with mode: 0644]
theme/boost/amd/src/bootstrap/util.js [new file with mode: 0644]
theme/boost/amd/src/button.js [deleted file]
theme/boost/amd/src/carousel.js [deleted file]
theme/boost/amd/src/collapse.js [deleted file]
theme/boost/amd/src/dropdown.js [deleted file]
theme/boost/amd/src/index.js [deleted file]
theme/boost/amd/src/loader.js
theme/boost/amd/src/modal.js [deleted file]
theme/boost/amd/src/popover.js
theme/boost/amd/src/sanitizer.js [deleted file]
theme/boost/amd/src/scrollspy.js [deleted file]
theme/boost/amd/src/tab.js [deleted file]
theme/boost/amd/src/tether.js [deleted file]
theme/boost/amd/src/toast.js
theme/boost/amd/src/tooltip.js [deleted file]
theme/boost/amd/src/util.js [deleted file]
theme/boost/readme_moodle.txt
theme/boost/scss/bootstrap/_badge.scss
theme/boost/scss/bootstrap/_breadcrumb.scss
theme/boost/scss/bootstrap/_button-group.scss
theme/boost/scss/bootstrap/_buttons.scss
theme/boost/scss/bootstrap/_card.scss
theme/boost/scss/bootstrap/_carousel.scss
theme/boost/scss/bootstrap/_close.scss
theme/boost/scss/bootstrap/_code.scss
theme/boost/scss/bootstrap/_custom-forms.scss
theme/boost/scss/bootstrap/_dropdown.scss
theme/boost/scss/bootstrap/_forms.scss
theme/boost/scss/bootstrap/_functions.scss
theme/boost/scss/bootstrap/_grid.scss
theme/boost/scss/bootstrap/_images.scss
theme/boost/scss/bootstrap/_input-group.scss
theme/boost/scss/bootstrap/_list-group.scss
theme/boost/scss/bootstrap/_mixins.scss
theme/boost/scss/bootstrap/_modal.scss
theme/boost/scss/bootstrap/_nav.scss
theme/boost/scss/bootstrap/_navbar.scss
theme/boost/scss/bootstrap/_pagination.scss
theme/boost/scss/bootstrap/_popover.scss
theme/boost/scss/bootstrap/_print.scss
theme/boost/scss/bootstrap/_progress.scss
theme/boost/scss/bootstrap/_reboot.scss
theme/boost/scss/bootstrap/_root.scss
theme/boost/scss/bootstrap/_spinners.scss
theme/boost/scss/bootstrap/_tables.scss
theme/boost/scss/bootstrap/_type.scss
theme/boost/scss/bootstrap/_utilities.scss
theme/boost/scss/bootstrap/_variables.scss
theme/boost/scss/bootstrap/bootstrap-grid.scss
theme/boost/scss/bootstrap/bootstrap-reboot.scss
theme/boost/scss/bootstrap/bootstrap.scss
theme/boost/scss/bootstrap/mixins/_background-variant.scss
theme/boost/scss/bootstrap/mixins/_badge.scss
theme/boost/scss/bootstrap/mixins/_border-radius.scss
theme/boost/scss/bootstrap/mixins/_buttons.scss
theme/boost/scss/bootstrap/mixins/_caret.scss
theme/boost/scss/bootstrap/mixins/_float.scss
theme/boost/scss/bootstrap/mixins/_forms.scss
theme/boost/scss/bootstrap/mixins/_grid-framework.scss
theme/boost/scss/bootstrap/mixins/_grid.scss
theme/boost/scss/bootstrap/mixins/_hover.scss
theme/boost/scss/bootstrap/mixins/_image.scss
theme/boost/scss/bootstrap/mixins/_list-group.scss
theme/boost/scss/bootstrap/mixins/_lists.scss
theme/boost/scss/bootstrap/mixins/_nav-divider.scss
theme/boost/scss/bootstrap/mixins/_reset-text.scss
theme/boost/scss/bootstrap/mixins/_screen-reader.scss
theme/boost/scss/bootstrap/mixins/_table-row.scss
theme/boost/scss/bootstrap/mixins/_text-emphasis.scss
theme/boost/scss/bootstrap/mixins/_transition.scss
theme/boost/scss/bootstrap/utilities/_background.scss
theme/boost/scss/bootstrap/utilities/_interactions.scss [new file with mode: 0644]
theme/boost/scss/bootstrap/utilities/_text.scss
theme/boost/scss/moodle/admin.scss
theme/boost/scss/moodle/blocks.scss
theme/boost/scss/moodle/buttons.scss
theme/boost/scss/moodle/calendar.scss
theme/boost/scss/moodle/contentbank.scss
theme/boost/scss/moodle/core.scss
theme/boost/scss/moodle/course.scss
theme/boost/scss/moodle/debug.scss
theme/boost/scss/moodle/drawer.scss
theme/boost/scss/moodle/filemanager.scss
theme/boost/scss/moodle/forms.scss
theme/boost/scss/moodle/grade.scss
theme/boost/scss/moodle/message.scss
theme/boost/scss/moodle/modules.scss
theme/boost/scss/moodle/question.scss
theme/boost/scss/moodle/user.scss
theme/boost/scss/preset/default.scss
theme/boost/style/moodle.css
theme/boost/templates/columns1.mustache
theme/boost/templates/columns2.mustache
theme/boost/templates/navbar.mustache
theme/boost/templates/secure.mustache
theme/boost/thirdpartylibs.xml
theme/classic/scss/classic/post.scss
theme/classic/scss/preset/default.scss
theme/classic/style/moodle.css
theme/classic/templates/columns.mustache
theme/classic/templates/contentonly.mustache
theme/classic/templates/navbar.mustache
theme/classic/templates/secure.mustache
user/amd/build/local/participantsfilter/filter.min.js [new file with mode: 0644]
user/amd/build/local/participantsfilter/filter.min.js.map [new file with mode: 0644]
user/amd/build/local/participantsfilter/filtertypes/courseid.min.js [new file with mode: 0644]
user/amd/build/local/participantsfilter/filtertypes/courseid.min.js.map [new file with mode: 0644]
user/amd/build/local/participantsfilter/filtertypes/keyword.min.js [new file with mode: 0644]
user/amd/build/local/participantsfilter/filtertypes/keyword.min.js.map [new file with mode: 0644]
user/amd/build/local/participantsfilter/selectors.min.js [new file with mode: 0644]
user/amd/build/local/participantsfilter/selectors.min.js.map [new file with mode: 0644]
user/amd/build/participants.min.js
user/amd/build/participants.min.js.map
user/amd/build/participantsfilter.min.js [new file with mode: 0644]
user/amd/build/participantsfilter.min.js.map [new file with mode: 0644]
user/amd/src/local/participantsfilter/filter.js [new file with mode: 0644]
user/amd/src/local/participantsfilter/filtertypes/courseid.js [new file with mode: 0644]
user/amd/src/local/participantsfilter/filtertypes/keyword.js [new file with mode: 0644]
user/amd/src/local/participantsfilter/selectors.js [new file with mode: 0644]
user/amd/src/participants.js
user/amd/src/participantsfilter.js [new file with mode: 0644]
user/classes/output/participants_filter.php [new file with mode: 0644]
user/classes/table/participants_search.php
user/index.php
user/lib.php
user/renderer.php
user/selector/lib.php
user/templates/local/participantsfilter/autocomplete_layout.mustache [new file with mode: 0644]
user/templates/local/participantsfilter/autocomplete_selection.mustache [new file with mode: 0644]
user/templates/local/participantsfilter/autocomplete_selection_items.mustache [new file with mode: 0644]
user/templates/local/participantsfilter/filterrow.mustache [new file with mode: 0644]
user/templates/local/participantsfilter/filtertype.mustache [new file with mode: 0644]
user/templates/local/participantsfilter/filtertypes.mustache [new file with mode: 0644]
user/templates/participantsfilter.mustache [new file with mode: 0644]
user/tests/behat/course_preference.feature
user/tests/externallib_test.php
user/tests/privacy_test.php
user/tests/profilelib_test.php
user/tests/table/participants_search_test.php
user/tests/userlib_test.php
version.php

index b9c0b6a..1bca50c 100644 (file)
@@ -74,19 +74,19 @@ media/player/videojs/videojs/video-js.swf
 mod/assign/feedback/editpdf/fpdi/
 repository/s3/S3.php
 theme/boost/scss/bootstrap/
-theme/boost/amd/src/alert.js
-theme/boost/amd/src/button.js
-theme/boost/amd/src/carousel.js
-theme/boost/amd/src/collapse.js
-theme/boost/amd/src/dropdown.js
-theme/boost/amd/src/index.js
-theme/boost/amd/src/modal.js
-theme/boost/amd/src/popover.js
-theme/boost/amd/src/sanitizer.js
-theme/boost/amd/src/scrollspy.js
-theme/boost/amd/src/tab.js
-theme/boost/amd/src/toast.js
-theme/boost/amd/src/tooltip.js
-theme/boost/amd/src/util.js
+theme/boost/amd/src/bootstrap/alert.js
+theme/boost/amd/src/bootstrap/button.js
+theme/boost/amd/src/bootstrap/carousel.js
+theme/boost/amd/src/bootstrap/collapse.js
+theme/boost/amd/src/bootstrap/dropdown.js
+theme/boost/amd/src/bootstrap/index.js
+theme/boost/amd/src/bootstrap/modal.js
+theme/boost/amd/src/bootstrap/popover.js
+theme/boost/amd/src/bootstrap/tools/sanitizer.js
+theme/boost/amd/src/bootstrap/scrollspy.js
+theme/boost/amd/src/bootstrap/tab.js
+theme/boost/amd/src/bootstrap/toast.js
+theme/boost/amd/src/bootstrap/tooltip.js
+theme/boost/amd/src/bootstrap/util.js
 theme/boost/amd/src/tether.js
 theme/boost/scss/fontawesome/
\ No newline at end of file
index 5d9e5c1..c0de22f 100644 (file)
@@ -75,19 +75,19 @@ media/player/videojs/videojs/video-js.swf
 mod/assign/feedback/editpdf/fpdi/
 repository/s3/S3.php
 theme/boost/scss/bootstrap/
-theme/boost/amd/src/alert.js
-theme/boost/amd/src/button.js
-theme/boost/amd/src/carousel.js
-theme/boost/amd/src/collapse.js
-theme/boost/amd/src/dropdown.js
-theme/boost/amd/src/index.js
-theme/boost/amd/src/modal.js
-theme/boost/amd/src/popover.js
-theme/boost/amd/src/sanitizer.js
-theme/boost/amd/src/scrollspy.js
-theme/boost/amd/src/tab.js
-theme/boost/amd/src/toast.js
-theme/boost/amd/src/tooltip.js
-theme/boost/amd/src/util.js
+theme/boost/amd/src/bootstrap/alert.js
+theme/boost/amd/src/bootstrap/button.js
+theme/boost/amd/src/bootstrap/carousel.js
+theme/boost/amd/src/bootstrap/collapse.js
+theme/boost/amd/src/bootstrap/dropdown.js
+theme/boost/amd/src/bootstrap/index.js
+theme/boost/amd/src/bootstrap/modal.js
+theme/boost/amd/src/bootstrap/popover.js
+theme/boost/amd/src/bootstrap/tools/sanitizer.js
+theme/boost/amd/src/bootstrap/scrollspy.js
+theme/boost/amd/src/bootstrap/tab.js
+theme/boost/amd/src/bootstrap/toast.js
+theme/boost/amd/src/bootstrap/tooltip.js
+theme/boost/amd/src/bootstrap/util.js
 theme/boost/amd/src/tether.js
 theme/boost/scss/fontawesome/
\ No newline at end of file
index 8936f7c..b6ba368 100644 (file)
@@ -61,12 +61,6 @@ if ($hassiteconfig or has_any_capability($capabilities, $systemcontext)) {
             array('moodle/restore:restorecourse')
         )
     );
-    $ADMIN->add('courses',
-        new admin_externalpage('activitychooser', new lang_string('activitychooserrecommendations', 'course'),
-            new moodle_url('/course/recommendations.php'),
-            array('moodle/course:recommendactivity')
-        )
-    );
 
     // Course Default Settings Page.
     // NOTE: these settings must be applied after all other settings because they depend on them.
@@ -187,6 +181,30 @@ if ($hassiteconfig or has_any_capability($capabilities, $systemcontext)) {
                 $CFG->wwwroot . '/course/pending.php', array('moodle/site:approvecourse')));
     }
 
+    // Add a category for the Activity Chooser.
+    $ADMIN->add('courses', new admin_category('activitychooser', new lang_string('activitychoosercategory', 'course')));
+    $temp = new admin_settingpage('activitychoosersettings', new lang_string('activitychoosersettings', 'course'));
+    $temp->add(
+        new admin_setting_configselect(
+            'activitychoosertabmode',
+            new lang_string('activitychoosertabmode', 'course'),
+            new lang_string('activitychoosertabmode_desc', 'course'),
+            0,
+            [
+                0 => new lang_string('activitychoosertabmodeone', 'course'),
+                1 => new lang_string('activitychoosertabmodetwo', 'course'),
+                2 => new lang_string('activitychoosertabmodethree', 'course'),
+            ]
+        )
+    );
+    $ADMIN->add('activitychooser', $temp);
+    $ADMIN->add('activitychooser',
+        new admin_externalpage('activitychooserrecommended', new lang_string('activitychooserrecommendations', 'course'),
+            new moodle_url('/course/recommendations.php'),
+            array('moodle/course:recommendactivity')
+        )
+    );
+
     // Add a category for backups.
     $ADMIN->add('courses', new admin_category('backups', new lang_string('backups','admin')));
 
diff --git a/admin/settings/license.php b/admin/settings/license.php
new file mode 100644 (file)
index 0000000..bd9ad8f
--- /dev/null
@@ -0,0 +1,45 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * This file defines the settings pages for licenses.
+ *
+ * @package    core
+ * @copyright  2020 Tom Dickman <tomdickman@catalyst-au.net>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die();
+
+require_once($CFG->libdir . '/licenselib.php');
+
+if ($hassiteconfig) {
+
+    $temp = new admin_settingpage('licensesettings', new lang_string('licensesettings', 'admin'));
+
+    $licenses = license_manager::get_active_licenses_as_array();
+
+    $temp->add(new admin_setting_configselect('sitedefaultlicense',
+        new lang_string('configsitedefaultlicense', 'admin'),
+        new lang_string('configsitedefaultlicensehelp', 'admin'),
+        'unknown',
+        $licenses));
+    $temp->add(new admin_setting_configcheckbox('rememberuserlicensepref',
+        new lang_string('rememberuserlicensepref', 'admin'),
+        new lang_string('rememberuserlicensepref_help', 'admin'),
+        1));
+    $ADMIN->add('license', $temp);
+}
index 278d4f3..4eb6ec4 100644 (file)
@@ -182,20 +182,6 @@ if ($hassiteconfig) {
         $plugin->load_settings($ADMIN, 'mlbackendsettings', $hassiteconfig);
     }
 
-/// License types
-    $ADMIN->add('modules', new admin_category('licensesettings', new lang_string('licenses')));
-    $temp = new admin_settingpage('managelicenses', new lang_string('managelicenses', 'admin'));
-
-    require_once($CFG->libdir . '/licenselib.php');
-    $licenses = array();
-    $array = explode(',', $CFG->licenses);
-    foreach ($array as $value) {
-        $licenses[$value] = new lang_string($value, 'license');
-    }
-    $temp->add(new admin_setting_configselect('sitedefaultlicense', new lang_string('configsitedefaultlicense','admin'), new lang_string('configsitedefaultlicensehelp','admin'), 'allrightsreserved', $licenses));
-    $temp->add(new admin_setting_managelicenses());
-    $ADMIN->add('licensesettings', $temp);
-
 /// Filter plugins
     $ADMIN->add('modules', new admin_category('filtersettings', new lang_string('managefilters')));
 
index 8c922d1..8717843 100644 (file)
@@ -32,6 +32,7 @@ $ADMIN->add('root', new admin_category('analytics', new lang_string('analytics',
 $ADMIN->add('root', new admin_category('competencies', new lang_string('competencies', 'core_competency')));
 $ADMIN->add('root', new admin_category('badges', new lang_string('badges'), empty($CFG->enablebadges)));
 $ADMIN->add('root', new admin_category('h5p', new lang_string('h5p', 'core_h5p')));
+$ADMIN->add('root', new admin_category('license', new lang_string('license')));
 $ADMIN->add('root', new admin_category('location', new lang_string('location','admin')));
 $ADMIN->add('root', new admin_category('language', new lang_string('language')));
 $ADMIN->add('root', new admin_category('messaging', new lang_string('messagingcategory', 'admin')));
index c6f7f0e..8384b2e 100644 (file)
@@ -12,7 +12,7 @@ Feature: Manage data categories
     And I press "Add category"
     And I set the field "Name" to "Category 1"
     And I set the field "Description" to "Category 1 description"
-    When I click on "Save" "button" in the "Delete category" "dialogue"
+    When I click on "Save" "button" in the "Add category" "dialogue"
     Then I should see "Category 1" in the "List of data categories" "table"
     And I should see "Category 1 description" in the "Category 1" "table_row"
 
@@ -30,5 +30,5 @@ Feature: Manage data categories
     And I choose "Delete" in the open action menu
     And I should see "Delete category"
     And I should see "Are you sure you want to delete the category 'Category 1'?"
-    When I click on "Delete" "button" in the "Confirm" "dialogue"
+    When I click on "Delete" "button" in the "Delete category" "dialogue"
     Then I should not see "Category 1" in the "List of data categories" "table"
index 772c590..dcd0c2a 100644 (file)
@@ -52,5 +52,5 @@ Feature: Manage data storage purposes
     And I choose "Delete" in the open action menu
     And I should see "Delete purpose"
     And I should see "Are you sure you want to delete the purpose 'Purpose 1'?"
-    When I click on "Delete" "button" in the "Confirm" "dialogue"
+    When I click on "Delete" "button" in the "Delete purpose" "dialogue"
     Then I should not see "Purpose 1" in the "List of data purposes" "table"
diff --git a/admin/tool/licensemanager/amd/build/delete_license.min.js b/admin/tool/licensemanager/amd/build/delete_license.min.js
new file mode 100644 (file)
index 0000000..17dd2af
Binary files /dev/null and b/admin/tool/licensemanager/amd/build/delete_license.min.js differ
diff --git a/admin/tool/licensemanager/amd/build/delete_license.min.js.map b/admin/tool/licensemanager/amd/build/delete_license.min.js.map
new file mode 100644 (file)
index 0000000..caee9ae
Binary files /dev/null and b/admin/tool/licensemanager/amd/build/delete_license.min.js.map differ
diff --git a/admin/tool/licensemanager/amd/src/delete_license.js b/admin/tool/licensemanager/amd/src/delete_license.js
new file mode 100644 (file)
index 0000000..6abdcec
--- /dev/null
@@ -0,0 +1,51 @@
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * Modal for confirming deletion of a custom license.
+ *
+ * @module     tool_licensemanager/delete_license
+ * @class      delete_license
+ * @package    tool_licensemanager
+ * @copyright  2019 Tom Dickman <tomdickman@catalyst-au.net>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+define(['jquery', 'core/modal_factory', 'core/modal_events', 'core/url', 'core/str'],
+    function($, ModalFactory, ModalEvents, Url, String) {
+
+        var trigger = $('.delete-license');
+        ModalFactory.create({
+            type: ModalFactory.types.SAVE_CANCEL,
+            title: String.get_string('deletelicense', 'tool_licensemanager'),
+            body: String.get_string('deletelicenseconfirmmessage', 'tool_licensemanager'),
+            preShowCallback: function(triggerElement, modal) {
+                triggerElement = $(triggerElement);
+                let params = {
+                    'action': 'delete',
+                    'license': triggerElement.data('license')
+                };
+                modal.deleteURL = Url.relativeUrl('/admin/tool/licensemanager/index.php', params, true);
+            },
+            large: true,
+        }, trigger)
+            .done(function(modal) {
+                modal.getRoot().on(ModalEvents.save, function(e) {
+                    // Stop the default save button behaviour which is to close the modal.
+                    e.preventDefault();
+                    // Redirect to delete url.
+                    window.location.href = modal.deleteURL;
+                });
+            });
+    });
diff --git a/admin/tool/licensemanager/classes/form/edit_license.php b/admin/tool/licensemanager/classes/form/edit_license.php
new file mode 100644 (file)
index 0000000..624ce80
--- /dev/null
@@ -0,0 +1,124 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * Form for creating/updating a custom license.
+ *
+ * @package    tool_licensemanager
+ * @copyright  2019 Tom Dickman <tom.dickman@catalyst-au.net>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace tool_licensemanager\form;
+
+use moodleform;
+use tool_licensemanager\helper;
+use tool_licensemanager\manager;
+
+defined('MOODLE_INTERNAL') || die('Direct access to this script is forbidden.');
+
+global $CFG;
+require_once($CFG->libdir . '/formslib.php');
+
+/**
+ * Form for creating/updating a custom license.
+ *
+ * @package    tool_licensemanager
+ * @copyright  2019 Tom Dickman <tom.dickman@catalyst-au.net>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class edit_license extends moodleform {
+
+    /**
+     * @var string the action form is taking.
+     */
+    private $action;
+
+    /**
+     * @var string license shortname if editing or empty string if creating license.
+     */
+    private $licenseshortname;
+
+    /**
+     * edit_license constructor.
+     *
+     * @param string $action the license_manager action to be taken by form.
+     * @param string $licenseshortname the shortname of the license to edit.
+     */
+    public function __construct(string $action, string $licenseshortname) {
+        $this->action = $action;
+        $this->licenseshortname = $licenseshortname;
+
+        if ($action == manager::ACTION_UPDATE && !empty($licenseshortname)) {
+            parent::__construct(helper::get_update_license_url($licenseshortname));
+        } else {
+            parent::__construct(helper::get_create_license_url());
+        }
+    }
+
+    /**
+     * Form definition for creation and editing of licenses.
+     */
+    public function definition() {
+
+        $mform = $this->_form;
+
+        $mform->addElement('text', 'shortname', get_string('shortname', 'tool_licensemanager'));
+        $mform->setType('shortname', PARAM_ALPHANUMEXT);
+        // Shortname is only editable when user is creating a license.
+        if ($this->action != manager::ACTION_CREATE) {
+            $mform->freeze('shortname');
+        } else {
+            $mform->addRule('shortname', get_string('shortnamerequirederror', 'tool_licensemanager'), 'required');
+        }
+
+        $mform->addElement('text', 'fullname', get_string('fullname', 'tool_licensemanager'));
+        $mform->setType('fullname', PARAM_TEXT);
+        $mform->addRule('fullname', get_string('fullnamerequirederror', 'tool_licensemanager'), 'required');
+
+        $mform->addElement('text', 'source', get_string('source', 'tool_licensemanager'));
+        $mform->setType('source', PARAM_URL);
+        $mform->addHelpButton('source', 'source', 'tool_licensemanager');
+        $mform->addRule('source', get_string('sourcerequirederror', 'tool_licensemanager'), 'required');
+
+        $mform->addElement('date_selector', 'version', get_string('version', 'tool_licensemanager'), get_string('from'));
+        $mform->addHelpButton('version', 'version', 'tool_licensemanager');
+
+        $this->add_action_buttons();
+    }
+
+    /**
+     * Validate form data and return errors (if any).
+     *
+     * @param array $data array of ("fieldname"=>value) of submitted data
+     * @param array $files array of uploaded files "element_name"=>tmp_file_path
+     * @return array of "element_name"=>"error_description" if there are errors,
+     *         or an empty array if everything is OK (true allowed for backwards compatibility too).
+     */
+    public function validation($data, $files) {
+        $errors = parent::validation($data, $files);
+
+        if (array_key_exists('source', $data)  && !filter_var($data['source'], FILTER_VALIDATE_URL)) {
+            $errors['source'] = get_string('invalidurl', 'tool_licensemanager');
+        }
+
+        if (array_key_exists('version', $data) && $data['version'] > time()) {
+            $errors['version'] = get_string('versioncannotbefuture', 'tool_licensemanager');
+        }
+
+        return $errors;
+    }
+}
diff --git a/admin/tool/licensemanager/classes/helper.php b/admin/tool/licensemanager/classes/helper.php
new file mode 100644 (file)
index 0000000..b71a9aa
--- /dev/null
@@ -0,0 +1,154 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * License manager helper class.
+ *
+ * @package    tool_licensemanager
+ * @copyright  2019 Tom Dickman <tomdickman@catalyst-au.net>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace tool_licensemanager;
+
+use moodle_url;
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * License manager helper class.
+ *
+ * @package    tool_licensemanager
+ * @copyright  2019 Tom Dickman <tomdickman@catalyst-au.net>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class helper {
+
+    /**
+     * Moodle relative path to the licenses manager.
+     */
+    const MANAGER_PATH = '/admin/tool/licensemanager/index.php';
+
+    /**
+     * Get the URL for viewing the license manager interface.
+     *
+     * @return \moodle_url
+     */
+    public static function get_licensemanager_url() : moodle_url {
+        global $CFG;
+
+        $url = new moodle_url($CFG->wwwroot . self::MANAGER_PATH,
+            ['sesskey' => sesskey()]);
+
+        return $url;
+    }
+
+    /**
+     * Get the URL for endpoint enabling a license.
+     *
+     * @param string $licenseshortname the shortname of license to enable.
+     *
+     * @return \moodle_url
+     */
+    public static function get_enable_license_url(string $licenseshortname) : moodle_url {
+        $url = new moodle_url(self::MANAGER_PATH,
+            ['action' => manager::ACTION_ENABLE, 'license' => $licenseshortname, 'sesskey' => sesskey()]);
+
+        return $url;
+    }
+
+    /**
+     * Get the URL for endpoint disabling a license.
+     *
+     * @param string $licenseshortname the shortname of license to disable.
+     *
+     * @return \moodle_url
+     */
+    public static function get_disable_license_url(string $licenseshortname) : moodle_url {
+        $url = new moodle_url(self::MANAGER_PATH,
+            ['action' => manager::ACTION_DISABLE, 'license' => $licenseshortname, 'sesskey' => sesskey()]);
+
+        return $url;
+    }
+
+    /**
+     * Get the URL endpoint to create a new license.
+     *
+     * @return \moodle_url
+     */
+    public static function get_create_license_url() : moodle_url {
+        $url = new moodle_url(self::MANAGER_PATH,
+            ['action' => manager::ACTION_CREATE, 'sesskey' => sesskey()]);
+
+        return $url;
+    }
+
+    /**
+     * Get the URL endpoint to update an existing license.
+     *
+     * @param string $licenseshortname the shortname of license to update.
+     *
+     * @return \moodle_url
+     */
+    public static function get_update_license_url(string $licenseshortname) : moodle_url {
+        $url = new moodle_url(self::MANAGER_PATH,
+            ['action' => manager::ACTION_UPDATE, 'license' => $licenseshortname, 'sesskey' => sesskey()]);
+
+        return $url;
+    }
+
+    /**
+     * Get the URL endpoint to move a license up order.
+     *
+     * @param string $licenseshortname the shortname of license to move up.
+     *
+     * @return \moodle_url
+     */
+    public static function get_moveup_license_url(string $licenseshortname) : moodle_url {
+        $url = new moodle_url(self::MANAGER_PATH,
+            ['action' => manager::ACTION_MOVE_UP, 'license' => $licenseshortname, 'sesskey' => sesskey()]);
+
+        return $url;
+    }
+
+    /**
+     * Get the URL endpoint to move a license down order.
+     *
+     * @param string $licenseshortname the shortname of license to move down.
+     *
+     * @return \moodle_url
+     */
+    public static function get_movedown_license_url(string $licenseshortname) : moodle_url {
+        $url = new moodle_url(self::MANAGER_PATH,
+            ['action' => manager::ACTION_MOVE_DOWN, 'license' => $licenseshortname, 'sesskey' => sesskey()]);
+
+        return $url;
+    }
+
+    /**
+     * Convert a license version number string to a UNIX epoch.
+     *
+     * @param string $version
+     *
+     * @return int $epoch
+     */
+    public static function convert_version_to_epoch(string $version) : int {
+        $date = substr($version, 0, 8);
+        $epoch = strtotime($date);
+
+        return $epoch;
+    }
+}
diff --git a/admin/tool/licensemanager/classes/manager.php b/admin/tool/licensemanager/classes/manager.php
new file mode 100644 (file)
index 0000000..2f3ad2c
--- /dev/null
@@ -0,0 +1,247 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * License manager.
+ *
+ * @package    tool_licensemanager
+ * @copyright  2019 Tom Dickman <tomdickman@catalyst-au.net>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace tool_licensemanager;
+
+use tool_licensemanager\form\edit_license;
+use license_manager;
+use stdClass;
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * License manager, main controller for tool_licensemanager.
+ *
+ * @package    tool_licensemanager
+ * @copyright  2019 Tom Dickman <tomdickman@catalyst-au.net>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class manager {
+
+    /**
+     * Action for creating a new custom license.
+     */
+    const ACTION_CREATE = 'create';
+
+    /**
+     * Action for updating a custom license's details.
+     */
+    const ACTION_UPDATE = 'update';
+
+    /**
+     * Action for deleting a custom license.
+     */
+    const ACTION_DELETE = 'delete';
+
+    /**
+     * Action for disabling a custom license.
+     */
+    const ACTION_DISABLE = 'disable';
+
+    /**
+     * Action for enabling a custom license.
+     */
+    const ACTION_ENABLE = 'enable';
+
+    /**
+     * Action for displaying the license list view.
+     */
+    const ACTION_VIEW_LICENSE_MANAGER = 'viewlicensemanager';
+
+    /**
+     * Action for moving a license up order.
+     */
+    const ACTION_MOVE_UP = 'moveup';
+
+    /**
+     * Action for moving a license down order.
+     */
+    const ACTION_MOVE_DOWN = 'movedown';
+
+    /**
+     * Entry point for internal license manager.
+     *
+     * @param string $action the api action to carry out.
+     * @param string|object $license the license object or shortname of license to carry action out on.
+     */
+    public function execute(string $action, $license) : void {
+
+        admin_externalpage_setup('licensemanager');
+
+        // Convert license to a string if it's a full license object.
+        if (is_object($license)) {
+            $license = $license->shortname;
+        }
+
+        $viewmanager = true;
+
+        switch ($action) {
+            case self::ACTION_DISABLE:
+                license_manager::disable($license);
+                break;
+
+            case self::ACTION_ENABLE:
+                license_manager::enable($license);
+                break;
+
+            case self::ACTION_DELETE:
+                license_manager::delete($license);
+                break;
+
+            case self::ACTION_CREATE:
+            case self::ACTION_UPDATE:
+                $viewmanager = $this->edit($action, $license);
+                break;
+
+            case self::ACTION_MOVE_UP:
+            case self::ACTION_MOVE_DOWN:
+                $this->change_license_order($action, $license);
+                break;
+
+            case self::ACTION_VIEW_LICENSE_MANAGER:
+            default:
+                break;
+        }
+        if ($viewmanager) {
+            $this->view_license_manager();
+        }
+    }
+
+    /**
+     * Edit an existing license or create a new license.
+     *
+     * @param string $action the form action to carry out.
+     * @param string $licenseshortname the shortname of the license to edit.
+     *
+     * @return bool true if license editing complete, false otherwise.
+     */
+    private function edit(string $action, string $licenseshortname) : bool {
+
+        if ($action != self::ACTION_CREATE && $action != self::ACTION_UPDATE) {
+            throw new \coding_exception('license edit actions are limited to create and update');
+        }
+
+        $form = new form\edit_license($action, $licenseshortname);
+
+        if ($form->is_cancelled()) {
+            return true;
+        } else if ($data = $form->get_data()) {
+
+            $license = new stdClass();
+            if ($action == self::ACTION_CREATE) {
+                // Check that license shortname isn't already in use.
+                if (!empty(license_manager::get_license_by_shortname($data->shortname))) {
+                    print_error('duplicatelicenseshortname', 'tool_licensemanager',
+                        helper::get_licensemanager_url(),
+                        $data->shortname);
+                }
+                $license->shortname = $data->shortname;
+            } else {
+                if (empty(license_manager::get_license_by_shortname($licenseshortname))) {
+                    print_error('licensenotfoundshortname', 'license',
+                        helper::get_licensemanager_url(),
+                        $licenseshortname);
+                }
+                $license->shortname = $licenseshortname;
+            }
+            $license->fullname = $data->fullname;
+            $license->source = $data->source;
+            // Legacy date format maintained to prevent breaking on upgrade.
+            $license->version = date('Ymd', $data->version) . '00';
+
+            license_manager::save($license);
+
+            return true;
+        } else {
+            $this->view_license_editor($action, $licenseshortname, $form);
+
+            return false;
+        }
+    }
+
+    /**
+     * Change license order by moving up or down license order.
+     *
+     * @param string $direction which direction to move, up or down.
+     * @param string $licenseshortname the shortname of the license to move up or down order.
+     */
+    private function change_license_order(string $direction, string $licenseshortname) : void {
+
+        if (!empty($licenseshortname)) {
+            if ($direction == self::ACTION_MOVE_UP) {
+                license_manager::change_license_sortorder(license_manager::LICENSE_MOVE_UP, $licenseshortname);
+            } else if ($direction == self::ACTION_MOVE_DOWN) {
+                license_manager::change_license_sortorder(license_manager::LICENSE_MOVE_DOWN, $licenseshortname);
+            }
+        }
+    }
+
+    /**
+     * View the license editor to create or edit a license.
+     *
+     * @param string $action
+     * @param string $licenseshortname the shortname of the license to create/edit.
+     * @param \tool_licensemanager\form\edit_license $form the form for submitting edit data.
+     */
+    private function view_license_editor(string $action, string $licenseshortname, edit_license $form) : void {
+        global $PAGE;
+
+        $renderer = $PAGE->get_renderer('tool_licensemanager');
+
+        if ($action == self::ACTION_UPDATE && $license = license_manager::get_license_by_shortname($licenseshortname)) {
+            $return = $renderer->render_edit_licence_headers($licenseshortname);
+
+            $form->set_data(['shortname' => $license->shortname]);
+            $form->set_data(['fullname' => $license->fullname]);
+            $form->set_data(['source' => $license->source]);
+            $form->set_data(['version' => helper::convert_version_to_epoch($license->version)]);
+
+        } else {
+            $return = $renderer->render_create_licence_headers();
+        }
+        $return .= $form->render();
+        $return .= $renderer->footer();
+
+        echo $return;
+    }
+
+    /**
+     * View the license manager.
+     */
+    private function view_license_manager() : void {
+        global $PAGE;
+
+        $PAGE->requires->js_call_amd('tool_licensemanager/delete_license');
+
+        $renderer = $PAGE->get_renderer('tool_licensemanager');
+        $html = $renderer->header();
+        $html .= $renderer->heading(get_string('licensemanager', 'tool_licensemanager'));
+
+        $table = new \tool_licensemanager\output\table();
+        $html .= $renderer->render($table);
+        $html .= $renderer->footer();
+
+        echo $html;
+    }
+}
diff --git a/admin/tool/licensemanager/classes/output/renderer.php b/admin/tool/licensemanager/classes/output/renderer.php
new file mode 100644 (file)
index 0000000..19ac8d4
--- /dev/null
@@ -0,0 +1,96 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * Renderer for 'tool_licensemanager' component.
+ *
+ * @package    tool_licensemanager
+ * @copyright  Tom Dickman <tomdickman@catalyst-au.net>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace tool_licensemanager\output;
+
+defined('MOODLE_INTERNAL') || die();
+
+use license_manager;
+use plugin_renderer_base;
+use tool_licensemanager\helper;
+
+/**
+ * Renderer class for 'tool_licensemanager' component.
+ *
+ * @package    tool_licensemanager
+ * @copyright  2019 Tom Dickman <tomdickman@catalyst-au.net>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class renderer extends plugin_renderer_base {
+
+    /**
+     * Render the headers for create license form.
+     *
+     * @return string html fragment for display.
+     */
+    public function render_create_licence_headers() : string {
+
+        $this->page->navbar->add(get_string('createlicense', 'tool_licensemanager'),
+            helper::get_create_license_url());
+
+        $return = $this->header();
+        $return .= $this->heading(get_string('createlicense', 'tool_licensemanager'));
+
+        return $return;
+    }
+
+    /**
+     * Render the headers for edit license form.
+     *
+     * @param string $licenseshortname the shortname of license to edit.
+     *
+     * @return string html fragment for display.
+     */
+    public function render_edit_licence_headers(string $licenseshortname) : string {
+
+        $this->page->navbar->add(get_string('editlicense', 'tool_licensemanager'),
+            helper::get_update_license_url($licenseshortname));
+
+        $return = $this->header();
+        $return .= $this->heading(get_string('editlicense', 'tool_licensemanager'));
+
+        return $return;
+    }
+
+    /**
+     * Render the license manager table.
+     *
+     * @param \renderable $table the renderable.
+     *
+     * @return string HTML.
+     */
+    public function render_table(\renderable $table) {
+        $licenses = license_manager::get_licenses();
+
+        // Add the create license button.
+        $html = $table->create_license_link();
+
+        // Add the table containing licenses for management.
+        $html .= $this->box_start('generalbox editorsui');
+        $html .= $table->create_license_manager_table($licenses, $this);
+        $html .= $this->box_end();
+
+        return $html;
+    }
+}
diff --git a/admin/tool/licensemanager/classes/output/table.php b/admin/tool/licensemanager/classes/output/table.php
new file mode 100644 (file)
index 0000000..17144fc
--- /dev/null
@@ -0,0 +1,185 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * Renderable for display of license manager table.
+ *
+ * @package   tool_licensemanager
+ * @copyright 2020 Tom Dickman <tomdickman@catalyst-au.net>
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+namespace tool_licensemanager\output;
+
+use html_table;
+use html_table_cell;
+use html_table_row;
+use html_writer;
+use license_manager;
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * Renderable for display of license manager table.
+ *
+ * @package   tool_licensemanager
+ * @copyright 2020 Tom Dickman <tomdickman@catalyst-au.net>
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class table implements \renderable {
+
+    /**
+     * 'Create License' link.
+     *
+     * @return string HTML string.
+     */
+    public function create_license_link() {
+        $link = html_writer::link(\tool_licensemanager\helper::get_create_license_url(),
+            get_string('createlicensebuttontext', 'tool_licensemanager'),
+            ['class' => 'btn btn-secondary mb-3']);
+
+        return $link;
+    }
+
+    /**
+     * Create the HTML table for license management.
+     *
+     * @param array $licenses
+     * @param \renderer_base $output
+     *
+     * @return string HTML for license manager table.
+     */
+    public function create_license_manager_table(array $licenses, \renderer_base $output) {
+        $table = new html_table();
+        $table->head  = [
+            get_string('enable'),
+            get_string('license', 'tool_licensemanager'),
+            get_string('version'),
+            get_string('order'),
+            get_string('edit'),
+            get_string('delete'),
+        ];
+        $table->colclasses = [
+            'text-center',
+            'text-left',
+            'text-left',
+            'text-center',
+            'text-center',
+            'text-center',
+        ];
+        $table->id = 'manage-licenses';
+        $table->attributes['class'] = 'admintable generaltable';
+        $table->data  = [];
+
+        $rownumber = 0;
+        $rowcount = count($licenses);
+
+        foreach ($licenses as $key => $value) {
+            $canmoveup = $rownumber > 0;
+            $canmovedown = $rownumber < $rowcount - 1;
+            $table->data[] = $this->get_license_table_row_data($value, $canmoveup, $canmovedown, $output);
+            $rownumber++;
+        }
+
+        $html = html_writer::table($table);
+
+        return $html;
+    }
+
+    /**
+     * Get table row data for a license.
+     *
+     * @param object $license the license to populate row data for.
+     * @param bool $canmoveup can this row move up.
+     * @param bool $canmovedown can this row move down.
+     * @param \renderer_base $output the renderer
+     *
+     * @return \html_table_row of columns values for row.
+     */
+    protected function get_license_table_row_data($license, bool $canmoveup, bool $canmovedown, \renderer_base $output) {
+        global $CFG;
+
+        $summary = $license->fullname . ' ('. $license->shortname . ')';
+        if (!empty($license->source)) {
+            $summary .= html_writer::empty_tag('br');
+            $summary .= html_writer::link($license->source, $license->source, ['target' => '_blank']);
+        }
+        $summarycell = new html_table_cell($summary);
+        $summarycell->attributes['class'] = 'license-summary';
+        $versioncell = new html_table_cell($license->version);
+        $versioncell->attributes['class'] = 'license-version';
+
+        $deletelicense = '';
+        if ($license->shortname == $CFG->sitedefaultlicense) {
+            $hideshow = $output->pix_icon('t/locked', get_string('sitedefaultlicenselock', 'tool_licensemanager'));
+        } else {
+            if ($license->enabled == license_manager::LICENSE_ENABLED) {
+                $hideshow = html_writer::link(\tool_licensemanager\helper::get_disable_license_url($license->shortname),
+                    $output->pix_icon('t/hide', get_string('disablelicensename', 'tool_licensemanager', $license->fullname)));
+            } else {
+                $hideshow = html_writer::link(\tool_licensemanager\helper::get_enable_license_url($license->shortname),
+                    $output->pix_icon('t/show', get_string('enablelicensename', 'tool_licensemanager', $license->fullname)));
+            }
+
+            if ($license->custom == license_manager::CUSTOM_LICENSE) {
+                // Link url is added by the JS `delete_license` modal used for confirmation of deletion, to avoid
+                // link being usable before JavaScript loads on page.
+                $deletelicense = html_writer::link('#', $output->pix_icon('i/trash',
+                    get_string('deletelicensename', 'tool_licensemanager', $license->fullname)),
+                    ['class' => 'delete-license', 'data-license' => $license->shortname]);
+            }
+        }
+        $hideshowcell = new html_table_cell($hideshow);
+        $hideshowcell->attributes['class'] = 'license-status';
+
+        if ($license->custom == license_manager::CUSTOM_LICENSE) {
+            $editlicense = html_writer::link(\tool_licensemanager\helper::get_update_license_url($license->shortname),
+                $output->pix_icon('t/editinline', get_string('editlicensename', 'tool_licensemanager', $license->fullname)),
+                ['class' => 'edit-license']);
+        } else {
+            $editlicense = '';
+        }
+        $editlicensecell = new html_table_cell($editlicense);
+        $editlicensecell->attributes['class'] = 'edit-license';
+
+        $spacer = $output->pix_icon('spacer', '', 'moodle', ['class' => 'iconsmall']);
+        $updown = '';
+        if ($canmoveup) {
+            $updown .= html_writer::link(\tool_licensemanager\helper::get_moveup_license_url($license->shortname),
+                    $output->pix_icon('t/up', get_string('movelicenseupname', 'tool_licensemanager', $license->fullname),
+                        'moodle', ['class' => 'iconsmall']),
+                    ['class' => 'move-up']) . '';
+        } else {
+            $updown .= $spacer;
+        }
+
+        if ($canmovedown) {
+            $updown .= '&nbsp;'.html_writer::link(\tool_licensemanager\helper::get_movedown_license_url($license->shortname),
+                    $output->pix_icon('t/down', get_string('movelicensedownname', 'tool_licensemanager', $license->fullname),
+                        'moodle', ['class' => 'iconsmall']),
+                    ['class' => 'move-down']);
+        } else {
+            $updown .= $spacer;
+        }
+        $updowncell = new html_table_cell($updown);
+        $updowncell->attributes['class'] = 'license-order';
+
+        $row = new html_table_row([$hideshowcell, $summarycell, $versioncell, $updowncell, $editlicensecell, $deletelicense]);
+        $row->attributes['data-license'] = $license->shortname;
+        $row->attributes['class'] = strtolower(get_string('license', 'tool_licensemanager'));
+
+        return $row;
+    }
+}
diff --git a/admin/tool/licensemanager/classes/privacy/provider.php b/admin/tool/licensemanager/classes/privacy/provider.php
new file mode 100644 (file)
index 0000000..b4eb611
--- /dev/null
@@ -0,0 +1,47 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * Privacy Subsystem implementation for tool_licensemanager implementing null_provider.
+ *
+ * @package    tool_licensemanager
+ * @copyright  2019 Tom Dickman <tomdickman@catalyst-au.net>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace tool_licensemanager\privacy;
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * Privacy Subsystem implementation for tool_licensemanager implementing null_provider.
+ *
+ * @package    tool_licensemanager
+ * @copyright  2019 Tom Dickman <tomdickman@catalyst-au.net>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class provider implements \core_privacy\local\metadata\null_provider {
+
+    /**
+     * Get the language string identifier with the component's language
+     * file to explain why this plugin stores no data.
+     *
+     * @return  string
+     */
+    public static function get_reason() : string {
+        return 'privacy:metadata';
+    }
+}
similarity index 55%
rename from admin/licenses.php
rename to admin/tool/licensemanager/index.php
index 820e775..123dd9d 100644 (file)
@@ -1,5 +1,4 @@
 <?php
-
 // This file is part of Moodle - http://moodle.org/
 //
 // Moodle is free software: you can redistribute it and/or modify
 // along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
 
 /**
- * Allows admin to configure licenses.
+ * License manager page.
+ *
+ * @package   tool_licensemanager
+ * @copyright 2019 Tom Dickman <tomdickman@catalyst-au.net>
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  */
 
-require_once('../config.php');
-require_once($CFG->libdir.'/adminlib.php');
-require_once($CFG->libdir.'/licenselib.php');
+require_once('../../../config.php');
+require_once($CFG->libdir . '/adminlib.php');
+require_once($CFG->libdir . '/licenselib.php');
 
 require_admin();
 
-$returnurl = "$CFG->wwwroot/$CFG->admin/settings.php?section=managelicenses";
+$returnurl = \tool_licensemanager\helper::get_licensemanager_url();
 
 $action = optional_param('action', '', PARAM_ALPHANUMEXT);
 $license = optional_param('license', '', PARAM_SAFEDIR);
 
-////////////////////////////////////////////////////////////////////////////////
-// process actions
-
 if (!confirm_sesskey()) {
     redirect($returnurl);
 }
 
-$return = true;
-switch ($action) {
-    case 'disable':
-        license_manager::disable($license);
-        break;
-
-    case 'enable':
-        license_manager::enable($license);
-        break;
+// Route via the manager.
+$licensemanager = new \tool_licensemanager\manager();
+$PAGE->set_context(context_system::instance());
+$PAGE->set_url(\tool_licensemanager\helper::get_licensemanager_url());
+$PAGE->set_title(get_string('licensemanager', 'tool_licensemanager'));
 
-    default:
-        break;
-}
-
-if ($return) {
-    redirect ($returnurl);
-}
+$licensemanager->execute($action, $license);
diff --git a/admin/tool/licensemanager/lang/en/tool_licensemanager.php b/admin/tool/licensemanager/lang/en/tool_licensemanager.php
new file mode 100644 (file)
index 0000000..97e372a
--- /dev/null
@@ -0,0 +1,52 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * Strings for component 'tool_licensemanager', language 'en'
+ *
+ * @package   tool_licensemanager
+ * @copyright 2019 Tom Dickman <tomdickman@catalyst-au.net>
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+$string['pluginname'] = 'License manager';
+$string['createlicense'] = 'Create custom licence';
+$string['createlicensebuttontext'] = 'Create licence';
+$string['deletelicense'] = 'Delete licence';
+$string['deletelicenseconfirmmessage'] = 'Are you sure you want to delete this licence?';
+$string['deletelicensename'] = 'Delete license \'{$a}\'';
+$string['disablelicensename'] = 'Disable licence \'{$a}\'';
+$string['duplicatelicenseshortname'] = 'Licence shortname must be unique, duplicate value found.';
+$string['editlicense'] = 'Edit licence';
+$string['editlicensename'] = 'Edit licence \'{$a}\'';
+$string['enablelicensename'] = 'Enable licence \'{$a}\'';
+$string['fullname'] = 'Licence full name';
+$string['fullnamerequirederror'] = 'You must enter a full name for the licence.';
+$string['invalidurl'] = 'Invalid source URL';
+$string['license'] = 'Licence';
+$string['licensemanager'] = 'Licence manager';
+$string['movelicensedownname'] = 'Move \'{$a}\' license down order';
+$string['movelicenseupname'] = 'Move \'{$a}\' license up order';
+$string['privacy:metadata'] = 'The tool_licensemanager plugin stores no personal data.';
+$string['shortname'] = 'Licence short name';
+$string['sitedefaultlicenselock'] = 'This is the site default license. It cannot be disabled.';
+$string['shortnamerequirederror'] = 'You must enter a short name for the licence.';
+$string['source'] = 'Licence source';
+$string['source_help'] = 'The URL (with http:// or https:// prefix) where the licence terms and conditions can be found.';
+$string['sourcerequirederror'] = 'You must enter a valid URL for licence source.';
+$string['version'] = 'Licence version';
+$string['versioncannotbefuture'] = 'Licence version cannot be set to a future date.';
+$string['version_help'] = 'Publication date of the licence version being utilised.';
+
diff --git a/admin/tool/licensemanager/settings.php b/admin/tool/licensemanager/settings.php
new file mode 100644 (file)
index 0000000..ffb61e7
--- /dev/null
@@ -0,0 +1,33 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * Settings page.
+ *
+ * @package   tool_licensemanager
+ * @copyright 2020 Tom Dickman <tomdickman@catalyst-au.net>
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die;
+
+if ($hassiteconfig) {
+    $temp = new admin_externalpage('licensemanager',
+        get_string('licensemanager', 'tool_licensemanager'),
+        \tool_licensemanager\helper::get_licensemanager_url());
+
+    $ADMIN->add('license', $temp);
+}
diff --git a/admin/tool/licensemanager/tests/behat/delete_license.feature b/admin/tool/licensemanager/tests/behat/delete_license.feature
new file mode 100644 (file)
index 0000000..9bab27b
--- /dev/null
@@ -0,0 +1,28 @@
+@tool @tool_licensemanager
+Feature: Delete custom licenses
+  In order to manage custom licenses
+  As an admin
+  I need to be able to delete custom licenses but not standard Moodle licenses
+
+  @javascript
+  Scenario: I can delete a custom license
+    Given I log in as "admin"
+    And I navigate to "Licence > Licence manager" in site administration
+    And I click on "Create licence" "link"
+    And I set the following fields to these values:
+    | shortname      | MIT                                 |
+    | fullname       | MIT Licence                         |
+    | source         | https://opensource.org/licenses/MIT |
+    | version[day]   | 1                                   |
+    | version[month] | March                               |
+    | version[year]  | 2019                                |
+    And I press "Save changes"
+    And I click on "Delete" "icon" in the "MIT" "table_row"
+    When I click on "Save changes" "button" in the "Delete licence" "dialogue"
+    Then I should not see "MIT Licence" in the "manage-licenses" "table"
+
+  Scenario: I cannot delete a standard license
+    Given I log in as "admin"
+    And I navigate to "Licence > Licence manager" in site administration
+    Then I should see "Licence not specified" in the "unknown" "table_row"
+    And I should not see "Delete" in the "unknown" "table_row"
diff --git a/admin/tool/licensemanager/tests/behat/edit_license.feature b/admin/tool/licensemanager/tests/behat/edit_license.feature
new file mode 100644 (file)
index 0000000..d4d73ff
--- /dev/null
@@ -0,0 +1,79 @@
+@tool @tool_licensemanager
+Feature: Custom licences
+  In order to use custom licences
+  As an admin
+  I need to be able to add custom licences
+
+  Scenario: I am able to create custom licences
+    Given I log in as "admin"
+    And I navigate to "Licence > Licence manager" in site administration
+    And I click on "Create licence" "link"
+    And I set the following fields to these values:
+      | shortname      | MIT                                 |
+      | fullname       | MIT Licence                         |
+      | source         | https://opensource.org/licenses/MIT |
+      | version[day]   | 1                                   |
+      | version[month] | January                             |
+      | version[year]  | 2020                                |
+    When I press "Save changes"
+    Then I should see "Licence manager"
+    And I should see "MIT Licence" in the "MIT" "table_row"
+    And I should see "https://opensource.org/licenses/MIT" in the "MIT" "table_row"
+
+  Scenario: I am only be able to make custom license with a valid url source (including scheme).
+    Given I log in as "admin"
+    And I navigate to "Licence > Licence manager" in site administration
+    And I click on "Create licence" "link"
+    And I set the following fields to these values:
+      | shortname      | MIT                                 |
+      | fullname       | MIT Licence                         |
+      | source         | opensource.org/licenses/MIT         |
+      | version[day]   | 1                                   |
+      | version[month] | January                             |
+      | version[year]  | 2020                                |
+    When I press "Save changes"
+    Then I should see "Invalid source URL"
+    And I set the following fields to these values:
+      | source         | mailto:tomdickman@catalyst-au.net   |
+    And I press "Save changes"
+    And I should see "Invalid source URL"
+    And I set the following fields to these values:
+      | source         | https://opensource.org/licenses/MIT |
+    And I press "Save changes"
+    And I should see "Licence manager"
+    And I should see "MIT Licence" in the "MIT" "table_row"
+    And I should see "https://opensource.org/licenses/MIT" in the "MIT" "table_row"
+
+  Scenario: Custom license version format must be YYYYMMDD00
+    Given I log in as "admin"
+    And I navigate to "Licence > Licence manager" in site administration
+    And I click on "Create licence" "link"
+    And I set the following fields to these values:
+      | shortname      | MIT                                 |
+      | fullname       | MIT Licence                         |
+      | source         | https://opensource.org/licenses/MIT |
+      | version[day]   | 1                                   |
+      | version[month] | March                               |
+      | version[year]  | 2019                                |
+    When I press "Save changes"
+    Then I should see "Licence manager"
+    And I should see "2019030100" in the "MIT" "table_row"
+
+  @javascript
+  Scenario: Custom license short name should not be editable after first creation
+    Given I log in as "admin"
+    And I navigate to "Licence > Licence manager" in site administration
+    And I click on "Create licence" "link"
+    And I set the following fields to these values:
+      | shortname      | MIT                                 |
+      | fullname       | MIT Licence                         |
+      | source         | https://opensource.org/licenses/MIT |
+      | version[day]   | 1                                   |
+      | version[month] | March                               |
+      | version[year]  | 2019                                |
+    And I press "Save changes"
+    And I should see "Licence manager"
+    And I should see "MIT Licence" in the "MIT" "table_row"
+    When I click on "Edit" "icon" in the "MIT" "table_row"
+    Then I should see "Edit licence"
+    And the "shortname" "field" should be disabled
diff --git a/admin/tool/licensemanager/tests/behat/license_manager.feature b/admin/tool/licensemanager/tests/behat/license_manager.feature
new file mode 100644 (file)
index 0000000..2977d12
--- /dev/null
@@ -0,0 +1,35 @@
+@tool @tool_licensemanager
+Feature: License manager
+  In order to manage licenses
+  As an admin
+  I need to be able to view and alter licence preferences in the license manager.
+
+  Scenario: I should be able to see the default Moodle licences.
+    Given I log in as "admin"
+    When I navigate to "Licence > Licence manager" in site administration
+    Then I should see "Licence not specified" in the "unknown" "table_row"
+    And I should see "All rights reserved" in the "allrightsreserved" "table_row"
+    And I should see "Public domain" in the "public" "table_row"
+    And I should see "Creative Commons" in the "cc" "table_row"
+    And I should see "Creative Commons - NoDerivs" in the "cc-nd" "table_row"
+    And I should see "Creative Commons - No Commercial NoDerivs" in the "cc-nc-nd" "table_row"
+    And I should see "Creative Commons - No Commercial" in the "cc-nc" "table_row"
+    And I should see "Creative Commons - No Commercial ShareAlike" in the "cc-nc-sa" "table_row"
+    And I should see "Creative Commons - ShareAlike" in the "cc-sa" "table_row"
+
+  Scenario: I should be able to enable and disable licenses
+    Given I log in as "admin"
+    And I navigate to "Licence > Licence settings" in site administration
+    When I set the field "Default site licence" to "Public domain"
+    And I press "Save changes"
+    And I navigate to "Licence > Licence manager" in site administration
+    Then "This is the site default license" "icon" should exist in the "public" "table_row"
+    And "Enable license" "icon" should not exist in the "public" "table_row"
+    And "This is the site default license" "icon" should not exist in the "cc" "table_row"
+    And I navigate to "Licence > Licence settings" in site administration
+    And I set the field "Default site licence" to "Creative Commons"
+    And I press "Save changes"
+    And I navigate to "Licence > Licence manager" in site administration
+    And "This is the site default license" "icon" should exist in the "cc" "table_row"
+    And "Enable license" "icon" should not exist in the "cc" "table_row"
+    And "This is the site default license" "icon" should not exist in the "public" "table_row"
diff --git a/admin/tool/licensemanager/tests/helper_test.php b/admin/tool/licensemanager/tests/helper_test.php
new file mode 100644 (file)
index 0000000..14c34c6
--- /dev/null
@@ -0,0 +1,44 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * Tests for tool_licensemanager helper class.
+ *
+ * @package    tool_licensemanager
+ * @copyright  2020 Tom Dickman <tom.dickman@catalyst-au.net>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * Tests for tool_licensemanager helper class.
+ *
+ * @package    tool_licensemanager
+ * @copyright  2020 Tom Dickman <tom.dickman@catalyst-au.net>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ * @group      tool_licensemanager
+ */
+class helper_test extends advanced_testcase {
+
+    public function test_convert_version_to_epoch() {
+
+        $version = '2020010100';
+        $expected = strtotime(20200101);
+
+        $this->assertEquals($expected, \tool_licensemanager\helper::convert_version_to_epoch($version));
+    }
+}
diff --git a/admin/tool/licensemanager/tests/manager_test.php b/admin/tool/licensemanager/tests/manager_test.php
new file mode 100644 (file)
index 0000000..e86cf68
--- /dev/null
@@ -0,0 +1,199 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * Tests for tool_licensemanager manager class.
+ *
+ * @package    tool_licensemanager
+ * @copyright  2020 Tom Dickman <tom.dickman@catalyst-au.net>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die();
+
+global $CFG;
+require_once($CFG->libdir . '/formslib.php');
+require_once($CFG->libdir . '/licenselib.php');
+
+/**
+ * Tests for tool_licensemanager manager class.
+ *
+ * @package    tool_licensemanager
+ * @copyright  2020 Tom Dickman <tom.dickman@catalyst-au.net>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ * @group      tool_licensemanager
+ */
+class manager_test extends advanced_testcase {
+
+    /**
+     * Test editing a license.
+     */
+    public function test_edit_existing_license() {
+        $this->resetAfterTest();
+
+        // Create initial custom license to edit.
+        $testlicense = new stdClass();
+        $testlicense->shortname = 'my-lic';
+        $testlicense->fullname = 'My License';
+        $testlicense->source = 'https://fakeurl.net';
+        $testlicense->version = date('Ymd', time()) . '00';
+        $testlicense->custom = license_manager::CUSTOM_LICENSE;
+
+        license_manager::save($testlicense);
+        license_manager::enable($testlicense->shortname);
+
+        $manager = new \tool_licensemanager\manager();
+
+        // Attempt to submit form data with altered details.
+        $formdata = [
+            'shortname' => 'new-value',
+            'fullname' => 'New License Name',
+            'source' => 'https://updatedfakeurl.net',
+            'version' => time()
+        ];
+
+        // Attempt to submit form data with an altered shortname.
+        \tool_licensemanager\form\edit_license::mock_submit($formdata);
+
+        // We're testing a private method, so we need to setup reflector magic.
+        $method = new ReflectionMethod('\tool_licensemanager\manager', 'edit');
+        $method->setAccessible(true); // Allow accessing of private method.
+        $method->invoke($manager, \tool_licensemanager\manager::ACTION_UPDATE, $testlicense->shortname);
+
+        // Should not create a new license when updating an existing license.
+        $this->assertEmpty(license_manager::get_license_by_shortname($formdata['shortname']));
+
+        $actual = license_manager::get_license_by_shortname('my-lic');
+        // Should not be able to update the shortname of the license.
+        $this->assertNotSame($formdata['shortname'], $actual->shortname);
+        // Should be able to update other details of the license.
+        $this->assertSame($formdata['fullname'], $actual->fullname);
+        $this->assertSame($formdata['source'], $actual->source);
+        $this->assertSame(date('Ymd', $formdata['version']) . '00', $actual->version);
+    }
+
+    public function test_edit_license_not_exists() {
+        $manager = new \tool_licensemanager\manager();
+
+        // We're testing a private method, so we need to setup reflector magic.
+        $method = new ReflectionMethod('\tool_licensemanager\manager', 'edit');
+        $method->setAccessible(true); // Allow accessing of private method.
+
+        // Attempt to update a license that doesn't exist.
+        $formdata = [
+            'shortname' => 'new-value',
+            'fullname' => 'New License Name',
+            'source' => 'https://updatedfakeurl.net',
+            'version' => time()
+        ];
+        \tool_licensemanager\form\edit_license::mock_submit($formdata);
+
+        // Should not be able to update a license with a shortname that doesn't exist.
+        $this->expectException('moodle_exception');
+        $method->invoke($manager, \tool_licensemanager\manager::ACTION_UPDATE, $formdata['shortname']);
+    }
+
+    public function test_edit_license_no_shortname() {
+        $manager = new \tool_licensemanager\manager();
+
+        // We're testing a private method, so we need to setup reflector magic.
+        $method = new ReflectionMethod('\tool_licensemanager\manager', 'edit');
+        $method->setAccessible(true); // Allow accessing of private method.
+
+        // Attempt to update a license without passing license shortname.
+        $formdata = [
+            'fullname' => 'New License Name',
+            'source' => 'https://updatedfakeurl.net',
+            'version' => time()
+        ];
+        \tool_licensemanager\form\edit_license::mock_submit($formdata);
+
+        // Should not be able to update empty license shortname.
+        $this->expectException('moodle_exception');
+        $method->invoke($manager, \tool_licensemanager\manager::ACTION_UPDATE, '');
+    }
+
+    /**
+     * Test creating a new license.
+     */
+    public function test_edit_create_license() {
+        $this->resetAfterTest();
+
+        $licensecount = count(license_manager::get_licenses());
+
+        $manager = new \tool_licensemanager\manager();
+
+        $formdata = [
+            'shortname' => 'new-value',
+            'fullname' => 'My License',
+            'source' => 'https://fakeurl.net',
+            'version' => time()
+        ];
+
+        // Attempt to submit form data for a new license.
+        \tool_licensemanager\form\edit_license::mock_submit($formdata);
+
+        // We're testing a private method, so we need to setup reflector magic.
+        $method = new ReflectionMethod('\tool_licensemanager\manager', 'edit');
+        $method->setAccessible(true); // Allow accessing of private method.
+        $method->invoke($manager, \tool_licensemanager\manager::ACTION_CREATE, $formdata['shortname']);
+
+        // Should create a new license in database.
+        $this->assertCount($licensecount + 1, license_manager::get_licenses());
+        $actual = license_manager::get_license_by_shortname($formdata['shortname']);
+        $this->assertSame($formdata['shortname'], $actual->shortname);
+        $this->assertSame($formdata['fullname'], $actual->fullname);
+        $this->assertSame($formdata['source'], $actual->source);
+        $this->assertSame(date('Ymd', $formdata['version']) . '00', $actual->version);
+
+        // Attempt to submit form data for a duplicate license.
+        \tool_licensemanager\form\edit_license::mock_submit($formdata);
+
+        // Should not be able to create duplicate licenses.
+        $this->expectException('moodle_exception');
+        $method->invoke($manager, \tool_licensemanager\manager::ACTION_CREATE, $formdata['shortname']);
+    }
+
+    /**
+     * Test changing the order of licenses.
+     */
+    public function test_change_license_order() {
+        $this->resetAfterTest();
+
+        $licenseorder = array_keys(license_manager::get_licenses());
+        $initialposition = array_search('cc-nc', $licenseorder);
+
+        $manager = new tool_licensemanager\manager();
+
+        // We're testing a private method, so we need to setup reflector magic.
+        $method = new ReflectionMethod('\tool_licensemanager\manager', 'change_license_order');
+        $method->setAccessible(true); // Allow accessing of private method.
+        $method->invoke($manager, \tool_licensemanager\manager::ACTION_MOVE_UP, 'cc-nc');
+
+        $licenseorder = array_keys(license_manager::get_licenses());
+        $newposition = array_search('cc-nc', $licenseorder);
+
+        $this->assertLessThan($initialposition, $newposition);
+
+        $initialposition = array_search('allrightsreserved', $licenseorder);
+        $method->invoke($manager, \tool_licensemanager\manager::ACTION_MOVE_DOWN, 'allrightsreserved');
+        $licenseorder = array_keys(license_manager::get_licenses());
+        $newposition = array_search('cc-nc', $licenseorder);
+
+        $this->assertGreaterThan($initialposition, $newposition);
+    }
+
+}
diff --git a/admin/tool/licensemanager/version.php b/admin/tool/licensemanager/version.php
new file mode 100644 (file)
index 0000000..faf9c51
--- /dev/null
@@ -0,0 +1,31 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * Version details for component 'tool_licensemanager'.
+ *
+ * @package    tool_licensemanager
+ * @copyright  Tom Dickman <tomdickman@catalyst-au.net>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die();
+
+$plugin->version   = 2020050600;
+$plugin->requires  = 2020050200;         // Requires this Moodle version.
+$plugin->component = 'tool_licensemanager';
+
+$plugin->maturity = MATURITY_STABLE;
index 2ce495b..3d86e56 100644 (file)
@@ -210,7 +210,7 @@ These actions only affect your view.
 
 You can also choose to display the courses in a list, or with summary information, or the default \'card\' view.';
 $string['tour3_title_displayoptions'] = 'Display options';
-$string['tour3_content_displayoptions'] = 'Courses may be sorted by course name or by last access date.
+$string['tour3_content_displayoptions'] = 'Courses may be sorted by course name, course short name or last access date.
 
 You can also choose to display the courses in a list, with summary information, or the default \'card\' view.';
 
index 9064a0b..816a253 100644 (file)
@@ -158,5 +158,5 @@ Feature: Manage analytics models
   Scenario: Delete model
     When I open the action menu in "Students at risk of not meeting the course completion conditions" "table_row"
     And I choose "Delete" in the open action menu
-    And I click on "Delete" "button" in the "Confirm" "dialogue"
+    And I click on "Delete" "button" in the "Delete" "dialogue"
     Then I should not see "Students at risk of not meeting the course completion conditions"
index c27ccf2..fffcd16 100644 (file)
@@ -161,6 +161,7 @@ class core_backup_async_helper_testcase extends \core_privacy\tests\provider_tes
         $bc = new \backup_controller(\backup::TYPE_1COURSE, $course->id, \backup::FORMAT_MOODLE,
             \backup::INTERACTIVE_NO, \backup::MODE_COPY, $USER->id, \backup::RELEASESESSION_YES);
         $backupid = $bc->get_backupid();
+        $bc->destroy();
         $copyrec = \async_helper::get_backup_record($backupid);
 
         $this->assertEquals($backupid, $copyrec->backupid);
@@ -185,8 +186,9 @@ class core_backup_async_helper_testcase extends \core_privacy\tests\provider_tes
         $this->assertFalse($ispending);
 
         // Create the initial backupcontoller.
-        new \backup_controller(\backup::TYPE_1COURSE, $course->id, \backup::FORMAT_MOODLE,
+        $bc = new \backup_controller(\backup::TYPE_1COURSE, $course->id, \backup::FORMAT_MOODLE,
             \backup::INTERACTIVE_NO, \backup::MODE_ASYNC, $USER->id, \backup::RELEASESESSION_YES);
+        $bc->destroy();
         $ispending = async_helper::is_async_pending($course->id, 'course', 'backup');
 
         // Should be false as there as async backup is false.
@@ -215,8 +217,9 @@ class core_backup_async_helper_testcase extends \core_privacy\tests\provider_tes
         $this->assertFalse($ispending);
 
         // Create the initial backupcontoller.
-        new \backup_controller(\backup::TYPE_1COURSE, $course->id, \backup::FORMAT_MOODLE,
+        $bc = new \backup_controller(\backup::TYPE_1COURSE, $course->id, \backup::FORMAT_MOODLE,
             \backup::INTERACTIVE_NO, \backup::MODE_COPY, $USER->id, \backup::RELEASESESSION_YES);
+        $bc->destroy();
         $ispending = async_helper::is_async_pending($course->id, 'course', 'backup');
 
         // Should be True as this a copy operation.
diff --git a/badges/amd/build/backpackactions.min.js b/badges/amd/build/backpackactions.min.js
new file mode 100644 (file)
index 0000000..659e07c
Binary files /dev/null and b/badges/amd/build/backpackactions.min.js differ
diff --git a/badges/amd/build/backpackactions.min.js.map b/badges/amd/build/backpackactions.min.js.map
new file mode 100644 (file)
index 0000000..52bdc23
Binary files /dev/null and b/badges/amd/build/backpackactions.min.js.map differ
diff --git a/badges/amd/build/selectors.min.js b/badges/amd/build/selectors.min.js
new file mode 100644 (file)
index 0000000..dbe7b36
Binary files /dev/null and b/badges/amd/build/selectors.min.js differ
diff --git a/badges/amd/build/selectors.min.js.map b/badges/amd/build/selectors.min.js.map
new file mode 100644 (file)
index 0000000..856297c
Binary files /dev/null and b/badges/amd/build/selectors.min.js.map differ
diff --git a/badges/amd/src/backpackactions.js b/badges/amd/src/backpackactions.js
new file mode 100644 (file)
index 0000000..5730b84
--- /dev/null
@@ -0,0 +1,89 @@
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * Action methods related to backpacks.
+ *
+ * @module     core_badges/backpackactions
+ * @package    core_badges
+ * @copyright  2020 Sara Arjona <sara@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+import $ from 'jquery';
+import selectors from 'core_badges/selectors';
+import {get_string as getString} from 'core/str';
+import Pending from 'core/pending';
+import ModalFactory from 'core/modal_factory';
+import ModalEvents from 'core/modal_events';
+import Config from 'core/config';
+
+/**
+ * Set up the actions.
+ *
+ * @method init
+ */
+export const init = () => {
+    const pendingPromise = new Pending();
+
+    const root = $(selectors.elements.main);
+    registerListenerEvents(root);
+
+    pendingPromise.resolve();
+};
+
+/**
+ * Register backpack related event listeners.
+ *
+ * @method registerListenerEvents
+ * @param {Object} root The root element.
+ */
+const registerListenerEvents = (root) => {
+
+    root.on('click', selectors.actions.deletebackpack, async(e) => {
+        e.preventDefault();
+
+        const link = $(e.currentTarget);
+        const modal = await buildModal(link);
+
+        displayModal(modal, link);
+    });
+};
+
+const buildModal = async(link) => {
+
+    const backpackurl = link.closest(selectors.elements.backpackurl).attr('data-backpackurl');
+
+    return ModalFactory.create({
+        title: await getString('delexternalbackpack', 'core_badges'),
+        body: await getString('delexternalbackpackconfirm', 'core_badges', backpackurl),
+        type: ModalFactory.types.SAVE_CANCEL,
+    });
+
+};
+
+const displayModal = async(modal, link) => {
+    modal.setSaveButtonText(await getString('delete', 'core'));
+
+    modal.getRoot().on(ModalEvents.save, function() {
+        window.location.href = link.attr('href') + '&sesskey=' + Config.sesskey + '&confirm=1';
+    });
+
+    modal.getRoot().on(ModalEvents.hidden, function() {
+        modal.destroy();
+    });
+
+    modal.show();
+};
diff --git a/badges/amd/src/selectors.js b/badges/amd/src/selectors.js
new file mode 100644 (file)
index 0000000..fea6dfb
--- /dev/null
@@ -0,0 +1,46 @@
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * Define all of the selectors we will be using on the backpack interface.
+ *
+ * @module     core_badges/selectors
+ * @package    core_badges
+ * @copyright  2020 Sara Arjona <sara@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+/**
+ * A small helper function to build queryable data selectors.
+ *
+ * @method getDataSelector
+ * @param {String} name
+ * @param {String} value
+ * @return {string}
+ */
+const getDataSelector = (name, value) => {
+    return `[data-${name}="${value}"]`;
+};
+
+export default {
+    actions: {
+        deletebackpack: getDataSelector('action', 'deletebackpack'),
+    },
+    elements: {
+        clearsearch: '.input-group-append .clear-icon',
+        main: '#backpacklist',
+        backpackurl: '[data-backpackurl]',
+    },
+};
diff --git a/badges/backpack-connect.php b/badges/backpack-connect.php
new file mode 100644 (file)
index 0000000..cece808
--- /dev/null
@@ -0,0 +1,58 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * Connect to backpack site.
+ *
+ * @package    core_badges
+ * @copyright  2020 Tung Thai
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ * @author     Tung Thai <Tung.ThaiDuc@nashtechglobal.com>
+ */
+
+require_once(__DIR__ . '/../config.php');
+require_once($CFG->libdir . '/badgeslib.php');
+
+$scope = optional_param('scope', '', PARAM_RAW);
+$action = optional_param('action', null, PARAM_RAW);
+
+if (badges_open_badges_backpack_api() != OPEN_BADGES_V2P1) {
+    throw new coding_exception('backpacks only support Open Badges V2.1');
+}
+
+require_login();
+
+$externalbackpack = badges_get_site_backpack($CFG->badges_site_backpack);
+$persistedissuer = \core\oauth2\issuer::get_record(['id' => $externalbackpack->oauth2_issuerid]);
+if ($persistedissuer) {
+    $issuer = new \core\oauth2\issuer($externalbackpack->oauth2_issuerid);
+    $returnurl = new moodle_url('/badges/backpack-connect.php',
+        ['action' => 'authorization', 'sesskey' => sesskey()]);
+
+    $client = new core_badges\oauth2\client($issuer, $returnurl, $scope, $externalbackpack);
+    if ($client) {
+        if (!$client->is_logged_in()) {
+            redirect($client->get_login_url());
+        }
+        $wantsurl = new moodle_url('/badges/mybadges.php');
+        $auth = new \core_badges\oauth2\auth();
+        $auth->complete_data($client, $wantsurl);
+    } else {
+        throw new moodle_exception('Could not get an OAuth client.');
+    }
+} else {
+    throw new moodle_exception('Unknown OAuth client.');
+}
diff --git a/badges/backpack-export.php b/badges/backpack-export.php
new file mode 100644 (file)
index 0000000..e1015b7
--- /dev/null
@@ -0,0 +1,62 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * Export badges to the backpack site.
+ *
+ * @package    core_badges
+ * @copyright  2020 Tung Thai
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ * @author     Tung Thai <Tung.ThaiDuc@nashtechglobal.com>
+ */
+require_once(__DIR__ . '/../config.php');
+require_once($CFG->libdir . '/badgeslib.php');
+
+if (badges_open_badges_backpack_api() != OPEN_BADGES_V2P1) {
+    throw new coding_exception('backpacks only support Open Badges V2.1');
+}
+$hash = optional_param('hash', null, PARAM_RAW);
+
+$PAGE->set_pagelayout('admin');
+$url = new moodle_url('/badges/backpack-export.php');
+
+require_login();
+if (empty($CFG->badges_allowexternalbackpack) || empty($CFG->enablebadges)) {
+    redirect($CFG->wwwroot);
+}
+$backpack = badges_get_site_backpack($CFG->badges_site_backpack);
+$userbadges = badges_get_user_badges($USER->id);
+$context = context_user::instance($USER->id);
+
+$PAGE->set_context($context);
+$PAGE->set_url($url);
+$title = get_string('badges', 'badges');
+$PAGE->set_title($title);
+$PAGE->set_heading(fullname($USER));
+$PAGE->set_pagelayout('standard');
+
+$redirecturl = new moodle_url('/badges/mybadges.php');
+if ($hash) {
+    $backpack = badges_get_site_backpack($CFG->badges_site_backpack);
+    $api = new core_badges\backpack_api2p1($backpack);
+    $notify = $api->put_assertions($hash);
+    if (!empty($notify['status']) && $notify['status'] == \core\output\notification::NOTIFY_SUCCESS) {
+        redirect($redirecturl, $notify['message'], null, \core\output\notification::NOTIFY_SUCCESS);
+    } else if (!empty($notify['status']) && $notify['status'] == \core\output\notification::NOTIFY_ERROR) {
+        redirect($redirecturl, $notify['message'], null, \core\output\notification::NOTIFY_ERROR);
+    }
+}
+redirect($redirecturl);
\ No newline at end of file
index 341d511..ae63fd6 100644 (file)
@@ -34,6 +34,7 @@ $output = $PAGE->get_renderer('core', 'badges');
 
 $id = optional_param('id', 0, PARAM_INT);
 $action = optional_param('action', '', PARAM_ALPHA);
+$confirm = optional_param('confirm', 1, PARAM_BOOL);
 
 $PAGE->set_pagelayout('admin');
 $url = new moodle_url('/badges/backpacks.php');
@@ -45,6 +46,18 @@ if (empty($CFG->badges_allowexternalbackpack)) {
 $PAGE->set_url($url);
 $PAGE->set_title(get_string('managebackpacks', 'badges'));
 $PAGE->set_heading($SITE->fullname);
+
+$msg = '';
+$msgtype = 'error';
+if ($action == 'delete' && $confirm && confirm_sesskey()) {
+    if (badges_delete_site_backpack($id)) {
+        $msg = get_string('sitebackpackdeleted', 'badges');
+        $msgtype = 'notifysuccess';
+    } else {
+        $msg = get_string('sitebackpacknotdeleted', 'badges');
+    }
+}
+
 if ($action == 'edit') {
     $backpack = null;
     if (!empty($id)) {
@@ -71,6 +84,9 @@ if ($action == 'edit') {
     echo $OUTPUT->header();
     echo $output->heading(get_string('managebackpacks', 'badges'));
 
+    if ($msg) {
+        echo $OUTPUT->notification($msg, $msgtype);
+    }
     $page = new \core_badges\output\external_backpacks_page($url);
     echo $output->render($page);
 }
diff --git a/badges/classes/backpack_api2p1.php b/badges/classes/backpack_api2p1.php
new file mode 100644 (file)
index 0000000..055a3c3
--- /dev/null
@@ -0,0 +1,236 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * Communicate with backpacks.
+ *
+ * @copyright  2020 Tung Thai based on Totara Learning Solutions Ltd {@link http://www.totaralms.com/} dode
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ * @author     Tung Thai <Tung.ThaiDuc@nashtechglobal.com>
+ */
+
+namespace core_badges;
+
+defined('MOODLE_INTERNAL') || die();
+
+require_once($CFG->libdir . '/filelib.php');
+
+use cache;
+use coding_exception;
+use context_system;
+use moodle_url;
+use core_badges\backpack_api2p1_mapping;
+use core_badges\oauth2\client;
+use curl;
+use stdClass;
+
+/**
+ * To process badges with backpack and control api request and this class using for Open Badge API v2.1 methods.
+ *
+ * @package   core_badges
+ * @copyright  2020 Tung Thai
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class backpack_api2p1 {
+
+    /** @var object is the external backpack. */
+    private $externalbackpack;
+
+    /** @var array define api mapping. */
+    private $mappings = [];
+
+    /** @var false|null|stdClass|\core_badges\backpack_api2p1 to */
+    private $tokendata;
+
+    /** @var null clienid. */
+    private $clientid = null;
+
+    /** @var null version api of the backpack. */
+    protected $backpackapiversion;
+
+    /** @var null api URL of the backpack. */
+    protected $backpackapiurl = '';
+
+    /**
+     * backpack_api2p1 constructor.
+     *
+     * @param object $externalbackpack object
+     * @throws coding_exception error message
+     */
+    public function __construct($externalbackpack) {
+
+        if (!empty($externalbackpack)) {
+            $this->externalbackpack = $externalbackpack;
+            $this->backpackapiversion = $externalbackpack->apiversion;
+            $this->backpackapiurl = $externalbackpack->backpackapiurl;
+            $this->get_clientid = $this->get_clientid($externalbackpack->oauth2_issuerid);
+
+            if (!($this->tokendata = $this->get_stored_token($externalbackpack->id))
+                && $this->backpackapiversion != OPEN_BADGES_V2P1) {
+                throw new coding_exception('Backpack incorrect');
+            }
+        }
+
+        $this->define_mappings();
+    }
+
+
+    /**
+     * Define the mappings supported by this usage and api version.
+     */
+    private function define_mappings() {
+        if ($this->backpackapiversion == OPEN_BADGES_V2P1) {
+
+            $mapping = [];
+            $mapping[] = [
+                'post.assertions',                               // Action.
+                '[URL]/assertions',   // URL
+                '[PARAM]',                                  // Post params.
+                false,                                      // Multiple.
+                'post',                                     // Method.
+                true,                                       // JSON Encoded.
+                true                                        // Auth required.
+            ];
+
+            $mapping[] = [
+                'get.assertions',                               // Action.
+                '[URL]/assertions',   // URL
+                '[PARAM]',                                  // Post params.
+                false,                                      // Multiple.
+                'get',                                     // Method.
+                true,                                       // JSON Encoded.
+                true                                        // Auth required.
+            ];
+
+            foreach ($mapping as $map) {
+                $map[] = false; // Site api function.
+                $map[] = OPEN_BADGES_V2P1; // V2 function.
+                $this->mappings[] = new backpack_api2p1_mapping(...$map);
+            }
+
+        }
+    }
+
+    /**
+     * Disconnect the backpack from this user.
+     *
+     * @param object $backpack to disconnect.
+     * @return bool
+     * @throws \dml_exception
+     */
+    public function disconnect_backpack($backpack) {
+        global $USER, $DB;
+
+        if ($backpack) {
+            $DB->delete_records_select('badge_external', 'backpackid = :backpack', ['backpack' => $backpack->id]);
+            $DB->delete_records('badge_backpack', ['id' => $backpack->id]);
+            $DB->delete_records('badge_backpack_oauth2', ['externalbackpackid' => $this->externalbackpack->id,
+                'userid' => $USER->id]);
+
+            return true;
+        }
+        return false;
+    }
+
+    /**
+     * Make an api request.
+     *
+     * @param string $action The api function.
+     * @param string $postdata The body of the api request.
+     * @return mixed
+     */
+    public function curl_request($action, $postdata = null) {
+        $tokenkey = $this->tokendata->token;
+        foreach ($this->mappings as $mapping) {
+            if ($mapping->is_match($action)) {
+                return $mapping->request(
+                    $this->backpackapiurl,
+                    $tokenkey,
+                    $postdata
+                );
+            }
+        }
+
+        throw new coding_exception('Unknown request');
+    }
+
+    /**
+     * Get token.
+     *
+     * @param int $externalbackpackid ID of external backpack.
+     * @return oauth2\badge_backpack_oauth2|false|stdClass|null
+     */
+    protected function get_stored_token($externalbackpackid) {
+        global $USER;
+
+        $token = \core_badges\oauth2\badge_backpack_oauth2::get_record(
+            ['externalbackpackid' => $externalbackpackid, 'userid' => $USER->id]);
+        if ($token !== false) {
+            $token = $token->to_record();
+            return $token;
+        }
+        return null;
+    }
+
+    /**
+     * Get client id.
+     *
+     * @param int $issuerid id of Oauth2 service.
+     * @throws coding_exception
+     */
+    private function get_clientid($issuerid) {
+        $issuer = \core\oauth2\api::get_issuer($issuerid);
+        if (!empty($issuer)) {
+            $this->clientid = $issuer->get('clientid');
+        }
+    }
+
+    /**
+     * Export a badge to the backpack site.
+     *
+     * @param string $hash of badge issued.
+     * @return array
+     * @throws \moodle_exception
+     * @throws coding_exception
+     */
+    public function put_assertions($hash) {
+        $data = [];
+        if (!$hash) {
+            return false;
+        }
+
+        $issuer = new \core\oauth2\issuer($this->externalbackpack->oauth2_issuerid);
+        $client = new client($issuer, new moodle_url('/badges/mybadges.php'), '', $this->externalbackpack);
+        if (!$client->is_logged_in()) {
+            $redirecturl = new moodle_url('/badges/mybadges.php', ['error' => 'backpackexporterror']);
+            redirect($redirecturl);
+        }
+
+        $this->tokendata = $this->get_stored_token($this->externalbackpack->id);
+
+        $assertion = new \core_badges_assertion($hash, OPEN_BADGES_V2);
+        $data['assertion'] = $assertion->get_badge_assertion();
+        $response = $this->curl_request('post.assertions', $data);
+        if ($response && isset($response->status->statusCode) && $response->status->statusCode == 200) {
+            $msg['status'] = \core\output\notification::NOTIFY_SUCCESS;
+            $msg['message'] = get_string('addedtobackpack', 'badges');
+        } else {
+            $msg['status'] = \core\output\notification::NOTIFY_ERROR;
+            $msg['message'] = get_string('backpackexporterror', 'badges', $data['assertion']['badge']['name']);
+        }
+        return $msg;
+    }
+}
diff --git a/badges/classes/backpack_api2p1_mapping.php b/badges/classes/backpack_api2p1_mapping.php
new file mode 100644 (file)
index 0000000..7419f3b
--- /dev/null
@@ -0,0 +1,178 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * Represent the url for each method and the encoding of the parameters and response.
+ *
+ * The code is based on badges/classes/backpack_api_mapping.php by Yuliya Bozhko <yuliya.bozhko@totaralms.com>.
+ *
+ * @copyright  2020 Tung Thai
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ * @author     Tung Thai <Tung.ThaiDuc@nashtechglobal.com>
+ */
+
+namespace core_badges;
+
+defined('MOODLE_INTERNAL') || die();
+
+global $CFG;
+require_once($CFG->libdir . '/filelib.php');
+
+use context_system;
+use curl;
+
+/**
+ * Represent a single method for the remote api and this class using for Open Badge API v2.1 methods.
+ *
+ * @package   core_badges
+ * @copyright  2020 Tung Thai
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class backpack_api2p1_mapping {
+
+    /** @var string The action of this method. */
+    public $action;
+
+    /** @var string The base url of this backpack. */
+    private $url;
+
+    /** @var array List of parameters for this method. */
+    public $params;
+
+    /** @var boolean This method returns an array of responses. */
+    public $multiple;
+
+    /** @var string get or post methods. */
+    public $method;
+
+    /** @var boolean json decode the response. */
+    public $json;
+
+    /** @var boolean Authentication is required for this request. */
+    public $authrequired;
+
+    /** @var boolean Differentiate the function that can be called on a user backpack or a site backpack. */
+    private $isuserbackpack;
+
+    /**
+     * Create a mapping.
+     *
+     * @param string $action The action of this method.
+     * @param string $url The base url of this backpack.
+     * @param mixed $postparams List of parameters for this method.
+     * @param boolean $multiple This method returns an array of responses.
+     * @param string $method get or post methods.
+     * @param boolean $json json decode the response.
+     * @param boolean $authrequired Authentication is required for this request.
+     * @param boolean $isuserbackpack user backpack or a site backpack.
+     * @param integer $backpackapiversion OpenBadges version 1 or 2.
+     */
+    public function __construct($action, $url, $postparams,
+                                $multiple, $method, $json, $authrequired, $isuserbackpack, $backpackapiversion) {
+        $this->action = $action;
+        $this->url = $url;
+        $this->postparams = $postparams;
+        $this->multiple = $multiple;
+        $this->method = $method;
+        $this->json = $json;
+        $this->authrequired = $authrequired;
+        $this->isuserbackpack = $isuserbackpack;
+        $this->backpackapiversion = $backpackapiversion;
+    }
+
+    /**
+     * Does the action match this mapping?
+     *
+     * @param string $action The action.
+     * @return boolean
+     */
+    public function is_match($action) {
+        return $this->action == $action;
+    }
+
+    /**
+     * Parse the method url and insert parameters.
+     *
+     * @param string $apiurl The raw apiurl.
+     * @return string
+     */
+    private function get_url($apiurl) {
+        $urlscheme = parse_url($apiurl, PHP_URL_SCHEME);
+        $urlhost = parse_url($apiurl, PHP_URL_HOST);
+
+        $url = $this->url;
+        $url = str_replace('[SCHEME]', $urlscheme, $url);
+        $url = str_replace('[HOST]', $urlhost, $url);
+        $url = str_replace('[URL]', $apiurl, $url);
+
+        return $url;
+    }
+
+    /**
+     * Standard options used for all curl requests.
+     *
+     * @return array
+     */
+    private function get_curl_options() {
+        return array(
+            'FRESH_CONNECT'     => true,
+            'RETURNTRANSFER'    => true,
+            'FORBID_REUSE'      => true,
+            'HEADER'            => 0,
+            'CONNECTTIMEOUT'    => 3,
+            'CONNECTTIMEOUT'    => 3,
+            // Follow redirects with the same type of request when sent 301, or 302 redirects.
+            'CURLOPT_POSTREDIR' => 3,
+        );
+    }
+
+    /**
+     * Make an api request and parse the response.
+     *
+     * @param string $apiurl Raw request url.
+     * @param string $tokenkey to verify authorization.
+     * @param array $post request method.
+     * @return bool|mixed
+     */
+    public function request($apiurl, $tokenkey, $post = []) {
+        $curl = new curl();
+        $url = $this->get_url($apiurl);
+        if ($tokenkey) {
+            $curl->setHeader('Authorization: Bearer ' . $tokenkey);
+        }
+
+        if ($this->json) {
+            $curl->setHeader(array('Content-type: application/json'));
+            if ($this->method == 'post') {
+                $post = json_encode($post);
+            }
+        }
+
+        $curl->setHeader(array('Accept: application/json', 'Expect:'));
+        $options = $this->get_curl_options();
+        if ($this->method == 'get') {
+            $response = $curl->get($url, $post, $options);
+        } else if ($this->method == 'post') {
+            $response = $curl->post($url, $post, $options);
+        }
+        $response = json_decode($response);
+        if (isset($response->result)) {
+            $response = $response->result;
+        }
+
+        return $response;
+    }
+}
\ No newline at end of file
index a060d85..6c7370c 100644 (file)
@@ -88,16 +88,18 @@ class backpack extends moodleform {
             $status = html_writer::tag('span', get_string('notconnected', 'badges'),
                 array('class' => 'notconnected', 'id' => 'connection-status'));
             $mform->addElement('static', 'status', get_string('status'), $status);
-            $mform->addElement('text', 'email', get_string('email'), 'maxlength="100" size="30"');
-            $mform->addHelpButton('email', 'backpackemail', 'badges');
-            $mform->addRule('email', get_string('required'), 'required', null, 'client');
-            $mform->setType('email', PARAM_EMAIL);
-            if (badges_open_badges_backpack_api() == OPEN_BADGES_V2) {
-                $mform->addElement('passwordunmask', 'backpackpassword', get_string('password'));
-                $mform->setType('backpackpassword', PARAM_RAW);
-            } else {
-                $mform->addElement('hidden', 'backpackpassword', '');
-                $mform->setType('backpackpassword', PARAM_RAW);
+            if (badges_open_badges_backpack_api() != OPEN_BADGES_V2P1) {
+                $mform->addElement('text', 'email', get_string('email'), 'maxlength="100" size="30"');
+                $mform->addHelpButton('email', 'backpackemail', 'badges');
+                $mform->addRule('email', get_string('required'), 'required', null, 'client');
+                $mform->setType('email', PARAM_EMAIL);
+                if (badges_open_badges_backpack_api() == OPEN_BADGES_V2) {
+                    $mform->addElement('passwordunmask', 'backpackpassword', get_string('password'));
+                    $mform->setType('backpackpassword', PARAM_RAW);
+                } else {
+                    $mform->addElement('hidden', 'backpackpassword', '');
+                    $mform->setType('backpackpassword', PARAM_RAW);
+                }
             }
             $this->add_action_buttons(false, get_string('backpackconnectionconnect', 'badges'));
         }
@@ -107,7 +109,12 @@ class backpack extends moodleform {
      * Validates form data
      */
     public function validation($data, $files) {
+        global $CFG;
+
         $errors = parent::validation($data, $files);
+        if (badges_open_badges_backpack_api() == OPEN_BADGES_V2P1) {
+            return $errors;
+        }
         // We don't need to verify the email address if we're clearing a pending email verification attempt.
         if (!isset($data['revertbutton'])) {
             $check = new stdClass();
index 7b7d653..f92febd 100644 (file)
@@ -48,33 +48,31 @@ class external_backpack extends \moodleform {
 
         if (isset($this->_customdata['externalbackpack'])) {
             $backpack = $this->_customdata['externalbackpack'];
-        } else {
-            throw new \coding_exception('backpack is required.');
         }
 
-        $url = $backpack->backpackapiurl;
+        $mform->addElement('hidden', 'action', 'edit');
+        $mform->setType('action', PARAM_ALPHA);
 
-        $mform->addElement('static', 'backpackapiurlinfo', get_string('backpackapiurl', 'core_badges'), $url);
+        if ($backpack) {
+            $mform->addElement('hidden', 'id', $backpack->id);
+            $mform->setType('id', PARAM_INTEGER);
+        }
 
-        $mform->addElement('hidden', 'backpackapiurl', $url);
+        $mform->addElement('text', 'backpackapiurl',  get_string('backpackapiurl', 'core_badges'));
         $mform->setType('backpackapiurl', PARAM_URL);
+        $mform->addRule('backpackapiurl', null, 'required', null, 'client');
+        $mform->addRule('backpackapiurl', get_string('maximumchars', '', 255), 'maxlength', 50, 'client');
 
-        $url = $backpack->backpackweburl;
-        $mform->addElement('static', 'backpackweburlinfo', get_string('backpackweburl', 'core_badges'), $url);
-        $mform->addElement('hidden', 'backpackweburl', $url);
+        $mform->addElement('text', 'backpackweburl', get_string('backpackweburl', 'core_badges'));
         $mform->setType('backpackweburl', PARAM_URL);
+        $mform->addRule('backpackweburl', null, 'required', null, 'client');
+        $mform->addRule('backpackweburl', get_string('maximumchars', '', 255), 'maxlength', 50, 'client');
 
-        $options = badges_get_badge_api_versions();
-        $label = $options[$backpack->apiversion];
-        $mform->addElement('static', 'apiversioninfo', get_string('apiversion', 'core_badges'), $label);
-        $mform->addElement('hidden', 'apiversion', $backpack->apiversion);
-        $mform->setType('apiversion', PARAM_INTEGER);
-
-        $mform->addElement('hidden', 'id', $backpack->id);
-        $mform->setType('id', PARAM_INTEGER);
-
-        $mform->addElement('hidden', 'action', 'edit');
-        $mform->setType('action', PARAM_ALPHA);
+        $apiversions = badges_get_badge_api_versions();
+        $mform->addElement('select', 'apiversion', get_string('apiversion', 'core_badges'), $apiversions);
+        $mform->setType('apiversion', PARAM_RAW);
+        $mform->setDefault('apiversion', OPEN_BADGES_V2P1);
+        $mform->addRule('apiversion', null, 'required', null, 'client');
 
         $issuername = $CFG->badges_defaultissuername;
         $mform->addElement('static', 'issuerinfo', get_string('defaultissuername', 'core_badges'), $issuername);
@@ -82,12 +80,19 @@ class external_backpack extends \moodleform {
         $issuercontact = $CFG->badges_defaultissuercontact;
         $mform->addElement('static', 'issuerinfo', get_string('defaultissuercontact', 'core_badges'), $issuercontact);
 
-        $mform->addElement('passwordunmask', 'password', get_string('defaultissuerpassword', 'core_badges'));
-        $mform->setType('password', PARAM_RAW);
-        $mform->addHelpButton('password', 'defaultissuerpassword', 'badges');
-        $mform->hideIf('password', 'apiversion', 'eq', 1);
-
-        $this->set_data($backpack);
+        if ($backpack && $backpack->apiversion != OPEN_BADGES_V2P1) {
+            $mform->addElement('passwordunmask', 'password', get_string('defaultissuerpassword', 'core_badges'));
+            $mform->setType('password', PARAM_RAW);
+            $mform->addHelpButton('password', 'defaultissuerpassword', 'badges');
+            $mform->hideIf('password', 'apiversion', 'eq', 1);
+        } else {
+            $oauth2options = badges_get_oauth2_service_options();
+            $mform->addElement('select', 'oauth2_issuerid', get_string('oauth2issuer', 'core_badges'), $oauth2options);
+            $mform->setType('oauth2_issuerid', PARAM_INT);
+        }
+        if ($backpack) {
+            $this->set_data($backpack);
+        }
 
         // Disable short forms.
         $mform->setDisableShortforms();
@@ -95,4 +100,24 @@ class external_backpack extends \moodleform {
         $this->add_action_buttons();
     }
 
+    /**
+     * Validate the data from the form.
+     *
+     * @param  array $data form data
+     * @param  array $files form files
+     * @return array An array of error messages.
+     */
+    public function validation($data, $files) {
+        $errors = parent::validation($data, $files);
+
+        // Ensure backpackapiurl and  are valid URLs.
+        if (!empty($data['backpackapiurl']) && !preg_match('@^https?://.+@', $data['backpackapiurl'])) {
+            $errors['backpackapiurl'] = get_string('invalidurl', 'badges');
+        }
+        if (!empty($data['backpackweburl']) && !preg_match('@^https?://.+@', $data['backpackweburl'])) {
+            $errors['backpackweburl'] = get_string('invalidurl', 'badges');
+        }
+
+        return $errors;
+    }
 }
diff --git a/badges/classes/helper.php b/badges/classes/helper.php
new file mode 100644 (file)
index 0000000..b744c1e
--- /dev/null
@@ -0,0 +1,75 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * Badge helper library.
+ *
+ * @package    core
+ * @subpackage badges
+ * @copyright  2020 Sara Arjona <sara@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+namespace core_badges;
+
+/**
+ * Badge helper library.
+ *
+ * @copyright  2020 Sara Arjona <sara@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class helper {
+
+    /**
+     * Create a backpack.
+     *
+     * @param array $params Parameters.
+     * @return object
+     */
+    public static function create_fake_backpack(array $params = []) {
+        global $DB;
+
+        $record = (object) array_merge([
+            'userid' => null,
+            'email' => 'test@example.com',
+            'backpackuid' => -1,
+            'autosync' => 0,
+            'password' => '',
+            'externalbackpackid' => 12345,
+        ], $params);
+        $record->id = $DB->insert_record('badge_backpack', $record);
+
+        return $record;
+    }
+
+    /**
+     * Create a user backpack collection.
+     *
+     * @param array $params Parameters.
+     * @return object
+     */
+    public static function create_fake_backpack_collection(array $params = []) {
+        global $DB;
+
+        $record = (object) array_merge([
+            'backpackid' => 12345,
+            'collectionid' => -1,
+            'entityid' => random_string(20),
+        ], $params);
+        $record->id = $DB->insert_record('badge_external', $record);
+
+        return $record;
+    }
+}
diff --git a/badges/classes/oauth2/auth.php b/badges/classes/oauth2/auth.php
new file mode 100644 (file)
index 0000000..038689c
--- /dev/null
@@ -0,0 +1,96 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * This file to proccess Oauth2 connects for backpack.
+ *
+ * @package    core_badges
+ * @subpackage badges
+ * @copyright  2020 Tung Thai
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ * @author     Tung Thai <Tung.ThaiDuc@nashtechglobal.com>
+ */
+
+namespace core_badges\oauth2;
+
+defined('MOODLE_INTERNAL') || die();
+
+require_once($CFG->libdir.'/authlib.php');
+
+use stdClass;
+
+/**
+ * Proccess Oauth2 connects to backpack site.
+ *
+ * @copyright  2020 Tung Thai
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ * @author     Tung Thai <Tung.ThaiDuc@nashtechglobal.com>
+ */
+class auth extends \auth_oauth2\auth {
+
+    /**
+     * To complete data after login.
+     *
+     * @param client $client object.
+     * @param string $redirecturl the url redirect.
+     */
+    public function complete_data(\core_badges\oauth2\client $client, $redirecturl) {
+        global $DB, $USER;
+
+        $userinfo = $client->get_userinfo();
+        $badgebackpack = new stdClass();
+        $badgebackpack->userid = $USER->id;
+        if ($userinfo && isset($userinfo->email)) {
+            $badgebackpack->email = $userinfo->email;
+        } else {
+            $badgebackpack->email = $USER->email;
+        }
+        $badgebackpack->externalbackpackid = $client->backpack->id;
+        $badgebackpack->backpackuid = 0;
+        $badgebackpack->autosync = 0;
+        $badgebackpack->password = '';
+        $record = $DB->get_record('badge_backpack', ['userid' => $USER->id,
+            'externalbackpackid' => $client->backpack->id]);
+        if (!$record) {
+            $DB->insert_record('badge_backpack', $badgebackpack);
+        } else {
+            $badgebackpack->id = $record->id;
+            $DB->update_record('badge_backpack', $badgebackpack);
+        }
+
+        redirect($redirecturl, get_string('backpackconnected', 'badges'), null,
+            \core\output\notification::NOTIFY_SUCCESS);
+    }
+
+    /**
+     * Check user has been logged the backpack site.
+     *
+     * @param int $externalbackpackid ID of external backpack.
+     * @param int $userid ID of user.
+     * @return bool
+     */
+    public static function is_logged_oauth2($externalbackpackid, $userid) {
+        global $USER;
+        if (empty($userid)) {
+            $userid = $USER->id;
+        }
+        $persistedtoken = badge_backpack_oauth2::get_record(['externalbackpackid' => $externalbackpackid, 'userid' => $userid]);
+        if ($persistedtoken) {
+            return true;
+        }
+        return false;
+    }
+}
diff --git a/badges/classes/oauth2/badge_backpack_oauth2.php b/badges/classes/oauth2/badge_backpack_oauth2.php
new file mode 100644 (file)
index 0000000..7e108ed
--- /dev/null
@@ -0,0 +1,77 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * This file contains the form add/update oauth2 for backpack is connected.
+ *
+ * @package    core_badges
+ * @subpackage badges
+ * @copyright  2020 Tung Thai
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ * @author     Tung Thai <Tung.ThaiDuc@nashtechglobal.com>
+ */
+
+namespace core_badges\oauth2;
+
+defined('MOODLE_INTERNAL') || die();
+
+use core\persistent;
+
+/**
+ * Class badge_backpack_oauth2 for backpack is connected.
+ *
+ * @copyright  2020 Tung Thai
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ * @author     Tung Thai <Tung.ThaiDuc@nashtechglobal.com>
+ */
+class badge_backpack_oauth2 extends persistent {
+
+    /**
+     * The table name.
+     */
+    const TABLE = 'badge_backpack_oauth2';
+
+    /**
+     * Return the definition of the properties of this model.
+     *
+     * @return array
+     */
+    protected static function define_properties() {
+        return array(
+            'userid' => array(
+                'type' => PARAM_INT,
+            ),
+            'issuerid' => array(
+                'type' => PARAM_INT
+            ),
+            'externalbackpackid' => array(
+                'type' => PARAM_INT
+            ),
+            'token' => array(
+                'type' => PARAM_TEXT
+            ),
+            'refreshtoken' => array(
+                'type' => PARAM_TEXT
+            ),
+            'expires' => array(
+                'type' => PARAM_INT
+            ),
+            'scope' => array(
+                'type' => PARAM_TEXT
+            ),
+        );
+    }
+}
\ No newline at end of file
diff --git a/badges/classes/oauth2/client.php b/badges/classes/oauth2/client.php
new file mode 100644 (file)
index 0000000..c214b5c
--- /dev/null
@@ -0,0 +1,348 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * Configurable OAuth2 client class.
+ *
+ * @package    core_badges
+ * @subpackage badges
+ * @copyright  2020 Tung Thai
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ * @author     Tung Thai <Tung.ThaiDuc@nashtechglobal.com>
+ */
+
+namespace core_badges\oauth2;
+
+defined('MOODLE_INTERNAL') || die();
+
+require_once($CFG->libdir . '/oauthlib.php');
+require_once($CFG->libdir . '/filelib.php');
+require_once('badge_backpack_oauth2.php');
+
+use moodle_url;
+use moodle_exception;
+use stdClass;
+
+define('BACKPACK_CHALLENGE_METHOD', 'S256');
+define('BACKPACK_CODE_VERIFIER_TIME', 60);
+
+/**
+ * Configurable OAuth2 client to request authorization and store token. Use the PKCE method to verifier authorization.
+ *
+ * @copyright  2020 Tung Thai
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ * @author     Tung Thai <Tung.ThaiDuc@nashtechglobal.com>
+ */
+class client extends \core\oauth2\client {
+
+    /**  @var \core\oauth2\issuer */
+    private $issuer;
+
+    /** @var string $clientid client identifier issued to the client */
+    private $clientid = '';
+
+    /** @var string $clientsecret The client secret. */
+    private $clientsecret = '';
+
+    /** @var moodle_url $returnurl URL to return to after authenticating */
+    private $returnurl = null;
+
+    /** @var string $grantscope */
+    protected $grantscope = '';
+
+    /** @var string $scope */
+    protected $scope = '';
+
+    /** @var bool basicauth */
+    protected $basicauth = true;
+
+    /** @var string|null backpack object */
+    public $backpack = '';
+
+    /**
+     * client constructor.
+     *
+     * @param issuer $issuer oauth2 service.
+     * @param string $returnurl return url after login
+     * @param string $additionalscopes the scopes has been granted
+     * @param null $backpack backpack object.
+     * @throws \coding_exception error message.
+     */
+    public function __construct(\core\oauth2\issuer $issuer, $returnurl = '', $additionalscopes = '',
+                                $backpack = null) {
+        $this->issuer = $issuer;
+        $this->clientid = $issuer->get('clientid');
+        $this->returnurl = $returnurl;
+        $this->clientsecret = $issuer->get('clientsecret');
+        $this->backpack = $backpack;
+        $this->grantscope = $additionalscopes;
+        $this->scope = $additionalscopes;
+        parent::__construct($issuer, $returnurl, $additionalscopes, false);
+    }
+
+    /**
+     * Get login url.
+     *
+     * @return moodle_url
+     * @throws \coding_exception
+     * @throws moodle_exception
+     */
+    public function get_login_url() {
+        $callbackurl = self::callback_url();
+        $scopes = $this->issuer->get('scopessupported');
+
+        // Removed the scopes does not support in authorization.
+        $excludescopes = ['profile', 'openid'];
+        $arrascopes = explode(' ', $scopes);
+        foreach ($excludescopes as $exscope) {
+            $key = array_search($exscope, $arrascopes);
+            if (isset($key)) {
+                unset($arrascopes[$key]);
+            }
+        }
+        $scopes = implode(' ', $arrascopes);
+
+        $params = array_merge(
+            [
+                'client_id' => $this->clientid,
+                'response_type' => 'code',
+                'redirect_uri' => $callbackurl->out(false),
+                'state' => $this->returnurl->out_as_local_url(false),
+                'scope' => $scopes,
+                'code_challenge' => $this->code_challenge(),
+                'code_challenge_method' => BACKPACK_CHALLENGE_METHOD,
+            ]
+        );
+        return new moodle_url($this->auth_url(), $params);
+    }
+
+    /**
+     * Generate code challenge.
+     *
+     * @return string
+     */
+    public function code_challenge() {
+        $random = bin2hex(openssl_random_pseudo_bytes(43));
+        $verifier = $this->base64url_encode(pack('H*', $random));
+        $challenge = $this->base64url_encode(pack('H*', hash('sha256', $verifier)));
+        $_SESSION['SESSION']->code_verifier = $verifier;
+        return $challenge;
+    }
+
+    /**
+     * Get code verifier.
+     *
+     * @return bool
+     */
+    public function code_verifier() {
+        if (isset($_SESSION['SESSION']) && !empty($_SESSION['SESSION']->code_verifier)) {
+            return $_SESSION['SESSION']->code_verifier;
+        }
+        return false;
+    }
+
+    /**
+     * Generate base64url encode.
+     *
+     * @param string $plaintext text to convert.
+     * @return string
+     */
+    public function base64url_encode($plaintext) {
+        $base64 = base64_encode($plaintext);
+        $base64 = trim($base64, "=");
+        $base64url = strtr($base64, '+/', '-_');
+        return ($base64url);
+    }
+
+    /**
+     * Callback url where the request is returned to.
+     *
+     * @return moodle_url url of callback
+     */
+    public static function callback_url() {
+        return new moodle_url('/badges/oauth2callback.php');
+    }
+
+    /**
+     * Check and refresh token to keep login on backpack site.
+     *
+     * @return bool
+     * @throws \coding_exception
+     * @throws moodle_exception
+     */
+    public function is_logged_in() {
+
+        // Has the token expired?
+        if (isset($this->accesstoken->expires) && time() >= $this->accesstoken->expires) {
+            if (isset($this->accesstoken->refreshtoken)) {
+                return $this->upgrade_token($this->accesstoken->refreshtoken, 'refresh_token');
+            } else {
+                throw new moodle_exception('Could not refresh oauth token, please try again.');
+            }
+        }
+
+        if (isset($this->accesstoken->token) && isset($this->accesstoken->scope)) {
+            return true;
+        }
+
+        // If we've been passed then authorization code generated by the
+        // authorization server try and upgrade the token to an access token.
+        $code = optional_param('oauth2code', null, PARAM_RAW);
+        // Note - sometimes we may call is_logged_in twice in the same request - we don't want to attempt
+        // to upgrade the same token twice.
+        if ($code && $this->upgrade_token($code, 'authorization_code')) {
+            return true;
+        }
+
+        return false;
+    }
+
+    /**
+     * Request new token.
+     *
+     * @param string $code code verify from Auth site.
+     * @param string $granttype grant type.
+     * @return bool
+     * @throws moodle_exception
+     */
+    public function upgrade_token($code, $granttype = 'authorization_code') {
+        $callbackurl = self::callback_url();
+
+        if ($granttype == 'authorization_code') {
+            $params = array('code' => $code,
+                'grant_type' => $granttype,
+                'redirect_uri' => $callbackurl->out(false),
+                'scope' => $this->get_scopes(),
+                'code_verifier' => $this->code_verifier()
+            );
+        } else if ($granttype == 'refresh_token') {
+            $this->basicauth = false;
+            $params = array('refresh_token' => $code,
+                'grant_type' => $granttype,
+                'scope' => $this->get_scopes(),
+            );
+        }
+        if ($this->basicauth) {
+            $idsecret = urlencode($this->clientid) . ':' . urlencode($this->clientsecret);
+            $this->setHeader('Authorization: Basic ' . base64_encode($idsecret));
+        } else {
+            $params['client_id'] = $this->clientid;
+            $params['client_secret'] = $this->clientsecret;
+        }
+        // Requests can either use http GET or POST.
+        $response = $this->post($this->token_url(), $this->build_post_data($params));
+        $r = json_decode($response);
+        if ($this->info['http_code'] !== 200) {
+            throw new moodle_exception('Could not upgrade oauth token');
+        }
+
+        if (is_null($r)) {
+            throw new moodle_exception("Could not decode JSON token response");
+        }
+
+        if (!empty($r->error)) {
+            throw new moodle_exception($r->error . ' ' . $r->error_description);
+        }
+
+        if (!isset($r->access_token)) {
+            return false;
+        }
+
+        // Store the token an expiry time.
+        $accesstoken = new stdClass;
+        $accesstoken->token = $r->access_token;
+        if (isset($r->expires_in)) {
+            // Expires 10 seconds before actual expiry.
+            $accesstoken->expires = (time() + ($r->expires_in - 10));
+        }
+        if (isset($r->refresh_token)) {
+            $this->refreshtoken = $r->refresh_token;
+            $accesstoken->refreshtoken = $r->refresh_token;
+        }
+        $accesstoken->scope = $r->scope;
+
+        // Also add the scopes.
+        $this->store_token($accesstoken);
+
+        return true;
+    }
+
+    /**
+     * Store a token to verify for send request.
+     *
+     * @param null|stdClass $token
+     */
+    protected function store_token($token) {
+        global $USER;
+
+        $this->accesstoken = $token;
+        // Create or update a DB record with the new token.
+        $persistedtoken = badge_backpack_oauth2::get_record(['externalbackpackid' => $this->backpack->id, 'userid' => $USER->id]);
+        if ($token !== null) {
+            if (!$persistedtoken) {
+                $persistedtoken = new badge_backpack_oauth2();
+                $persistedtoken->set('issuerid', $this->backpack->oauth2_issuerid);
+                $persistedtoken->set('externalbackpackid', $this->backpack->id);
+                $persistedtoken->set('userid', $USER->id);
+            } else {
+                $persistedtoken->set('timemodified', time());
+            }
+            // Update values from $token. Don't use from_record because that would skip validation.
+            $persistedtoken->set('usermodified', $USER->id);
+            $persistedtoken->set('token', $token->token);
+            $persistedtoken->set('refreshtoken', $token->refreshtoken);
+            $persistedtoken->set('expires', $token->expires);
+            $persistedtoken->set('scope', $token->scope);
+            $persistedtoken->save();
+        } else {
+            if ($persistedtoken) {
+                $persistedtoken->delete();
+            }
+        }
+    }
+
+    /**
+     * Get token of current user.
+     *
+     * @return stdClass|null token object
+     */
+    protected function get_stored_token() {
+        global $USER;
+
+        $token = badge_backpack_oauth2::get_record(['externalbackpackid' => $this->backpack->id, 'userid' => $USER->id]);
+        if ($token !== false) {
+            $token = $token->to_record();
+            return $token;
+        }
+        return null;
+    }
+
+    /**
+     * Get scopes granted.
+     *
+     * @return null|string
+     */
+    protected function get_scopes() {
+        if (!empty($this->grantscope)) {
+            return $this->grantscope;
+        }
+        $token = $this->get_stored_token();
+        if ($token) {
+            return $token->scope;
+        }
+        return null;
+    }
+}
index aa9592f..f9258b3 100644 (file)
@@ -56,6 +56,10 @@ class external_backpacks_page implements \renderable {
      * @return stdClass
      */
     public function export_for_template(\renderer_base $output) {
+        global $CFG, $PAGE;
+
+        $PAGE->requires->js_call_amd('core_badges/backpackactions', 'init');
+
         $data = new \stdClass();
         $data->baseurl = $this->url;
         $data->backpacks = array();
@@ -63,11 +67,13 @@ class external_backpacks_page implements \renderable {
         foreach ($this->backpacks as $backpack) {
             $exporter = new backpack_exporter($backpack);
             $backpack = $exporter->export($output);
-            if ($backpack->apiversion == OPEN_BADGES_V2) {
+            if ($backpack->apiversion == OPEN_BADGES_V2 || $backpack->apiversion == OPEN_BADGES_V2P1) {
                 $backpack->canedit = true;
             } else {
                 $backpack->canedit = false;
             }
+            $backpack->iscurrent = ($backpack->id == $CFG->badges_site_backpack);
+
             $data->backpacks[] = $backpack;
         }
         $data->warning = badges_verify_site_backpack();
index cd0c010..06dfd7a 100644 (file)
@@ -105,6 +105,14 @@ class provider implements
             'issuer' => 'privacy:metadata:external:backpacks:issuer',
         ], 'privacy:metadata:external:backpacks');
 
+        $collection->add_database_table('badge_backpack_oauth2', [
+            'userid' => 'privacy:metadata:backpackoauth2:userid',
+            'usermodified' => 'privacy:metadata:backpackoauth2:usermodified',
+            'token' => 'privacy:metadata:backpackoauth2:token',
+            'issuerid' => 'privacy:metadata:backpackoauth2:issuerid',
+            'scope' => 'privacy:metadata:backpackoauth2:scope',
+        ], 'privacy:metadata:backpackoauth2');
+
         return $collection;
     }
 
index bcb9482..61dd374 100644 (file)
@@ -56,10 +56,17 @@ $badgescache = cache::make('core', 'externalbadges');
 if ($disconnect && $backpack) {
     require_sesskey();
     $sitebackpack = badges_get_site_backpack($backpack->externalbackpackid);
-    // If backpack is connected, need to select collections.
-    $bp = new \core_badges\backpack_api($sitebackpack, $backpack);
-    $bp->disconnect_backpack($USER->id, $backpack->id);
-    redirect(new moodle_url('/badges/mybackpack.php'));
+    if ($sitebackpack->apiversion == OPEN_BADGES_V2P1) {
+        $bp = new \core_badges\backpack_api2p1($sitebackpack);
+        $bp->disconnect_backpack($backpack);
+        redirect(new moodle_url('/badges/mybackpack.php'), get_string('backpackdisconnected', 'badges'), null,
+            \core\output\notification::NOTIFY_SUCCESS);
+    } else {
+        // If backpack is connected, need to select collections.
+        $bp = new \core_badges\backpack_api($sitebackpack, $backpack);
+        $bp->disconnect_backpack($USER->id, $backpack->id);
+        redirect(new moodle_url('/badges/mybackpack.php'));
+    }
 }
 $warning = '';
 if ($backpack) {
@@ -100,6 +107,16 @@ if ($backpack) {
         $bp->set_backpack_collections($backpack->id, $groups);
         redirect(new moodle_url('/badges/mybadges.php'));
     }
+} else if (badges_open_badges_backpack_api() == OPEN_BADGES_V2P1) {
+    // If backpack is version 2.1 to redirect on the backpack site to login.
+    // User input username/email/password on the backpack site
+    // After confirm the scopes.
+    $form = new \core_badges\form\backpack(new moodle_url('/badges/mybackpack.php'));
+    if ($form->is_cancelled()) {
+        redirect(new moodle_url('/badges/mybadges.php'));
+    } else if ($data = $form->get_submitted_data()) {
+        redirect(new moodle_url('/badges/backpack-connect.php'));
+    }
 } else {
     // If backpack is not connected, need to connect first.
     // To create a new connection to the backpack, first we need to verify the user's email address:
diff --git a/badges/oauth2callback.php b/badges/oauth2callback.php
new file mode 100644 (file)
index 0000000..b3b8153
--- /dev/null
@@ -0,0 +1,57 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * Verify authorization callback.
+ *
+ * @package    core_badges
+ * @copyright  2020 Tung Thai
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ * @author     Tung Thai <Tung.ThaiDuc@nashtechglobal.com>
+ */
+require_once(__DIR__ . '/../config.php');
+
+$error = optional_param('error', '', PARAM_RAW);
+
+if ($error) {
+    $message = optional_param('error_description', '', PARAM_RAW);
+    if ($message) {
+        print_error($message);
+    } else {
+        print_error($error);
+    }
+    die();
+}
+
+require_login();
+
+// The authorization code generated by the authorization server.
+$code = required_param('code', PARAM_RAW);
+$scope = required_param('scope', PARAM_RAW);
+
+// The state parameter we've given (used in moodle as a redirect url).
+$state = required_param('state', PARAM_LOCALURL);
+
+$redirecturl = new moodle_url($state);
+$params = $redirecturl->params();
+
+if (isset($params['sesskey']) and confirm_sesskey($params['sesskey'])) {
+    $redirecturl->param('oauth2code', $code);
+    $redirecturl->param('scope', $scope);
+    redirect($redirecturl);
+} else {
+    print_error('invalidsesskey');
+}
index d3c2a6b..2f7bad8 100644 (file)
@@ -87,6 +87,8 @@ class core_badges_renderer extends plugin_renderer_base {
                     if (badges_open_badges_backpack_api() == OPEN_BADGES_V1) {
                         $action = new component_action('click', 'addtobackpack', array('assertion' => $assertion->out(false)));
                         $addurl = new moodle_url('#');
+                    } else if (badges_open_badges_backpack_api() == OPEN_BADGES_V2P1) {
+                        $addurl = new moodle_url('/badges/backpack-export.php', array('hash' => $badge->uniquehash));
                     } else {
                         $addurl = new moodle_url('/badges/backpack-add.php', array('hash' => $badge->uniquehash));
                     }
@@ -354,7 +356,11 @@ class core_badges_renderer extends plugin_renderer_base {
                     $this->output->add_action_handler($action, 'addbutton');
                     $output .= $tobackpack;
                 } else {
-                    $assertion = new moodle_url('/badges/backpack-add.php', array('hash' => $ibadge->hash));
+                    if (badges_open_badges_backpack_api() == OPEN_BADGES_V2P1) {
+                        $assertion = new moodle_url('/badges/backpack-export.php', array('hash' => $ibadge->hash));
+                    } else {
+                        $assertion = new moodle_url('/badges/backpack-add.php', array('hash' => $ibadge->hash));
+                    }
                     $attributes = ['class' => 'btn btn-secondary m-1', 'role' => 'button'];
                     $tobackpack = html_writer::link($assertion, get_string('addtobackpack', 'badges'), $attributes);
                     $output .= $tobackpack;
index 2d84733..aab0f2f 100644 (file)
         "warning": "<span class='text-warning'>Could not login</span>"
     }
 }}
-<table class="generaltable fullwidth">
+
+<form action="{{baseurl}}" method="get" id="createbackpack">
+   <input type="hidden" name="action" value="edit"/>
+   <button type="submit" class="btn btn-secondary">{{#str}}newbackpack, core_badges{{/str}}</button>
+</form>
+
+<table class="generaltable fullwidth" id="backpacklist">
     <caption>{{#str}}listbackpacks, core_badges{{/str}}</caption>
     <thead>
         <tr>
     </thead>
     <tbody>
         {{#backpacks}}
-        <tr>
+        <tr data-backpackurl="{{{backpackweburl}}}">
             <td> {{{backpackweburl}}} </td>
             <td> {{#sitebackpack}}Yes{{/sitebackpack}} </td>
             <td>
             {{#canedit}}
-                <a href="{{baseurl}}?id={{id}}&action=edit">
-                    {{#str}}editsettings, core_badges{{/str}}
-                </a>
+                <a href="{{baseurl}}?id={{id}}&action=edit">{{#pix}}t/edit, core,{{#str}}editsettings{{/str}}{{/pix}}</a>
             {{/canedit}}
+            {{^iscurrent}}
+                <a href="{{baseurl}}?id={{id}}&action=delete" role="button" data-action="deletebackpack">
+                    {{#pix}}t/delete, core,{{#str}}delete{{/str}}{{/pix}}
+                </a>
+            {{/iscurrent}}
             </td>
         </tr>
         {{/backpacks}}
index d3d3541..596c67e 100644 (file)
@@ -30,6 +30,8 @@ global $CFG;
 require_once($CFG->libdir . '/badgeslib.php');
 require_once($CFG->dirroot . '/badges/lib.php');
 
+use core_badges\helper;
+
 class core_badges_badgeslib_testcase extends advanced_testcase {
     protected $badgeid;
     protected $course;
@@ -855,4 +857,58 @@ class core_badges_badgeslib_testcase extends advanced_testcase {
         $badge->delete_alignment($alignments1[$newid2]->id);
         $this->assertCount(1, $badge->get_alignments());
     }
+
+    /**
+     * Test badges_delete_site_backpack().
+     *
+     */
+    public function test_badges_delete_site_backpack(): void {
+        global $DB;
+
+        $this->setAdminUser();
+
+        // Create one backpack.
+        $total = $DB->count_records('badge_external_backpack');
+        $this->assertEquals(1, $total);
+
+        $data = new \stdClass();
+        $data->apiversion = OPEN_BADGES_V2P1;
+        $data->backpackapiurl = 'https://dc.imsglobal.org/obchost/ims/ob/v2p1';
+        $data->backpackweburl = 'https://dc.imsglobal.org';
+        badges_create_site_backpack($data);
+        $backpack = $DB->get_record('badge_external_backpack', ['backpackweburl' => $data->backpackweburl]);
+        $user1 = $this->getDataGenerator()->create_user();
+        $user2 = $this->getDataGenerator()->create_user();
+        // User1 is connected to the backpack to be removed and has 2 collections.
+        $backpackuser1 = helper::create_fake_backpack(['userid' => $user1->id, 'externalbackpackid' => $backpack->id]);
+        helper::create_fake_backpack_collection(['backpackid' => $backpackuser1->id]);
+        helper::create_fake_backpack_collection(['backpackid' => $backpackuser1->id]);
+        // User2 is connected to a different backpack and has 1 collection.
+        $backpackuser2 = helper::create_fake_backpack(['userid' => $user2->id]);
+        helper::create_fake_backpack_collection(['backpackid' => $backpackuser2->id]);
+
+        $total = $DB->count_records('badge_external_backpack');
+        $this->assertEquals(2, $total);
+        $total = $DB->count_records('badge_backpack');
+        $this->assertEquals(2, $total);
+        $total = $DB->count_records('badge_external');
+        $this->assertEquals(3, $total);
+
+        // Remove the backpack created previously.
+        $result = badges_delete_site_backpack($backpack->id);
+        $this->assertTrue($result);
+
+        $total = $DB->count_records('badge_external_backpack');
+        $this->assertEquals(1, $total);
+
+        $total = $DB->count_records('badge_backpack');
+        $this->assertEquals(1, $total);
+
+        $total = $DB->count_records('badge_external');
+        $this->assertEquals(1, $total);
+
+        // Try to remove an non-existent backpack.
+        $result = badges_delete_site_backpack($backpack->id);
+        $this->assertFalse($result);
+    }
 }
diff --git a/badges/tests/behat/backpack.feature b/badges/tests/behat/backpack.feature
new file mode 100644 (file)
index 0000000..3cb8e78
--- /dev/null
@@ -0,0 +1,121 @@
+@core @core_badges @_file_upload
+Feature: Backpack badges
+  The settings to connect to backpack with OAuth2 service
+  As an learner
+  I need to verify display backpack in the my profile
+
+  Background:
+    Given the following "badge external backpack" exist:
+      | backpackapiurl                               | backpackweburl           | apiversion |
+      | https://dc.imsglobal.org/obchost/ims/ob/v2p1 | https://dc.imsglobal.org | 2.1          |
+    And the following "users" exist:
+      | username | firstname | lastname | email                |
+      | student1 | Student   | 1        | student1@example.com |
+
+  @javascript
+  Scenario: Verify backback settings
+    Given I am on homepage
+    And I log in as "admin"
+    And I navigate to "Badges > Backpack settings" in site administration
+    And I set the following fields to these values:
+      | External backpack connection | 1                        |
+      | Active external backpack     | https://dc.imsglobal.org |
+    And I press "Save changes"
+    And I navigate to "Badges > Add a new badge" in site administration
+    And I set the following fields to these values:
+      | Name          | Test badge verify backpack |
+      | Version       | v1                         |
+      | Language      | English                    |
+      | Description   | Test badge description     |
+      | Image author  | http://author.example.com  |
+      | Image caption | Test caption image         |
+      | issuername    | Test Badge Site            |
+      | issuercontact | testuser@example.com       |
+    And I upload "badges/tests/behat/badge.png" file to "Image" filemanager
+    And I press "Create badge"
+    And I set the field "type" to "Manual issue by role"
+    And I set the field "Manager" to "1"
+    And I press "Save"
+    And I press "Enable access"
+    And I press "Continue"
+    And I follow "Recipients (0)"
+    And I press "Award badge"
+    And I set the field "potentialrecipients[]" to "Student 1 (student1@example.com)"
+    And I press "Award badge"
+    And I log out
+    When I am on homepage
+    And I log in as "student1"
+    And I follow "Preferences" in the user menu
+    And I follow "Backpack settings"
+    Then I should see "https://dc.imsglobal.org"
+    And I should see "Not connected"
+
+  @javascript
+  Scenario: User has been connected backpack
+    Given I am on homepage
+    And I log in as "admin"
+    And I navigate to "Badges > Backpack settings" in site administration
+    And I set the following fields to these values:
+      | External backpack connection | 1                        |
+      | Active external backpack     | https://dc.imsglobal.org |
+    And I press "Save changes"
+    And I navigate to "Badges > Add a new badge" in site administration
+    And I set the following fields to these values:
+      | Name           | Test badge verify backpack |
+      | Version        | v1                         |
+      | Language       | English                    |
+      | Description    | Test badge description     |
+      | Image author   | http://author.example.com  |
+      | Image caption  | Test caption image         |
+      | issuername     | Test Badge Site            |
+      | issuercontact  | testuser@example.com       |
+    And I upload "badges/tests/behat/badge.png" file to "Image" filemanager
+    And I press "Create badge"
+    And I set the field "type" to "Manual issue by role"
+    And I set the field "Manager" to "1"
+    And I press "Save"
+    And I press "Enable access"
+    And I press "Continue"
+    And I follow "Recipients (0)"
+    And I press "Award badge"
+    And I set the field "potentialrecipients[]" to "Student 1 (student1@example.com)"
+    And I press "Award badge"
+    And I log out
+    And the following "setup backpack connected" exist:
+      | user     | externalbackpack         |
+      | student1 | https://dc.imsglobal.org |
+    When I log in as "student1"
+    And I follow "Preferences" in the user menu
+    And I follow "Backpack settings"
+    Then I should see "Connected"
+    And I follow "Preferences" in the user menu
+    And I follow "Manage badges"
+    And I should see "Test badge verify backpack"
+    And "Add to backpack" "link" should exist
+
+  @javascript
+  Scenario: Add a new site backpack
+    Given I am on homepage
+    And I log in as "admin"
+    And I navigate to "Badges > Manage backpacks" in site administration
+    When I press "Add a new backpack"
+    And I set the field "backpackapiurl" to "http://backpackapiurl.cat"
+    And I set the field "backpackweburl" to "aaa"
+    And I press "Save changes"
+    And I should see "Invalid URL"
+    And I set the field "backpackweburl" to "http://backpackweburl.cat"
+    And I press "Save changes"
+    Then I should see "http://backpackweburl.cat"
+    And "Delete" "button" should exist
+
+  @javascript
+  Scenario: Remove a site backpack
+    Given I am on homepage
+    And I log in as "admin"
+    And I navigate to "Badges > Manage backpacks" in site administration
+    When I click on "Delete" "link" in the "https://dc.imsglobal.org" "table_row"
+    And I should see "Delete site backpack 'https://dc.imsglobal.org'?"
+    And I click on "Delete" "button" in the "Delete site backpack" "dialogue"
+    Then I should see "The site backpack has been deleted."
+    And I should not see "https://dc.imsglobal.org"
+    And "Delete" "button" should not exist