Merge branch 'MDL-70412-master' of git://github.com/aanabit/moodle
authorAndrew Nicols <andrew@nicols.co.uk>
Thu, 22 Apr 2021 02:10:38 +0000 (10:10 +0800)
committerAndrew Nicols <andrew@nicols.co.uk>
Thu, 22 Apr 2021 02:10:38 +0000 (10:10 +0800)
538 files changed:
.eslintignore
.github/workflows/push.yml
.grunt/tasks/ignorefiles.js
.stylelintignore
.travis.yml
admin/auth_config.php
admin/message.php
admin/oauth2callback.php
admin/purgecaches.php
admin/settings/courses.php
admin/settings/users.php
admin/tool/dataprivacy/amd/build/contactdpo.min.js [new file with mode: 0644]
admin/tool/dataprivacy/amd/build/contactdpo.min.js.map [new file with mode: 0644]
admin/tool/dataprivacy/amd/build/myrequestactions.min.js
admin/tool/dataprivacy/amd/build/myrequestactions.min.js.map
admin/tool/dataprivacy/amd/src/contactdpo.js [new file with mode: 0644]
admin/tool/dataprivacy/amd/src/myrequestactions.js
admin/tool/dataprivacy/classes/external.php
admin/tool/dataprivacy/classes/form/contactdpo.php [new file with mode: 0644]
admin/tool/dataprivacy/classes/output/renderer.php
admin/tool/dataprivacy/lang/en/tool_dataprivacy.php
admin/tool/dataprivacy/lib.php
admin/tool/dataprivacy/templates/contact_dpo.mustache [deleted file]
admin/tool/dataprivacy/tests/behat/contact_privacy_officer.feature
admin/tool/dataprivacy/tests/behat/my_data_requests.feature [new file with mode: 0644]
admin/tool/lp/templates/user_competency_summary.mustache
admin/tool/oauth2/classes/form/issuer.php
admin/tool/oauth2/classes/output/renderer.php
admin/tool/oauth2/issuers.php
admin/tool/oauth2/lang/en/tool_oauth2.php
admin/tool/oauth2/pix/notconfigured.svg [new file with mode: 0644]
admin/tool/oauth2/tests/behat/basic_settings.feature
admin/tool/uploaduser/classes/process.php
admin/tool/uploaduser/user_form.php
admin/tool/usertours/classes/manager.php
admin/user/user_bulk_download.php
analytics/classes/prediction.php
analytics/tests/prediction_actions_test.php
analytics/upgrade.txt
auth/mnet/classes/privacy/provider.php
auth/mnet/lang/en/auth_mnet.php
auth/oauth2/classes/api.php
auth/oauth2/classes/auth.php
auth/oauth2/classes/output/renderer.php
auth/oauth2/linkedlogins.php
auth/oauth2/login.php
availability/condition/profile/classes/frontend.php
backup/moodle2/backup_course_task.class.php
backup/moodle2/backup_root_task.class.php
backup/moodle2/backup_settingslib.php
backup/moodle2/backup_stepslib.php
backup/moodle2/restore_final_task.class.php
backup/moodle2/restore_root_task.class.php
backup/moodle2/restore_settingslib.php
backup/moodle2/restore_stepslib.php
backup/tests/backup_cleanup_task_test.php
backup/tests/backup_restore_base_testcase.php [new file with mode: 0644]
backup/tests/backup_restore_permission_test.php [new file with mode: 0644]
backup/upgrade.txt
backup/util/dbops/backup_controller_dbops.class.php
backup/util/dbops/restore_controller_dbops.class.php
backup/util/helper/backup_anonymizer_helper.class.php
backup/util/ui/tests/behat/import_contentbank_content.feature
backup/util/ui/tests/behat/import_course.feature
backup/util/ui/tests/behat/restore_moodle2_courses.feature
badges/backpack-connect.php
badges/classes/oauth2/client.php
badges/criteria/award_criteria_profile.php
badges/renderer.php
badges/tests/badgeslib_test.php
blocks/myprofile/classes/output/myprofile.php
blocks/myprofile/edit_form.php
blocks/myprofile/lang/en/block_myprofile.php
blocks/myprofile/templates/myprofile.mustache
blocks/myprofile/tests/behat/block_myprofile.feature
completion/criteria/completion_criteria_grade.php
contentbank/amd/build/upload.min.js [new file with mode: 0644]
contentbank/amd/build/upload.min.js.map [new file with mode: 0644]
contentbank/amd/src/upload.js [new file with mode: 0644]
contentbank/classes/form/upload_files.php [new file with mode: 0644]
contentbank/contenttype/h5p/classes/content.php
contentbank/contenttype/h5p/classes/contenttype.php
contentbank/contenttype/h5p/tests/behat/admin_replace_content.feature
contentbank/contenttype/h5p/tests/behat/admin_upload_content.feature
contentbank/contenttype/h5p/tests/behat/disable_contenttypes.feature [new file with mode: 0644]
contentbank/contenttype/h5p/tests/behat/manage_content.feature
contentbank/contenttype/h5p/tests/behat/teacher_replace_content.feature
contentbank/contenttype/h5p/tests/behat/teacher_upload_content.feature
contentbank/contenttype/h5p/tests/content_h5p_test.php
contentbank/files_form.php [deleted file]
contentbank/index.php
contentbank/templates/bankcontent/toolbar.mustache
contentbank/tests/behat/delete_content.feature
contentbank/tests/behat/download_content.feature
contentbank/tests/behat/edit_content.feature
contentbank/tests/behat/events.feature
contentbank/tests/behat/search_content.feature
contentbank/tests/behat/sort_content.feature
contentbank/tests/behat/view_preferences.feature
contentbank/tests/behat/visibility.feature
contentbank/tests/contentbank_test.php
contentbank/upgrade.txt
contentbank/upload.php [deleted file]
contentbank/view.php
course/classes/output/section_format/cmitem.php
course/moodleform_mod.php
enrol/externallib.php
enrol/self/tests/behat/self_enrolment.feature
enrol/tests/enrollib_test.php
filter/displayh5p/tests/behat/h5p_filter.feature
grade/classes/external/create_gradecategories.php [new file with mode: 0644]
grade/grading/form/guide/db/services.php
grade/grading/form/rubric/db/services.php
grade/grading/manage.php
grade/grading/tests/behat/behat_grading.php
grade/import/csv/classes/load_data.php
grade/report/grader/lib.php
grade/report/overview/lib.php
grade/report/user/externallib.php
grade/report/user/lib.php
grade/tests/external/create_gradecategories_test.php [new file with mode: 0644]
group/index.php
group/lib.php
group/overview.php
group/tests/behat/custom_fields.feature [new file with mode: 0644]
group/tests/lib_test.php
h5p/classes/api.php
h5p/classes/core.php
h5p/classes/editor_ajax.php
h5p/classes/framework.php
h5p/classes/helper.php
h5p/classes/local/library/handler.php
h5p/classes/output/libraries.php
h5p/classes/player.php
h5p/libraries.php
h5p/templates/h5plibraries.mustache
h5p/tests/api_test.php
h5p/tests/behat/h5p_libraries.feature
h5p/tests/editor_framework_test.php
h5p/tests/generator/lib.php
h5p/tests/generator_test.php
h5p/upgrade.txt
install/lang/da_wp/langconfig.php [new file with mode: 0644]
install/lang/id/error.php [new file with mode: 0644]
install/lang/mk/install.php
install/lang/mk/langconfig.php
install/lang/vi/moodle.php
lang/en/admin.php
lang/en/backup.php
lang/en/contentbank.php
lang/en/deprecated.txt
lang/en/h5p.php
lang/en/moodle.php
lib/adodb/LICENSE.md
lib/adodb/README.md
lib/adodb/adodb-active-record.inc.php
lib/adodb/adodb-active-recordx.inc.php
lib/adodb/adodb-csvlib.inc.php
lib/adodb/adodb-datadict.inc.php
lib/adodb/adodb-error.inc.php
lib/adodb/adodb-errorhandler.inc.php
lib/adodb/adodb-errorpear.inc.php
lib/adodb/adodb-exceptions.inc.php
lib/adodb/adodb-iterator.inc.php
lib/adodb/adodb-lib.inc.php
lib/adodb/adodb-loadbalancer.inc.php [new file with mode: 0644]
lib/adodb/adodb-memcache.lib.inc.php
lib/adodb/adodb-pager.inc.php
lib/adodb/adodb-pear.inc.php
lib/adodb/adodb-perf.inc.php
lib/adodb/adodb-php4.inc.php
lib/adodb/adodb-time.inc.php
lib/adodb/adodb-xmlschema.inc.php
lib/adodb/adodb-xmlschema03.inc.php
lib/adodb/adodb.inc.php
lib/adodb/datadict/datadict-access.inc.php
lib/adodb/datadict/datadict-db2.inc.php
lib/adodb/datadict/datadict-firebird.inc.php
lib/adodb/datadict/datadict-generic.inc.php
lib/adodb/datadict/datadict-ibase.inc.php
lib/adodb/datadict/datadict-informix.inc.php
lib/adodb/datadict/datadict-mssql.inc.php
lib/adodb/datadict/datadict-mssqlnative.inc.php
lib/adodb/datadict/datadict-mysql.inc.php
lib/adodb/datadict/datadict-oci8.inc.php
lib/adodb/datadict/datadict-postgres.inc.php
lib/adodb/datadict/datadict-sapdb.inc.php
lib/adodb/datadict/datadict-sqlite.inc.php
lib/adodb/datadict/datadict-sybase.inc.php
lib/adodb/drivers/adodb-access.inc.php
lib/adodb/drivers/adodb-ado.inc.php
lib/adodb/drivers/adodb-ado5.inc.php
lib/adodb/drivers/adodb-ado_access.inc.php
lib/adodb/drivers/adodb-ado_mssql.inc.php
lib/adodb/drivers/adodb-ads.inc.php
lib/adodb/drivers/adodb-borland_ibase.inc.php
lib/adodb/drivers/adodb-csv.inc.php
lib/adodb/drivers/adodb-db2.inc.php
lib/adodb/drivers/adodb-db2oci.inc.php
lib/adodb/drivers/adodb-db2ora.inc.php
lib/adodb/drivers/adodb-fbsql.inc.php
lib/adodb/drivers/adodb-firebird.inc.php
lib/adodb/drivers/adodb-ibase.inc.php
lib/adodb/drivers/adodb-informix.inc.php
lib/adodb/drivers/adodb-informix72.inc.php
lib/adodb/drivers/adodb-ldap.inc.php
lib/adodb/drivers/adodb-mssql.inc.php
lib/adodb/drivers/adodb-mssql_n.inc.php
lib/adodb/drivers/adodb-mssqlnative.inc.php
lib/adodb/drivers/adodb-mssqlpo.inc.php
lib/adodb/drivers/adodb-mysql.inc.php
lib/adodb/drivers/adodb-mysqli.inc.php
lib/adodb/drivers/adodb-mysqlpo.inc.php
lib/adodb/drivers/adodb-mysqlt.inc.php
lib/adodb/drivers/adodb-netezza.inc.php
lib/adodb/drivers/adodb-oci8.inc.php
lib/adodb/drivers/adodb-oci805.inc.php
lib/adodb/drivers/adodb-oci8po.inc.php
lib/adodb/drivers/adodb-oci8quercus.inc.php
lib/adodb/drivers/adodb-odbc.inc.php
lib/adodb/drivers/adodb-odbc_db2.inc.php
lib/adodb/drivers/adodb-odbc_mssql.inc.php
lib/adodb/drivers/adodb-odbc_mssql2012.inc.php [new file with mode: 0644]
lib/adodb/drivers/adodb-odbc_oracle.inc.php
lib/adodb/drivers/adodb-odbtp.inc.php
lib/adodb/drivers/adodb-odbtp_unicode.inc.php
lib/adodb/drivers/adodb-oracle.inc.php
lib/adodb/drivers/adodb-pdo.inc.php
lib/adodb/drivers/adodb-pdo_dblib.inc.php [new file with mode: 0644]
lib/adodb/drivers/adodb-pdo_firebird.inc.php [new file with mode: 0644]
lib/adodb/drivers/adodb-pdo_mssql.inc.php
lib/adodb/drivers/adodb-pdo_mysql.inc.php
lib/adodb/drivers/adodb-pdo_oci.inc.php
lib/adodb/drivers/adodb-pdo_pgsql.inc.php
lib/adodb/drivers/adodb-pdo_sqlite.inc.php
lib/adodb/drivers/adodb-pdo_sqlsrv.inc.php
lib/adodb/drivers/adodb-postgres.inc.php
lib/adodb/drivers/adodb-postgres64.inc.php
lib/adodb/drivers/adodb-postgres7.inc.php
lib/adodb/drivers/adodb-postgres8.inc.php
lib/adodb/drivers/adodb-postgres9.inc.php
lib/adodb/drivers/adodb-proxy.inc.php
lib/adodb/drivers/adodb-sapdb.inc.php
lib/adodb/drivers/adodb-sqlanywhere.inc.php
lib/adodb/drivers/adodb-sqlite.inc.php
lib/adodb/drivers/adodb-sqlite3.inc.php
lib/adodb/drivers/adodb-sqlitepo.inc.php
lib/adodb/drivers/adodb-sybase.inc.php
lib/adodb/drivers/adodb-sybase_ase.inc.php
lib/adodb/drivers/adodb-text.inc.php [new file with mode: 0644]
lib/adodb/drivers/adodb-vfp.inc.php
lib/adodb/perf/perf-db2.inc.php
lib/adodb/perf/perf-informix.inc.php
lib/adodb/perf/perf-mssql.inc.php
lib/adodb/perf/perf-mssqlnative.inc.php
lib/adodb/perf/perf-mysql.inc.php
lib/adodb/perf/perf-oci8.inc.php
lib/adodb/perf/perf-postgres.inc.php
lib/adodb/pivottable.inc.php
lib/adodb/readme_moodle.txt
lib/adodb/rsfilter.inc.php
lib/adodb/toexport.inc.php
lib/adodb/tohtml.inc.php
lib/adodb/xmlschema03.dtd
lib/amd/build/toast.min.js.map
lib/amd/src/toast.js
lib/antivirus/clamav/settings.php
lib/authlib.php
lib/classes/event/base.php
lib/classes/grades_external.php
lib/classes/navigation/views/primary.php [new file with mode: 0644]
lib/classes/oauth2/api.php
lib/classes/oauth2/discovery/openidconnect.php
lib/classes/oauth2/issuer.php
lib/classes/oauth2/refresh_system_tokens_task.php
lib/classes/oauth2/service/facebook.php [new file with mode: 0644]
lib/classes/oauth2/service/google.php
lib/classes/oauth2/service/imsobv2p1.php
lib/classes/oauth2/service/microsoft.php [new file with mode: 0644]
lib/classes/oauth2/service/nextcloud.php [new file with mode: 0644]
lib/classes/user.php
lib/datalib.php
lib/db/install.xml
lib/db/services.php
lib/db/upgrade.php
lib/enrollib.php
lib/filelib.php
lib/form/filemanager.js
lib/grade/grade_item.php
lib/moodlelib.php
lib/myprofilelib.php
lib/navigationlib.php
lib/outputrenderers.php
lib/pagelib.php
lib/plagiarismlib.php
lib/setup.php
lib/setuplib.php
lib/templates/local/toast/message.mustache
lib/tests/behat/datetime_any.feature
lib/tests/completion_daily_task_test.php
lib/tests/datalib_test.php
lib/tests/event/contentbank_content_uploaded_test.php
lib/tests/myprofilelib_test.php
lib/tests/navigation/views/primary_test.php [new file with mode: 0644]
lib/tests/oauth2_test.php
lib/tests/task_database_logger_test.php
lib/thirdpartylibs.xml
lib/upgrade.txt
lib/upgradelib.php
media/player/videojs/db/services.php
message/output/airnotifier/checkconfiguration.php [new file with mode: 0644]
message/output/airnotifier/classes/manager.php
message/output/airnotifier/lang/en/message_airnotifier.php
message/output/airnotifier/settings.php
message/output/airnotifier/tests/manager_test.php [new file with mode: 0644]
mod/assign/externallib.php
mod/assign/feedback/offline/importgradeslib.php
mod/assign/gradingtable.php
mod/assign/lib.php
mod/assign/locallib.php
mod/assign/renderer.php
mod/assign/tests/lib_test.php
mod/assign/tests/locallib_participants_test.php
mod/assign/tests/locallib_test.php
mod/data/lib.php
mod/forum/classes/task/send_user_notifications.php
mod/forum/deprecatedlib.php
mod/glossary/db/services.php
mod/glossary/lib.php
mod/h5pactivity/db/services.php
mod/h5pactivity/tests/behat/result_longfillin.feature
mod/label/view.php
mod/lesson/index.php
mod/lesson/lib.php
mod/lesson/settings.php
mod/lti/OAuth.php
mod/lti/amd/build/tool_configure_controller.min.js
mod/lti/amd/build/tool_configure_controller.min.js.map
mod/lti/amd/src/tool_configure_controller.js
mod/lti/classes/local/ltiopenid/registration_helper.php
mod/lti/classes/output/registration_upgrade_choice_page.php [new file with mode: 0644]
mod/lti/classes/output/renderer.php
mod/lti/lang/en/lti.php
mod/lti/locallib.php
mod/lti/openid-configuration.php
mod/lti/openid-registration.php
mod/lti/startltiadvregistration.php
mod/lti/templates/registration_upgrade_choice_page.mustache [new file with mode: 0644]
mod/lti/tests/behat/lti_activity_completion.feature
mod/lti/tests/openidregistration_test.php
mod/quiz/attemptlib.php
mod/quiz/lang/en/quiz.php
mod/quiz/lib.php
mod/quiz/locallib.php
mod/quiz/renderer.php
mod/quiz/report/overview/report.php
mod/quiz/review.php
mod/quiz/reviewquestion.php
mod/quiz/tests/behat/attempt_redo_questions.feature
mod/quiz/tests/behat/info_page.feature [new file with mode: 0644]
mod/quiz/view.php
mod/scorm/lib.php
mod/scorm/mod_form.php
mod/url/locallib.php
mod/workshop/lang/en/workshop.php
mod/workshop/tests/behat/workshop_assessment.feature
mod/workshop/view.php
payment/gateway/paypal/db/services.php
question/engine/lib.php
question/engine/questionattemptstep.php
question/engine/questionusage.php
question/engine/renderer.php
question/engine/tests/questionattemptstep_test.php
question/engine/tests/questionusagebyactivity_test.php
question/engine/tests/walkthrough_test.php
question/type/calculatedmulti/edit_calculatedmulti_form.php
question/type/ddimageortext/edit_ddimageortext_form.php
question/type/ddimageortext/questiontype.php
question/type/ddimageortext/tests/behat/add.feature
question/type/ddmarker/edit_ddmarker_form.php
question/type/ddmarker/questiontype.php
question/type/ddmarker/tests/behat/add.feature
question/type/edit_question_form.php
question/type/essay/edit_essay_form.php
question/type/essay/questiontype.php
question/type/essay/tests/behat/add.feature
question/type/match/edit_match_form.php
question/type/match/questiontype.php
question/type/match/tests/behat/add.feature
question/type/multianswer/edit_multianswer_form.php
question/type/multichoice/edit_multichoice_form.php
question/type/numerical/edit_numerical_form.php
question/type/numerical/questiontype.php
question/type/numerical/tests/behat/add.feature
report/insights/classes/output/insight.php
report/progress/classes/local/helper.php [new file with mode: 0644]
report/progress/classes/output/renderer.php [new file with mode: 0644]
report/progress/index.php
report/progress/lang/en/report_progress.php
report/progress/tests/behat/activity_completion_report_filter.feature [new file with mode: 0644]
report/progress/tests/report_progress_helper_testcase.php [new file with mode: 0644]
repository/contentbank/tests/browser_test.php
repository/contentbank/tests/search_test.php
repository/googledocs/classes/googledocs_content.php [new file with mode: 0644]
repository/googledocs/classes/googledocs_content_search.php [new file with mode: 0644]
repository/googledocs/classes/helper.php [new file with mode: 0644]
repository/googledocs/classes/local/browser/googledocs_drive_content.php [new file with mode: 0644]
repository/googledocs/classes/local/browser/googledocs_root_content.php [new file with mode: 0644]
repository/googledocs/classes/local/browser/googledocs_shared_drives_content.php [new file with mode: 0644]
repository/googledocs/classes/local/node/file_node.php [new file with mode: 0644]
repository/googledocs/classes/local/node/folder_node.php [new file with mode: 0644]
repository/googledocs/classes/local/node/node.php [new file with mode: 0644]
repository/googledocs/classes/rest.php
repository/googledocs/lang/en/repository_googledocs.php
repository/googledocs/lib.php
repository/googledocs/tests/generator/lib.php
repository/googledocs/tests/googledocs_content_testcase.php [new file with mode: 0644]
repository/googledocs/tests/googledocs_search_content_test.php [new file with mode: 0644]
repository/googledocs/tests/helper_test.php [new file with mode: 0644]
repository/googledocs/tests/local/browser/googledocs_drive_content_test.php [new file with mode: 0644]
repository/googledocs/tests/local/browser/googledocs_root_content_test.php [new file with mode: 0644]
repository/googledocs/tests/local/browser/googledocs_shared_drives_content_test.php [new file with mode: 0644]
repository/googledocs/tests/local/node/file_node_test.php [new file with mode: 0644]
repository/googledocs/tests/local/node/folder_node_test.php [new file with mode: 0644]
repository/googledocs/tests/repository_googledocs_testcase.php [new file with mode: 0644]
repository/googledocs/version.php
repository/upgrade.txt
theme/boost/amd/build/bootstrap/alert.min.js
theme/boost/amd/build/bootstrap/alert.min.js.map
theme/boost/amd/build/bootstrap/button.min.js
theme/boost/amd/build/bootstrap/button.min.js.map
theme/boost/amd/build/bootstrap/carousel.min.js
theme/boost/amd/build/bootstrap/carousel.min.js.map
theme/boost/amd/build/bootstrap/collapse.min.js
theme/boost/amd/build/bootstrap/collapse.min.js.map
theme/boost/amd/build/bootstrap/dropdown.min.js
theme/boost/amd/build/bootstrap/dropdown.min.js.map
theme/boost/amd/build/bootstrap/index.min.js [deleted file]
theme/boost/amd/build/bootstrap/index.min.js.map [deleted file]
theme/boost/amd/build/bootstrap/modal.min.js
theme/boost/amd/build/bootstrap/modal.min.js.map
theme/boost/amd/build/bootstrap/popover.min.js
theme/boost/amd/build/bootstrap/popover.min.js.map
theme/boost/amd/build/bootstrap/scrollspy.min.js
theme/boost/amd/build/bootstrap/scrollspy.min.js.map
theme/boost/amd/build/bootstrap/tab.min.js
theme/boost/amd/build/bootstrap/tab.min.js.map
theme/boost/amd/build/bootstrap/toast.min.js
theme/boost/amd/build/bootstrap/toast.min.js.map
theme/boost/amd/build/bootstrap/tools/sanitizer.min.js.map
theme/boost/amd/build/bootstrap/tooltip.min.js
theme/boost/amd/build/bootstrap/tooltip.min.js.map
theme/boost/amd/build/bootstrap/util.min.js.map
theme/boost/amd/build/index.min.js [new file with mode: 0644]
theme/boost/amd/build/index.min.js.map [new file with mode: 0644]
theme/boost/amd/build/loader.min.js
theme/boost/amd/build/loader.min.js.map
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 [deleted file]
theme/boost/amd/src/bootstrap/modal.js
theme/boost/amd/src/bootstrap/popover.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/tools/sanitizer.js
theme/boost/amd/src/bootstrap/tooltip.js
theme/boost/amd/src/bootstrap/util.js
theme/boost/amd/src/index.js [new file with mode: 0644]
theme/boost/amd/src/loader.js
theme/boost/readme_moodle.txt
theme/boost/scss/bootstrap/_alert.scss
theme/boost/scss/bootstrap/_breadcrumb.scss
theme/boost/scss/bootstrap/_card.scss
theme/boost/scss/bootstrap/_carousel.scss
theme/boost/scss/bootstrap/_custom-forms.scss
theme/boost/scss/bootstrap/_dropdown.scss
theme/boost/scss/bootstrap/_functions.scss
theme/boost/scss/bootstrap/_grid.scss
theme/boost/scss/bootstrap/_input-group.scss
theme/boost/scss/bootstrap/_list-group.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/_progress.scss
theme/boost/scss/bootstrap/_reboot.scss
theme/boost/scss/bootstrap/_root.scss
theme/boost/scss/bootstrap/_spinners.scss
theme/boost/scss/bootstrap/_toasts.scss
theme/boost/scss/bootstrap/_type.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/_border-radius.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/_image.scss
theme/boost/scss/bootstrap/mixins/_screen-reader.scss
theme/boost/scss/bootstrap/mixins/_transition.scss
theme/boost/scss/bootstrap/utilities/_borders.scss
theme/boost/scss/bootstrap/utilities/_text.scss
theme/boost/scss/moodle.scss
theme/boost/scss/moodle/core.scss
theme/boost/scss/moodle/toasts.scss [new file with mode: 0644]
theme/boost/scss/preset/default.scss
theme/boost/style/moodle.css
theme/boost/thirdpartylibs.xml
theme/classic/scss/preset/default.scss
theme/classic/style/moodle.css
user/classes/analytics/indicator/user_profile_set.php
user/classes/privacy/provider.php
user/editlib.php
user/externallib.php
user/lib.php
user/profile/definelib.php
user/profile/field/social/classes/helper.php [new file with mode: 0644]
user/profile/field/social/classes/privacy/provider.php [new file with mode: 0644]
user/profile/field/social/define.class.php [new file with mode: 0644]
user/profile/field/social/field.class.php [new file with mode: 0644]
user/profile/field/social/lang/en/profilefield_social.php [new file with mode: 0644]
user/profile/field/social/tests/behat/social_profile_field.feature [new file with mode: 0644]
user/profile/field/social/tests/privacy_test.php [new file with mode: 0644]
user/profile/field/social/upgradelib.php [new file with mode: 0644]
user/profile/field/social/version.php [new file with mode: 0644]
user/profile/index.php
user/profile/lib.php
user/selector/lib.php
user/tests/behat/custom_profile_fields.feature
user/tests/externallib_test.php
user/tests/privacy_test.php
user/tests/userlib_test.php
version.php

index a6d5413..2262b1f 100644 (file)
@@ -84,7 +84,6 @@ 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
@@ -93,4 +92,5 @@ 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/scss/fontawesome/
\ No newline at end of file
+theme/boost/amd/src/index.js
+theme/boost/scss/fontawesome/
index 35fd4a7..0c4f1ba 100644 (file)
@@ -1,6 +1,12 @@
 name: Core
 
-on: [push]
+on:
+  push:
+    branches-ignore:
+      - master
+      - MOODLE_[0-9]+_STABLE
+    tags-ignore:
+      - v[0-9]+.[0-9]+.[0-9]+*
 
 env:
   php: 7.4
index d8b9ec1..9cc2f6c 100644 (file)
@@ -41,7 +41,7 @@ module.exports = grunt => {
             '*/**/yui/src/*/meta/',
             '*/**/build/',
         ].concat(thirdPartyPaths);
-        grunt.file.write('.eslintignore', eslintIgnores.join('\n'));
+        grunt.file.write('.eslintignore', eslintIgnores.join('\n') + '\n');
 
         // Generate .stylelintignore.
         const stylelintIgnores = [
@@ -50,7 +50,7 @@ module.exports = grunt => {
             'theme/boost/style/moodle.css',
             'theme/classic/style/moodle.css',
         ].concat(thirdPartyPaths);
-        grunt.file.write('.stylelintignore', stylelintIgnores.join('\n'));
+        grunt.file.write('.stylelintignore', stylelintIgnores.join('\n') + '\n');
     };
 
     grunt.registerTask('ignorefiles', 'Generate ignore files for linters', handler);
index 0207e41..2d28f80 100644 (file)
@@ -84,7 +84,6 @@ 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
@@ -93,4 +92,5 @@ 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/scss/fontawesome/
\ No newline at end of file
+theme/boost/amd/src/index.js
+theme/boost/scss/fontawesome/
index 0c565f3..6d52469 100644 (file)
@@ -19,6 +19,12 @@ services:
 addons:
   postgresql: "9.6"
 
+branches:
+  except:
+    - master
+    - /MOODLE_[0-9]+_STABLE/
+    - /^v[0-9]+\.[0-9]+\.[0-9]+.*/
+
 jobs:
     # Enable fast finish.
     # This will fail the build if a single job fails (except those in allow_failures).
index 3ee8ebc..503dd91 100644 (file)
@@ -171,8 +171,6 @@ function print_auth_lock_options($auth, $user_fields, $helptext, $retrieveopts,
                 // limit for the setting name is 100.
                 continue;
             }
-        } elseif ($fieldname == 'url') {
-            $fieldname = get_string('webpage');
         } else {
             $fieldname = get_string($fieldname);
         }
index b5c31d7..aa632b1 100644 (file)
@@ -119,10 +119,19 @@ if (($form = data_submitted()) && confirm_sesskey()) {
     // Save processors enabled/disabled status.
     foreach ($allprocessors as $processor) {
         $enabled = isset($form->{$processor->name});
+        if ($enabled != $processor->enabled) {
+            add_to_config_log($processor->name, $processor->enabled, $enabled, 'core');
+        }
         \core_message\api::update_processor_status($processor, $enabled);
     }
 
     foreach ($newpreferences as $name => $value) {
+        $old = isset($preferences->$name) ? $preferences->$name : '';
+
+        if ($old != $value) {
+            add_to_config_log($name, $old, $value, 'core');
+        }
+
         set_config($name, $value, 'message');
     }
     $transaction->allow_commit();
index 709e28c..c0b5572 100644 (file)
@@ -34,11 +34,12 @@ $error = optional_param('error', '', PARAM_RAW);
 if ($error) {
     $message = optional_param('error_description', '', PARAM_RAW);
     if ($message) {
-        print_error($message);
+        $SESSION->loginerrormsg = $message;
+        redirect(new moodle_url(get_login_url()));
     } else {
-        print_error($error);
+        $SESSION->loginerrormsg = $error;
+        redirect(new moodle_url(get_login_url()));
     }
-    die();
 }
 
 // The authorization code generated by the authorization server.
@@ -53,5 +54,6 @@ if (isset($params['sesskey']) and confirm_sesskey($params['sesskey'])) {
     $redirecturl->param('oauth2code', $code);
     redirect($redirecturl);
 } else {
-    print_error('invalidsesskey');
+    $SESSION->loginerrormsg = get_string('invalidsesskey', 'error');
+    redirect(new moodle_url(get_login_url()));
 }
index b4401c4..b43aa77 100644 (file)
@@ -52,8 +52,14 @@ if ($data = $form->get_data()) {
     $message = get_string('purgecachesfinished', 'admin');
 }
 
+// Redirect and/or show notification message confirming cache(s) were purged.
 if (isset($message)) {
-    redirect($returnurl, $message);
+    if (!$PAGE->url->compare($returnurl, URL_MATCH_BASE)) {
+        redirect($returnurl, $message);
+    }
+
+    // We are already on the purge caches page, add the notification.
+    \core\notification::add($message, \core\output\notification::NOTIFY_INFO);
 }
 
 // Otherwise, show a form to actually purge the caches.
index 86d198e..2037a63 100644 (file)
@@ -343,6 +343,11 @@ if ($hassiteconfig or has_any_capability($capabilities, $systemcontext)) {
 
     // Import defaults section.
     $temp->add(new admin_setting_heading('importsettings', new lang_string('importsettings', 'backup'), ''));
+    $temp->add(new admin_setting_configcheckbox_with_lock(
+            'backup/backup_import_permissions',
+            new lang_string('generalpermissions', 'backup'),
+            new lang_string('configgeneralpermissions', 'backup'),
+            array('value' => 0, 'locked' => 0)));
     $temp->add(new admin_setting_configcheckbox_with_lock('backup/backup_import_activities', new lang_string('generalactivities','backup'), new lang_string('configgeneralactivities','backup'), array('value'=>1, 'locked'=>0)));
     $temp->add(new admin_setting_configcheckbox_with_lock('backup/backup_import_blocks', new lang_string('generalblocks','backup'), new lang_string('configgeneralblocks','backup'), array('value'=>1, 'locked'=>0)));
     $temp->add(new admin_setting_configcheckbox_with_lock('backup/backup_import_filters', new lang_string('generalfilters','backup'), new lang_string('configgeneralfilters','backup'), array('value'=>1, 'locked'=>0)));
@@ -517,6 +522,9 @@ if ($hassiteconfig or has_any_capability($capabilities, $systemcontext)) {
     $temp->add(new admin_setting_configcheckbox_with_lock('restore/restore_general_role_assignments',
         new lang_string('generalroleassignments', 'backup'),
         new lang_string('configrestoreroleassignments', 'backup'), array('value' => 1, 'locked' => 0)));
+    $temp->add(new admin_setting_configcheckbox_with_lock('restore/restore_general_permissions',
+        new lang_string('generalpermissions', 'backup'),
+        new lang_string('configrestorepermissions', 'backup'), array('value' => 1, 'locked' => 0)));
     $temp->add(new admin_setting_configcheckbox_with_lock('restore/restore_general_activities',
         new lang_string('generalactivities', 'backup'),
         new lang_string('configrestoreactivities', 'backup'), array('value' => 1, 'locked' => 0)));
index 6e58d4c..2f265ee 100644 (file)
@@ -196,12 +196,6 @@ if ($hassiteconfig
                              'country' => new lang_string('country'),
                              'moodlenetprofile' => new lang_string('moodlenetprofile', 'user'),
                              'timezone' => new lang_string('timezone'),
-                             'webpage' => new lang_string('webpage'),
-                             'icqnumber' => new lang_string('icqnumber'),
-                             'skypeid' => new lang_string('skypeid'),
-                             'yahooid' => new lang_string('yahooid'),
-                             'aimid' => new lang_string('aimid'),
-                             'msnid' => new lang_string('msnid'),
                              'firstaccess' => new lang_string('firstaccess'),
                              'lastaccess' => new lang_string('lastaccess'),
                              'lastip' => new lang_string('lastip'),
diff --git a/admin/tool/dataprivacy/amd/build/contactdpo.min.js b/admin/tool/dataprivacy/amd/build/contactdpo.min.js
new file mode 100644 (file)
index 0000000..7351478
Binary files /dev/null and b/admin/tool/dataprivacy/amd/build/contactdpo.min.js differ
diff --git a/admin/tool/dataprivacy/amd/build/contactdpo.min.js.map b/admin/tool/dataprivacy/amd/build/contactdpo.min.js.map
new file mode 100644 (file)
index 0000000..6445af7
Binary files /dev/null and b/admin/tool/dataprivacy/amd/build/contactdpo.min.js.map differ
index a6ba759..b1b7317 100644 (file)
Binary files a/admin/tool/dataprivacy/amd/build/myrequestactions.min.js and b/admin/tool/dataprivacy/amd/build/myrequestactions.min.js differ
index c9f78ed..4dfdc98 100644 (file)
Binary files a/admin/tool/dataprivacy/amd/build/myrequestactions.min.js.map and b/admin/tool/dataprivacy/amd/build/myrequestactions.min.js.map differ
diff --git a/admin/tool/dataprivacy/amd/src/contactdpo.js b/admin/tool/dataprivacy/amd/src/contactdpo.js
new file mode 100644 (file)
index 0000000..50417a8
--- /dev/null
@@ -0,0 +1,67 @@
+// 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/>.
+
+/**
+ * Javascript module for contacting the site DPO
+ *
+ * @module      tool_dataprivacy/contactdpo
+ * @package     tool_dataprivacy
+ * @copyright   2021 Paul Holden <paulh@moodle.com>
+ * @license     http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+import ModalForm from 'core_form/modalform';
+import Notification from 'core/notification';
+import {get_string as getString} from 'core/str';
+import {add as addToast} from 'core/toast';
+
+const SELECTORS = {
+    CONTACT_DPO: '[data-action="contactdpo"]',
+};
+
+/**
+ * Initialize module
+ */
+export const init = () => {
+    const triggerElement = document.querySelector(SELECTORS.CONTACT_DPO);
+
+    triggerElement.addEventListener('click', event => {
+        event.preventDefault();
+
+        const modalForm = new ModalForm({
+            modalConfig: {
+                title: getString('contactdataprotectionofficer', 'tool_dataprivacy'),
+            },
+            formClass: 'tool_dataprivacy\\form\\contactdpo',
+            saveButtonText: getString('send', 'tool_dataprivacy'),
+            returnFocus: triggerElement,
+        });
+
+        // Show a toast notification when the form is submitted.
+        modalForm.addEventListener(modalForm.events.FORM_SUBMITTED, event => {
+            if (event.detail.result) {
+                getString('requestsubmitted', 'tool_dataprivacy').then(addToast).catch();
+            } else {
+                const warningMessages = event.detail.warnings.map(warning => warning.message);
+                Notification.addNotification({
+                    type: 'error',
+                    message: warningMessages.join('<br>')
+                });
+            }
+        });
+
+        modalForm.show();
+    });
+};
index 54f94fb..b75b386 100644 (file)
  * @copyright  2018 Jun Pataleta
  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  */
-define([
-    'jquery',
-    'core/ajax',
-    'core/notification',
-    'core/str',
-    'core/modal_factory',
-    'core/modal_events',
-    'core/templates',
-    'core/pending'],
-function($, Ajax, Notification, Str, ModalFactory, ModalEvents, Templates, Pending) {
 
-    /**
-     * List of action selectors.
-     *
-     * @type {{CANCEL_REQUEST: string}}
-     * @type {{CONTACT_DPO: string}}
-     */
-    var ACTIONS = {
-        CANCEL_REQUEST: '[data-action="cancel"]',
-        CONTACT_DPO: '[data-action="contactdpo"]',
-    };
+import Ajax from 'core/ajax';
+import Notification from 'core/notification';
+import Pending from 'core/pending';
+import {get_strings as getStrings} from 'core/str';
 
-    /**
-     * MyRequestActions class.
-     */
-    var MyRequestActions = function() {
-        this.registerEvents();
-    };
+const SELECTORS = {
+    CANCEL_REQUEST: '[data-action="cancel"][data-requestid]',
+};
 
-    /**
-     * Register event listeners.
-     */
-    MyRequestActions.prototype.registerEvents = function() {
-        $(ACTIONS.CANCEL_REQUEST).click(function(e) {
-            e.preventDefault();
-
-            var requestId = $(this).data('requestid');
-            var stringkeys = [
-                {
-                    key: 'cancelrequest',
-                    component: 'tool_dataprivacy'
-                },
-                {
-                    key: 'cancelrequestconfirmation',
-                    component: 'tool_dataprivacy'
-                }
-            ];
-
-            Str.get_strings(stringkeys).then(function(langStrings) {
-                var title = langStrings[0];
-                var confirmMessage = langStrings[1];
-                return ModalFactory.create({
-                    title: title,
-                    body: confirmMessage,
-                    type: ModalFactory.types.SAVE_CANCEL
-                }).then(function(modal) {
-                    modal.setSaveButtonText(title);
-
-                    // Handle save event.
-                    modal.getRoot().on(ModalEvents.save, function() {
-                        // Cancel the request.
-                        var params = {
-                            'requestid': requestId
-                        };
-
-                        var request = {
-                            methodname: 'tool_dataprivacy_cancel_data_request',
-                            args: params
-                        };
-
-                        Ajax.call([request])[0].done(function(data) {
-                            if (data.result) {
-                                window.location.reload();
-                            } else {
-                                Notification.addNotification({
-                                    message: data.warnings[0].message,
-                                    type: 'error'
-                                });
-                            }
-                        }).fail(Notification.exception);
-                    });
-
-                    // Handle hidden event.
-                    modal.getRoot().on(ModalEvents.hidden, function() {
-                        // Destroy when hidden.
-                        modal.destroy();
-                    });
-
-                    return modal;
-                });
-            }).done(function(modal) {
-                // Show the modal!
-                modal.show();
-
-            }).fail(Notification.exception);
-        });
-
-        $(ACTIONS.CONTACT_DPO).click(function(e) {
-            var pendingPromise = new Pending('dataprivacy/crud:initModal:contactdpo');
-            e.preventDefault();
+/**
+ * Initialize module
+ */
+export const init = () => {
+    document.addEventListener('click', event => {
+        const triggerElement = event.target.closest(SELECTORS.CANCEL_REQUEST);
+        if (triggerElement === null) {
+            return;
+        }
 
-            var replyToEmail = $(this).data('replytoemail');
+        event.preventDefault();
 
-            var keys = [
-                {
-                    key: 'contactdataprotectionofficer',
-                    component: 'tool_dataprivacy'
-                },
-                {
-                    key: 'send',
-                    component: 'tool_dataprivacy'
-                },
-            ];
+        const requiredStrings = [
+            {key: 'cancelrequest', component: 'tool_dataprivacy'},
+            {key: 'cancelrequestconfirmation', component: 'tool_dataprivacy'},
+        ];
 
-            var sendButtonText = '';
-            Str.get_strings(keys).then(function(langStrings) {
-                var modalTitle = langStrings[0];
-                sendButtonText = langStrings[1];
-                var context = {
-                    'replytoemail': replyToEmail
+        getStrings(requiredStrings).then(([cancelRequest, cancelConfirm]) => {
+            return Notification.confirm(cancelRequest, cancelConfirm, cancelRequest, null, () => {
+                const pendingPromise = new Pending('tool/dataprivacy:cancelRequest');
+                const request = {
+                    methodname: 'tool_dataprivacy_cancel_data_request',
+                    args: {requestid: triggerElement.dataset.requestid}
                 };
-                return ModalFactory.create({
-                    title: modalTitle,
-                    body: Templates.render('tool_dataprivacy/contact_dpo', context),
-                    type: ModalFactory.types.SAVE_CANCEL,
-                    large: true
-                });
-            }).then(function(modal) {
-                modal.setSaveButtonText(sendButtonText);
-
-                // Show the modal!
-                modal.show();
 
-                // Handle send event.
-                modal.getRoot().on(ModalEvents.save, function(e) {
-                    var message = $('#message').val().trim();
-                    if (message.length === 0) {
-                        e.preventDefault();
-                        // Show validation error when the message is empty.
-                        $('[data-region="messageinput"]').addClass('has-danger notifyproblem');
-                        $('#id_error_message').removeAttr('hidden');
+                Ajax.call([request])[0].then(response => {
+                    if (response.result) {
+                        window.location.reload();
                     } else {
-                        // Send the message.
-                        sendMessageToDPO(message);
+                        Notification.addNotification({
+                            type: 'error',
+                            message: response.warnings[0].message
+                        });
                     }
-                });
-
-                // Handle hidden event.
-                modal.getRoot().on(ModalEvents.hidden, function() {
-                    // Destroy when hidden.
-                    modal.destroy();
-                });
-
-                return;
-            }).then(pendingPromise.resolve)
-            .catch(Notification.exception);
-        });
-    };
-
-    /**
-     * Send message to the Data Protection Officer.
-     *
-     * @param {String} message The message to send.
-     */
-    function sendMessageToDPO(message) {
-        var request = {
-            methodname: 'tool_dataprivacy_contact_dpo',
-            args: {
-                message: message
-            }
-        };
-
-        var requestType = 'success';
-        Ajax.call([request])[0].then(function(data) {
-            if (data.result) {
-                return Str.get_string('requestsubmitted', 'tool_dataprivacy');
-            }
-            requestType = 'error';
-            return data.warnings.join('<br>');
-
-        }).done(function(message) {
-            Notification.addNotification({
-                message: message,
-                type: requestType
+                    return pendingPromise.resolve();
+                }).catch(Notification.exception);
             });
-
-        }).fail(Notification.exception);
-    }
-
-    return /** @alias module:tool_dataprivacy/myrequestactions */ {
-        // Public variables and functions.
-
-        /**
-         * Initialise the unified user filter.
-         *
-         * @method init
-         * @return {MyRequestActions}
-         */
-        'init': function() {
-            return new MyRequestActions();
-        }
-    };
-});
+        }).catch();
+    });
+};
index 92dd398..30d2e6d 100644 (file)
@@ -212,7 +212,8 @@ class external extends external_api {
                 $warnings[] = [
                     'item' => $dpo->id,
                     'warningcode' => 'errorsendingtodpo',
-                    'message' => get_string('errorsendingmessagetodpo', 'tool_dataprivacy')
+                    'message' => get_string('errorsendingmessagetodpo', 'tool_dataprivacy',
+                        fullname($dpo))
                 ];
             }
         }
diff --git a/admin/tool/dataprivacy/classes/form/contactdpo.php b/admin/tool/dataprivacy/classes/form/contactdpo.php
new file mode 100644 (file)
index 0000000..dc17291
--- /dev/null
@@ -0,0 +1,100 @@
+<?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/>.
+
+
+namespace tool_dataprivacy\form;
+
+use context;
+use context_user;
+use moodle_exception;
+use moodle_url;
+use core_form\dynamic_form;
+use tool_dataprivacy\api;
+use tool_dataprivacy\external;
+
+/**
+ * Contact DPO modal form
+ *
+ * @package    tool_dataprivacy
+ * @copyright  2021 Paul Holden <paulh@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class contactdpo extends dynamic_form {
+
+    /**
+     * Form definition
+     */
+    protected function definition() {
+        global $USER;
+
+        $mform = $this->_form;
+
+        $mform->addElement('static', 'replyto', get_string('replyto', 'tool_dataprivacy'), s($USER->email));
+
+        $mform->addElement('textarea', 'message', get_string('message', 'tool_dataprivacy'), 'cols="60" rows="8"');
+        $mform->setType('message', PARAM_TEXT);
+        $mform->addRule('message', get_string('required'), 'required', null, 'client');
+    }
+
+    /**
+     * Return form context
+     *
+     * @return context
+     */
+    protected function get_context_for_dynamic_submission(): context {
+        global $USER;
+
+        return context_user::instance($USER->id);
+    }
+
+    /**
+     * Check if current user has access to this form, otherwise throw exception
+     *
+     * @throws moodle_exception
+     */
+    protected function check_access_for_dynamic_submission(): void {
+        if (!api::can_contact_dpo()) {
+            throw new moodle_exception('errorcontactdpodisabled', 'tool_dataprivacy');
+        }
+    }
+
+    /**
+     * Process the form submission, used if form was submitted via AJAX
+     *
+     * @return array
+     */
+    public function process_dynamic_submission() {
+        return external::contact_dpo($this->get_data()->message);
+    }
+
+    /**
+     * Load in existing data as form defaults (not applicable)
+     */
+    public function set_data_for_dynamic_submission(): void {
+        return;
+    }
+
+    /**
+     * Returns url to set in $PAGE->set_url() when form is being rendered or submitted via AJAX
+     *
+     * @return moodle_url
+     */
+    protected function get_page_url_for_dynamic_submission(): moodle_url {
+        global $USER;
+
+        return new moodle_url('/user/profile.php', ['id' => $USER->id]);
+    }
+}
index 8032906..5d3f4cb 100644 (file)
@@ -55,14 +55,11 @@ class renderer extends plugin_renderer_base {
     /**
      * Render the contact DPO link.
      *
-     * @param string $replytoemail The Reply-to email address
      * @return string The HTML for the link.
-     * @throws coding_exception
      */
-    public function render_contact_dpo_link($replytoemail) {
+    public function render_contact_dpo_link() {
         $params = [
             'data-action' => 'contactdpo',
-            'data-replytoemail' => $replytoemail,
         ];
         return html_writer::link('#', get_string('contactdataprotectionofficer', 'tool_dataprivacy'), $params);
     }
index 46d7af2..53ce860 100644 (file)
@@ -135,6 +135,7 @@ $string['effectiveretentionperioduser'] = '{$a} (since the last time the user ac
 $string['emailsalutation'] = 'Dear {$a},';
 $string['errorcannotrequestdeleteforself'] = 'You don\'t have permission to create deletion request for yourself.';
 $string['errorcannotrequestdeleteforother'] = 'You don\'t have permission to create deletion request for this user.';
+$string['errorcontactdpodisabled'] = 'Contacting the privacy officer is disabled';
 $string['errorinvalidrequestcomments'] = 'The comments field may contain plain text only.';
 $string['errorinvalidrequestcreationmethod'] = 'Invalid request creation method!';
 $string['errorinvalidrequeststatus'] = 'Invalid request status!';
index 5b5f28a..25091fd 100644 (file)
@@ -54,10 +54,12 @@ function tool_dataprivacy_myprofile_navigation(tree $tree, $user, $iscurrentuser
     // Contact data protection officer link.
     if (\tool_dataprivacy\api::can_contact_dpo() && $iscurrentuser) {
         $renderer = $PAGE->get_renderer('tool_dataprivacy');
-        $content = $renderer->render_contact_dpo_link($USER->email);
+        $content = $renderer->render_contact_dpo_link();
         $node = new core_user\output\myprofile\node('privacyandpolicies', 'contactdpo', null, null, null, $content);
         $category->add_node($node);
-        $PAGE->requires->js_call_amd('tool_dataprivacy/myrequestactions', 'init');
+
+        // Require our Javascript module to handle contact DPO interaction.
+        $PAGE->requires->js_call_amd('tool_dataprivacy/contactdpo', 'init');
 
         $url = new moodle_url('/admin/tool/dataprivacy/mydatarequests.php');
         $node = new core_user\output\myprofile\node('privacyandpolicies', 'datarequests',
diff --git a/admin/tool/dataprivacy/templates/contact_dpo.mustache b/admin/tool/dataprivacy/templates/contact_dpo.mustache
deleted file mode 100644 (file)
index d3a0e86..0000000
+++ /dev/null
@@ -1,58 +0,0 @@
-{{!
-    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/>.
-}}
-{{!
-    @template tool_dataprivacy/contact_dpo
-
-    The purpose of this template is to enable the user to contact the site's DPO via email.
-
-    Classes required for JS:
-    * none
-
-    Data attributes required for JS:
-    * none
-
-    Context variables required for this template:
-    * userid int The user's ID.
-    * email string The user's email address.
-
-    Example context (json):
-    {
-        "userid": 1,
-        "replytoemail": "martha@example.com"
-    }
-}}
-<div class="container">
-    <div class="row mb-2">
-        <label class="col-md-3 col-form-label">{{#str}}replyto, tool_dataprivacy{{/str}}</label>
-        <div class="col-md-9 col-form-label">{{replytoemail}}</div>
-    </div>
-    <div class="row" data-region="messageinput">
-        <label for="message" class="col-md-3 col-form-label">
-            {{#str}}message, tool_dataprivacy{{/str}}
-            <span class="float-sm-right text-nowrap">
-            <abbr class="initialism text-danger" title="{{#str}}required{{/str}}">{{#pix}}req, core, {{#str}}required{{/str}}{{/pix}}</abbr>
-        </span>
-        </label>
-        <div class="col-md-9">
-            <textarea class="form-control" id="message" cols="60" rows="8"></textarea>
-            <div class="form-control-feedback" id="id_error_message" hidden="hidden">
-                {{#str}}required, moodle{{/str}}
-            </div>
-        </div>
-    </div>
-</div>
-
index 528d1f6..28f5de8 100644 (file)
@@ -8,19 +8,21 @@ Feature: Contact the privacy officer
     Given the following "users" exist:
       | username | firstname | lastname | email          |
       | student1 | Student   | 1        | s1@example.com |
-    And I log in as "admin"
-    And I set the following administration settings values:
-      | contactdataprotectionofficer | 1 |
-    And I log out
 
   @javascript
   Scenario: Contacting the privacy officer
-    Given I log in as "student1"
+    Given the following config values are set as admin:
+      | contactdataprotectionofficer | 1 | tool_dataprivacy |
+    When I log in as "student1"
     And I follow "Profile" in the user menu
-    And I should see "Contact the privacy officer"
     And I click on "Contact the privacy officer" "link"
     And I set the field "Message" to "Hello DPO!"
     And I click on "Send" "button" in the "Contact the privacy officer" "dialogue"
-    And I should see "Your request has been submitted to the privacy officer"
+    Then I should see "Your request has been submitted to the privacy officer"
     And I click on "Data requests" "link"
     And I should see "Hello DPO!" in the "General inquiry" "table_row"
+
+  Scenario: Contacting the privacy officer when not enabled
+    When I log in as "student1"
+    And I follow "Profile" in the user menu
+    Then "Contact the privacy officer" "link" should not exist
diff --git a/admin/tool/dataprivacy/tests/behat/my_data_requests.feature b/admin/tool/dataprivacy/tests/behat/my_data_requests.feature
new file mode 100644 (file)
index 0000000..15de9cd
--- /dev/null
@@ -0,0 +1,26 @@
+@tool @tool_dataprivacy
+Feature: Manage my own data requests
+  In order to manage my own data requests
+  As a user
+  I need to be able to view and cancel all my data requests
+
+  Background:
+    Given the following "users" exist:
+      | username | firstname | lastname | email          |
+      | student1 | Student   | 1        | s1@example.com |
+    And the following config values are set as admin:
+      | contactdataprotectionofficer | 1 | tool_dataprivacy |
+
+  @javascript
+  Scenario: Cancel my own data request
+    Given I log in as "student1"
+    And I follow "Profile" in the user menu
+    And I click on "Contact the privacy officer" "link"
+    And I set the field "Message" to "Hello DPO!"
+    And I click on "Send" "button" in the "Contact the privacy officer" "dialogue"
+    And I should see "Your request has been submitted to the privacy officer"
+    When I click on "Data requests" "link"
+    And I open the action menu in "Hello DPO!" "table_row"
+    And I choose "Cancel" in the open action menu
+    And I click on "Cancel request" "button" in the "Cancel request" "dialogue"
+    Then I should see "Cancelled" in the "Hello DPO!" "table_row"
index 863aaed..cac07d9 100644 (file)
     This template does not have an example context because it includes ajax functionality.
 }}
 <div data-region="user-competency-full-info" data-node="user-competency" data-competencyid="{{usercompetency.competencyid}}" data-userid="{{usercompetency.userid}}" data-region-id="{{uniqid}}">
-<div data-region="competency-summary">
-    {{#competency}}
-        {{> tool_lp/competency_summary }}
-    {{/competency}}
-    <dl>
-        {{#usercompetency}}
-        <dt>{{#str}}reviewstatus, tool_lp{{/str}}</dt>
-        <dd data-region="user-competency-status">{{statusname}}
+    <div data-region="competency-summary">
+        {{#competency}}
+            {{> tool_lp/competency_summary }}
+        {{/competency}}
+        <dl>
+            {{#usercompetency}}
+            <dt>{{#str}}reviewstatus, tool_lp{{/str}}</dt>
+            <dd data-region="user-competency-status">{{statusname}}
 
-            {{#isstatusinreview}}
-                - {{reviewer.fullname}}
-            {{/isstatusinreview}}
+                {{#isstatusinreview}}
+                    - {{reviewer.fullname}}
+                {{/isstatusinreview}}
 
-            {{#isrequestreviewallowed}}<button class="btn btn-secondary" data-action="request-review">{{#str}}requestreview, tool_lp{{/str}}</button>{{/isrequestreviewallowed}}
-            {{#iscancelreviewrequestallowed}}<button class="btn btn-secondary" data-action="cancel-review-request">{{#str}}cancelreviewrequest, tool_lp{{/str}}</button>{{/iscancelreviewrequestallowed}}
-            {{#isstartreviewallowed}}<button class="btn btn-secondary" data-action="start-review">{{#str}}startreview, tool_lp{{/str}}</button>{{/isstartreviewallowed}}
-            {{#isstopreviewallowed}}<button class="btn btn-secondary" data-action="stop-review">{{#str}}stopreview, tool_lp{{/str}}</button>{{/isstopreviewallowed}}
-        </dd>
-        <dt>{{#str}}proficient, tool_lp{{/str}}</dt>
-        <dd>
-            <span class="badge {{#proficiency}}badge-success{{/proficiency}}{{^proficiency}}badge-danger{{/proficiency}} float-left">
-                {{proficiencyname}}
-            </span>
-        </dd>
-        <dt>{{#str}}rating, tool_lp{{/str}}</dt>
-        <dd>{{gradename}}
-            {{#cangrade}}
-                <button class="btn btn-secondary" id="rate_{{uniqid}}">{{#str}}rate, tool_lp{{/str}}</button>
-            {{/cangrade}}
-        </dd>
-        {{#js}}
-        require(['jquery', 'tool_lp/grade_user_competency_inline', 'tool_lp/user_competency_info', 'tool_lp/user_competency_workflow'], function($, mod, info, UserCompWorkflow) {
+                {{#isrequestreviewallowed}}<button class="btn btn-secondary" data-action="request-review">{{#str}}requestreview, tool_lp{{/str}}</button>{{/isrequestreviewallowed}}
+                {{#iscancelreviewrequestallowed}}<button class="btn btn-secondary" data-action="cancel-review-request">{{#str}}cancelreviewrequest, tool_lp{{/str}}</button>{{/iscancelreviewrequestallowed}}
+                {{#isstartreviewallowed}}<button class="btn btn-secondary" data-action="start-review">{{#str}}startreview, tool_lp{{/str}}</button>{{/isstartreviewallowed}}
+                {{#isstopreviewallowed}}<button class="btn btn-secondary" data-action="stop-review">{{#str}}stopreview, tool_lp{{/str}}</button>{{/isstopreviewallowed}}
+            </dd>
+            <dt>{{#str}}proficient, tool_lp{{/str}}</dt>
+            <dd>
+                <span class="badge {{#proficiency}}badge-success{{/proficiency}}{{^proficiency}}badge-danger{{/proficiency}} float-left">
+                    {{proficiencyname}}
+                </span>
+            </dd>
+            <dt>{{#str}}rating, tool_lp{{/str}}</dt>
+            <dd>{{gradename}}
+                {{#cangrade}}
+                    <button class="btn btn-secondary" id="rate_{{uniqid}}">{{#str}}rate, tool_lp{{/str}}</button>
+                {{/cangrade}}
+            </dd>
+            {{#js}}
+            require(['jquery', 'tool_lp/grade_user_competency_inline', 'tool_lp/user_competency_info', 'tool_lp/user_competency_workflow'], function($, mod, info, UserCompWorkflow) {
 
-            var competencyElement = $('[data-region-id="{{uniqid}}"]');
-            var infoReloader = new info(competencyElement, '{{competency.competency.id}}', '{{user.id}}');
+                var competencyElement = $('[data-region-id="{{uniqid}}"]');
+                var infoReloader = new info(competencyElement, '{{competency.competency.id}}', '{{user.id}}');
 
-            var ucw = new UserCompWorkflow();
-            ucw.registerEvents('[data-region="user-competency-status"]');
-            ucw.on('status-changed', infoReloader.reload.bind(infoReloader));
-            ucw.on('error-occured', infoReloader.reload.bind(infoReloader));
+                var ucw = new UserCompWorkflow();
+                ucw.registerEvents('[data-region="user-competency-status"]');
+                ucw.on('status-changed', infoReloader.reload.bind(infoReloader));
+                ucw.on('error-occured', infoReloader.reload.bind(infoReloader));
 
-            var inlineGrader = new mod('#rate_{{uniqid}}', '{{competency.scaleid}}', '{{competency.competency.id}}', '{{user.id}}', '{{plan.id}}', '', '{{#str}}chooserating, tool_lp{{/str}}');
-            inlineGrader.on('competencyupdated', infoReloader.reload.bind(infoReloader));
-        });
-        {{/js}}
-        {{/usercompetency}}
-    </dl>
-    {{#commentarea}}
-        {{#canpostorhascomments}}
-            {{>tool_lp/comment_area}}
-        {{/canpostorhascomments}}
-    {{/commentarea}}
-    <dl data-region="evidence-listing">
-        <dt>{{#str}}evidence, tool_lp{{/str}}</dt>
-        <dd>
-            {{#evidence}}
-                {{> tool_lp/evidence_summary }}
-            {{/evidence}}
-            {{^evidence}}
-                <p>{{#str}}noevidence, tool_lp{{/str}}</p>
-            {{/evidence}}
-        </dd>
-    </dl>
+                var inlineGrader = new mod('#rate_{{uniqid}}', '{{competency.scaleid}}', '{{competency.competency.id}}', '{{user.id}}', '{{plan.id}}', '', '{{#str}}chooserating, tool_lp{{/str}}');
+                inlineGrader.on('competencyupdated', infoReloader.reload.bind(infoReloader));
+            });
+            {{/js}}
+            {{/usercompetency}}
+        </dl>
+        {{#commentarea}}
+            {{#canpostorhascomments}}
+                {{>tool_lp/comment_area}}
+            {{/canpostorhascomments}}
+        {{/commentarea}}
+        <dl data-region="evidence-listing">
+            <dt>{{#str}}evidence, tool_lp{{/str}}</dt>
+            <dd>
+                {{#evidence}}
+                    {{> tool_lp/evidence_summary }}
+                {{/evidence}}
+                {{^evidence}}
+                    <p>{{#str}}noevidence, tool_lp{{/str}}</p>
+                {{/evidence}}
+            </dd>
+        </dl>
+    </div>
 </div>
index a5d6c97..84b0669 100644 (file)
@@ -115,54 +115,69 @@ class issuer extends persistent {
         $mform->addElement('checkbox', 'basicauth', get_string('usebasicauth', 'tool_oauth2'));
         $mform->addHelpButton('basicauth', 'usebasicauth', 'tool_oauth2');
 
+        // Base Url.
+        $mform->addElement('text', 'baseurl', get_string('issuerbaseurl', 'tool_oauth2'));
+        $mform->addRule('baseurl', get_string('maximumchars', '', 1024), 'maxlength', 1024, 'client');
+        $mform->addHelpButton('baseurl', 'issuerbaseurl', 'tool_oauth2');
+        if ($this->type && $this->type == 'nextcloud') {
+            $mform->addRule('baseurl', null, 'required', null, 'client');
+        }
+
+        // Image.
+        $mform->addElement('text', 'image', get_string('issuerimage', 'tool_oauth2'), 'maxlength="1024"');
+        $mform->addRule('image', get_string('maximumchars', '', 1024), 'maxlength', 1024, 'client');
+        $mform->addHelpButton('image', 'issuername', 'tool_oauth2');
+
+        // Show on login page.
+        $options = [
+            \core\oauth2\issuer::EVERYWHERE => get_string('issueruseineverywhere', 'tool_oauth2'),
+            \core\oauth2\issuer::LOGINONLY => get_string('issueruseinloginonly', 'tool_oauth2'),
+            \core\oauth2\issuer::SERVICEONLY => get_string('issueruseininternalonly', 'tool_oauth2'),
+        ];
+        $mform->addElement('select', 'showonloginpage', get_string('issuerusein', 'tool_oauth2'), $options);
+        $mform->addHelpButton('showonloginpage', 'issuerusein', 'tool_oauth2');
+
+        // Name on login page.
+        $mform->addElement('text', 'loginpagename', get_string('issuerloginpagename', 'tool_oauth2'));
+        $mform->addRule('loginpagename', get_string('maximumchars', '', 255), 'maxlength', 255, 'client');
+        $mform->addHelpButton('loginpagename', 'issuerloginpagename', 'tool_oauth2');
+        $mform->hideIf('loginpagename', 'showonloginpage', 'eq', \core\oauth2\issuer::SERVICEONLY);
+
         // Login scopes.
         $mform->addElement('text', 'loginscopes', get_string('issuerloginscopes', 'tool_oauth2'));
-        $mform->addRule('loginscopes', null, 'required', null, 'client');
         $mform->addRule('loginscopes', get_string('maximumchars', '', 255), 'maxlength', 255, 'client');
         $mform->addHelpButton('loginscopes', 'issuerloginscopes', 'tool_oauth2');
+        $mform->hideIf('loginscopes', 'showonloginpage', 'eq', \core\oauth2\issuer::SERVICEONLY);
 
         // Login scopes offline.
         $mform->addElement('text', 'loginscopesoffline', get_string('issuerloginscopesoffline', 'tool_oauth2'));
-        $mform->addRule('loginscopesoffline', null, 'required', null, 'client');
         $mform->addRule('loginscopesoffline', get_string('maximumchars', '', 255), 'maxlength', 255, 'client');
         $mform->addHelpButton('loginscopesoffline', 'issuerloginscopesoffline', 'tool_oauth2');
+        $mform->hideIf('loginscopesoffline', 'showonloginpage', 'eq', \core\oauth2\issuer::SERVICEONLY);
 
         // Login params.
         $mform->addElement('text', 'loginparams', get_string('issuerloginparams', 'tool_oauth2'));
         $mform->addRule('loginparams', get_string('maximumchars', '', 255), 'maxlength', 255, 'client');
         $mform->addHelpButton('loginparams', 'issuerloginparams', 'tool_oauth2');
+        $mform->hideIf('loginparams', 'showonloginpage', 'eq', \core\oauth2\issuer::SERVICEONLY);
 
         // Login params offline.
         $mform->addElement('text', 'loginparamsoffline', get_string('issuerloginparamsoffline', 'tool_oauth2'));
         $mform->addRule('loginparamsoffline', get_string('maximumchars', '', 255), 'maxlength', 255, 'client');
         $mform->addHelpButton('loginparamsoffline', 'issuerloginparamsoffline', 'tool_oauth2');
-
-        // Base Url.
-        $mform->addElement('text', 'baseurl', get_string('issuerbaseurl', 'tool_oauth2'));
-        $mform->addRule('baseurl', get_string('maximumchars', '', 1024), 'maxlength', 1024, 'client');
-        $mform->addHelpButton('baseurl', 'issuerbaseurl', 'tool_oauth2');
-        if ($this->type && $this->type == 'nextcloud') {
-            $mform->addRule('baseurl', null, 'required', null, 'client');
-        }
+        $mform->hideIf('loginparamsoffline', 'showonloginpage', 'eq', \core\oauth2\issuer::SERVICEONLY);
 
         // Allowed Domains.
         $mform->addElement('text', 'alloweddomains', get_string('issueralloweddomains', 'tool_oauth2'));
         $mform->addRule('alloweddomains', get_string('maximumchars', '', 1024), 'maxlength', 1024, 'client');
         $mform->addHelpButton('alloweddomains', 'issueralloweddomains', 'tool_oauth2');
-
-        // Image.
-        $mform->addElement('text', 'image', get_string('issuerimage', 'tool_oauth2'), 'maxlength="1024"');
-        $mform->addRule('image', get_string('maximumchars', '', 1024), 'maxlength', 1024, 'client');
-        $mform->addHelpButton('image', 'issuername', 'tool_oauth2');
-
-        // Show on login page.
-        $mform->addElement('checkbox', 'showonloginpage', get_string('issuershowonloginpage', 'tool_oauth2'));
-        $mform->addHelpButton('showonloginpage', 'issuershowonloginpage', 'tool_oauth2');
+        $mform->hideIf('alloweddomains', 'showonloginpage', 'eq', \core\oauth2\issuer::SERVICEONLY);
 
         if ($this->showrequireconfirm) {
             // Require confirmation email for new accounts.
             $mform->addElement('advcheckbox', 'requireconfirmation', get_string('issuerrequireconfirmation', 'tool_oauth2'));
             $mform->addHelpButton('requireconfirmation', 'issuerrequireconfirmation', 'tool_oauth2');
+            $mform->hideIf('requireconfirmation', 'showonloginpage', 'eq', \core\oauth2\issuer::SERVICEONLY);
         }
 
         if ($this->type == 'imsobv2p1' || $issuer->get('servicetype') == 'imsobv2p1') {
@@ -209,4 +224,36 @@ class issuer extends persistent {
             $mform->getElement('servicetype')->setValue($this->type);
         }
     }
+
+    /**
+     * Define extra validation mechanims.
+     *
+     * The data here:
+     * - does not include {@see self::$fieldstoremove}.
+     * - does include {@see self::$foreignfields}.
+     * - was converted to map persistent-like data, e.g. array $description to string $description + int $descriptionformat.
+     *
+     * You can modify the $errors parameter in order to remove some validation errors should you
+     * need to. However, the best practice is to return new or overriden errors. Only modify the
+     * errors passed by reference when you have no other option.
+     *
+     * Do not add any logic here, it is only intended to be used by child classes.
+     *
+     * @param  stdClass $data Data to validate.
+     * @param  array $files Array of files.
+     * @param  array $errors Currently reported errors.
+     * @return array of additional errors, or overridden errors.
+     */
+    protected function extra_validation($data, $files, array &$errors) {
+        $errors = [];
+        if ($data->showonloginpage != \core\oauth2\issuer::SERVICEONLY) {
+            if (!strlen(trim($data->loginscopes))) {
+                $errors['loginscopes'] = get_string('required');
+            }
+            if (!strlen(trim($data->loginscopesoffline))) {
+                $errors['loginscopesoffline'] = get_string('required');
+            }
+        }
+        return $errors;
+    }
 }
index b838d0a..cb79978 100644 (file)
@@ -53,8 +53,9 @@ class renderer extends plugin_renderer_base {
         $table = new html_table();
         $table->head  = [
             get_string('name'),
-            get_string('configuredstatus', 'tool_oauth2'),
-            get_string('loginissuer', 'tool_oauth2'),
+            get_string('issuerusedforlogin', 'tool_oauth2'),
+            get_string('logindisplay', 'tool_oauth2'),
+            get_string('issuerusedforinternal', 'tool_oauth2'),
             get_string('discoverystatus', 'tool_oauth2') . ' ' . $this->help_icon('discovered', 'tool_oauth2'),
             get_string('systemauthstatus', 'tool_oauth2') . ' ' . $this->help_icon('systemaccountconnected', 'tool_oauth2'),
             get_string('edit'),
@@ -84,21 +85,29 @@ class renderer extends plugin_renderer_base {
             $namecell = new html_table_cell($name);
             $namecell->header = true;
 
-            // Configured.
-            if ($issuer->is_configured()) {
-                $configured = $this->pix_icon('yes', get_string('configured', 'tool_oauth2'), 'tool_oauth2');
+            // Login issuer.
+            if ((int)$issuer->get('showonloginpage') == issuer::SERVICEONLY) {
+                $loginissuer = $this->pix_icon('no', get_string('notloginissuer', 'tool_oauth2'), 'tool_oauth2');
+                $logindisplayas = '';
             } else {
-                $configured = $this->pix_icon('no', get_string('notconfigured', 'tool_oauth2'), 'tool_oauth2');
+                $logindisplayas = s($issuer->get_display_name());
+                if ($issuer->get('id') && $issuer->is_configured() && !empty($issuer->get_endpoint_url('userinfo'))) {
+                    $loginissuer = $this->pix_icon('yes', get_string('loginissuer', 'tool_oauth2'), 'tool_oauth2');
+                } else {
+                    $loginissuer = $this->pix_icon('notconfigured', get_string('notconfigured', 'tool_oauth2'), 'tool_oauth2');
+                }
             }
-            $configuredstatuscell = new html_table_cell($configured);
+            $loginissuerstatuscell = new html_table_cell($loginissuer);
 
-            // Login issuer.
-            if (!empty($issuer->get('showonloginpage'))) {
-                $loginissuer = $this->pix_icon('yes', get_string('loginissuer', 'tool_oauth2'), 'tool_oauth2');
+            // Internal services issuer.
+            if ((int)$issuer->get('showonloginpage') == issuer::LOGINONLY) {
+                $serviceissuer = $this->pix_icon('no', get_string('issuersservicesnotallow', 'tool_oauth2'), 'tool_oauth2');
+            } else if ($issuer->get('id') && $issuer->is_configured()) {
+                $serviceissuer = $this->pix_icon('yes', get_string('issuersservicesallow', 'tool_oauth2'), 'tool_oauth2');
             } else {
-                $loginissuer = $this->pix_icon('no', get_string('notloginissuer', 'tool_oauth2'), 'tool_oauth2');
+                $serviceissuer = $this->pix_icon('notconfigured', get_string('notconfigured', 'tool_oauth2'), 'tool_oauth2');
             }
-            $loginissuerstatuscell = new html_table_cell($loginissuer);
+            $internalissuerstatuscell = new html_table_cell($serviceissuer);
 
             // Discovered.
             if (!empty($issuer->get('scopessupported'))) {
@@ -186,13 +195,18 @@ class renderer extends plugin_renderer_base {
 
             $row = new html_table_row([
                 $namecell,
-                $configuredstatuscell,
                 $loginissuerstatuscell,
+                $logindisplayas,
+                $internalissuerstatuscell,
                 $discoverystatuscell,
                 $systemauthstatuscell,
                 $editcell,
             ]);
 
+            if (!$issuer->get('enabled')) {
+                $row->attributes['class'] = 'dimmed_text';
+            }
+
             $data[] = $row;
             $index++;
         }
index cc5da40..ad720c4 100644 (file)
@@ -53,7 +53,7 @@ if ($action == 'edit') {
     if ($issuer) {
         $PAGE->navbar->add(get_string('editissuer', 'tool_oauth2', s($issuer->get('name'))));
     } else {
-        $PAGE->navbar->add(get_string('createnewservice', 'tool_oauth2') . get_string('custom_service', 'tool_oauth2'));
+        $PAGE->navbar->add(get_string('createnewservice', 'tool_oauth2') . ' ' . get_string('custom_service', 'tool_oauth2'));
     }
 
     $showrequireconfirm = false;
@@ -96,7 +96,7 @@ if ($mform && $mform->is_cancelled()) {
         if ($issuer) {
             echo $OUTPUT->heading(get_string('editissuer', 'tool_oauth2', s($issuer->get('name'))));
         } else {
-            echo $OUTPUT->heading(get_string('createnewservice', 'tool_oauth2') . get_string('custom_service', 'tool_oauth2'));
+            echo $OUTPUT->heading(get_string('createnewservice', 'tool_oauth2') . ' ' . get_string('custom_service', 'tool_oauth2'));
         }
         $mform->display();
         echo $OUTPUT->footer();
@@ -115,7 +115,7 @@ if ($mform && $mform->is_cancelled()) {
         redirect($PAGE->url, get_string('changessaved'), null, \core\output\notification::NOTIFY_SUCCESS);
     } else {
         echo $OUTPUT->header();
-        echo $OUTPUT->heading(get_string('createnewservice', 'tool_oauth2') . get_string($type . '_service', 'tool_oauth2'));
+        echo $OUTPUT->heading(get_string('createnewservice', 'tool_oauth2') . ' ' . get_string($type . '_service', 'tool_oauth2'));
         $mform->display();
         echo $OUTPUT->footer();
     }
@@ -130,9 +130,9 @@ if ($mform && $mform->is_cancelled()) {
     $mform = new \tool_oauth2\form\issuer(null, ['persistent' => $issuer, 'type' => $type,
         'showrequireconfirm' => $showrequireconfirm]);
 
-    $PAGE->navbar->add(get_string('createnewservice', 'tool_oauth2') . get_string($type . '_service', 'tool_oauth2'));
+    $PAGE->navbar->add(get_string('createnewservice', 'tool_oauth2') . ' ' . get_string($type . '_service', 'tool_oauth2'));
     echo $OUTPUT->header();
-    echo $OUTPUT->heading(get_string('createnewservice', 'tool_oauth2') . get_string($type . '_service', 'tool_oauth2'));
+    echo $OUTPUT->heading(get_string('createnewservice', 'tool_oauth2') . ' ' . get_string($type . '_service', 'tool_oauth2'));
     $mform->display();
     echo $OUTPUT->footer();
 
@@ -195,11 +195,11 @@ if ($mform && $mform->is_cancelled()) {
     echo $OUTPUT->header();
     echo $OUTPUT->heading(get_string('pluginname', 'tool_oauth2'));
     echo $OUTPUT->doc_link('OAuth2_Services', get_string('serviceshelp', 'tool_oauth2'));
-    $issuers = core\oauth2\api::get_all_issuers();
+    $issuers = core\oauth2\api::get_all_issuers(true);
     echo $renderer->issuers_table($issuers);
 
     echo $renderer->container_start();
-    echo get_string('createnewservice', 'tool_oauth2');
+    echo get_string('createnewservice', 'tool_oauth2') . ' ';
 
     // Google template.
     $docs = 'admin/tool/oauth2/issuers/google';
index 938c576..b929911 100644 (file)
@@ -31,7 +31,7 @@ $string['connectsystemaccount'] = 'Connect to a system account';
 $string['createfromtemplate'] = 'Create an OAuth 2 service from a template';
 $string['createfromtemplatedesc'] = 'Choose one of the OAuth 2 service templates below to create an OAuth service with a valid configuration for one of the known service types. This will create the OAuth 2 service, with all the correct end points and parameters required for authentication, though you will still need to enter the client ID and secret for the new service before it can be used.';
 $string['createnewendpoint'] = 'Create new endpoint for issuer "{$a}"';
-$string['createnewservice'] = 'Create new service: ';
+$string['createnewservice'] = 'Create new service:';
 $string['createnewuserfieldmapping'] = 'Create new user field mapping for issuer "{$a}"';
 $string['custom_service'] = 'Custom';
 $string['deleteconfirm'] = 'Are you sure you want to delete the identity issuer "{$a}"? Any plugins relying on this issuer will stop working.';
@@ -53,7 +53,7 @@ $string['endpointurl_help'] = 'URL for this endpoint. Must use https:// protocol
 $string['endpointurl'] = 'URL';
 $string['facebook_service'] = 'Facebook';
 $string['google_service'] = 'Google';
-$string['imsobv2p1_service'] = 'IMS OBv2.1';
+$string['imsobv2p1_service'] = 'OpenBadges';
 $string['issuersetup'] = 'Detailed instructions on configuring the common OAuth 2 services';
 $string['issuersetuptype'] = 'Detailed instructions on setting up the {$a} OAuth 2 provider';
 $string['issueralloweddomains_help'] = 'If set, this setting is a comma separated list of domains that logins will be restricted to when using this provider.';
@@ -70,6 +70,8 @@ $string['issuerdisabled'] = 'Identity issuer disabled';
 $string['issuerenabled'] = 'Identity issuer enabled';
 $string['issuerimage_help'] = 'An image URL used to show a logo for this issuer. May be displayed on login page.';
 $string['issuerimage'] = 'Logo URL';
+$string['issuerloginpagename'] = 'Name displayed on the login page';
+$string['issuerloginpagename_help'] = 'If specified, this name will be used on the login page instead of the service name above';
 $string['issuerloginparams'] = 'Additional parameters included in a login request.';
 $string['issuerloginparams_help'] = 'Some systems require additional parameters for a login request in order to read the user\'s basic profile.';
 $string['issuerloginparamsoffline'] = 'Additional parameters included in a login request for offline access.';
@@ -85,6 +87,16 @@ $string['issuershowonloginpage'] = 'Show on login page';
 $string['issuerrequireconfirmation_help'] = 'Require that all users verify their email address before they can log in with OAuth. This applies to newly created accounts as part of the login process, or when an existing Moodle account is connected to an OAuth login via matching email addresses.';
 $string['issuerrequireconfirmation'] = 'Require email verification';
 $string['issuers'] = 'Issuers';
+$string['issuersservicesallow'] = 'Allow services';
+$string['issuersservicesnotallow'] = 'Do not allow services';
+$string['issuerusein'] = 'This service will be used';
+$string['issuerusein_help'] = 'OAuth 2 services can be used in some internal services, on the login page, or both, if needed';
+$string['issueruseineverywhere'] = 'Login page and internal services';
+$string['issueruseininternalonly'] = 'Internal services only';
+$string['issueruseinloginonly'] = 'Login page only';
+$string['issuerusedforlogin'] = 'Login';
+$string['issuerusedforinternal'] = 'Internal services';
+$string['logindisplay'] = 'Display on login page as';
 $string['loginissuer'] = 'Allow login';
 $string['microsoft_service'] = 'Microsoft';
 $string['nextcloud_service'] = 'Nextcloud';
diff --git a/admin/tool/oauth2/pix/notconfigured.svg b/admin/tool/oauth2/pix/notconfigured.svg
new file mode 100644 (file)
index 0000000..300198e
--- /dev/null
@@ -0,0 +1,14 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Generator: Adobe Illustrator 25.2.1, SVG Export Plug-In . SVG Version: 6.00 Build 0)  -->
+<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
+        viewBox="0 0 16 16" style="enable-background:new 0 0 16 16;" xml:space="preserve">
+<style type="text/css">
+       .st0{fill:#FFA500;}
+</style>
+<path class="st0" d="M9.1,12.9v-1.7c0-0.1,0-0.2-0.1-0.2s-0.1-0.1-0.2-0.1H7.2c-0.1,0-0.1,0-0.2,0.1s-0.1,0.1-0.1,0.2v1.7
+       c0,0.1,0,0.2,0.1,0.2s0.1,0.1,0.2,0.1h1.7c0.1,0,0.1,0,0.2-0.1S9.1,13,9.1,12.9z M9.1,9.5l0.2-4.1c0-0.1,0-0.1-0.1-0.2
+       C9.1,5.2,9,5.2,9,5.2H7c-0.1,0-0.1,0-0.2,0.1c-0.1,0-0.1,0.1-0.1,0.2l0.2,4.1c0,0.1,0,0.1,0.1,0.1c0.1,0,0.1,0.1,0.2,0.1h1.6
+       c0.1,0,0.2,0,0.2-0.1C9.1,9.7,9.1,9.6,9.1,9.5z M9,1.3l6.8,12.5c0.2,0.4,0.2,0.7,0,1.1c-0.1,0.2-0.2,0.3-0.4,0.4s-0.4,0.2-0.6,0.2
+       H1.2c-0.2,0-0.4-0.1-0.6-0.2S0.3,15,0.2,14.9c-0.2-0.4-0.2-0.7,0-1.1L7,1.3c0.1-0.2,0.2-0.3,0.4-0.4S7.8,0.7,8,0.7s0.4,0.1,0.6,0.2
+       C8.8,0.9,8.9,1.1,9,1.3z"/>
+</svg>
index 2eb3345..1a258b3 100644 (file)
@@ -18,8 +18,8 @@ Feature: Basic OAuth2 functionality
     When I press "Save changes"
     Then I should see "Changes saved"
     And I should see "Testing service"
-    And "Configured" "icon" should exist in the "Testing service" "table_row"
     And "Allow login" "icon" should exist in the "Testing service" "table_row"
+    And "Allow services" "icon" should exist in the "Testing service" "table_row"
     And "Service discovery successful" "icon" should exist in the "Testing service" "table_row"
     And I click on "Configure endpoints" "link" in the "Testing service" "table_row"
     And I should see "https://accounts.google.com/.well-known/openid-configuration" in the "discovery_endpoint" "table_row"
@@ -51,8 +51,8 @@ Feature: Basic OAuth2 functionality
     When I press "Save changes"
     Then I should see "Changes saved"
     And I should see "Testing service"
-    And "Configured" "icon" should exist in the "Testing service" "table_row"
     And "Allow login" "icon" should exist in the "Testing service" "table_row"
+    And "Allow services" "icon" should exist in the "Testing service" "table_row"
     And I should see "-" in the "Testing service" "table_row"
     And I click on "Configure endpoints" "link" in the "Testing service" "table_row"
     And I should see "authorization_endpoint"
@@ -83,8 +83,8 @@ Feature: Basic OAuth2 functionality
     When I press "Save changes"
     Then I should see "Changes saved"
     And I should see "Testing service"
-    And "Configured" "icon" should exist in the "Testing service" "table_row"
     And "Allow login" "icon" should exist in the "Testing service" "table_row"
+    And "Allow services" "icon" should exist in the "Testing service" "table_row"
     And I should see "-" in the "Testing service" "table_row"
     And I click on "Configure endpoints" "link" in the "Testing service" "table_row"
     And I should see "authorization_endpoint"
@@ -120,8 +120,8 @@ Feature: Basic OAuth2 functionality
     When I press "Save changes"
     Then I should see "Changes saved"
     And I should see "Testing service"
-    And "Configured" "icon" should exist in the "Testing service" "table_row"
     And "Do not allow login" "icon" should exist in the "Testing service" "table_row"
+    And "Allow services" "icon" should exist in the "Testing service" "table_row"
     And I should see "-" in the "Testing service" "table_row"
     And I click on "Configure endpoints" "link" in the "Testing service" "table_row"
     And I should see "authorization_endpoint"
@@ -142,29 +142,29 @@ Feature: Basic OAuth2 functionality
     And I should see "Identity issuer deleted"
     And I should not see "Testing service modified"
 
-  Scenario: Create, edit and delete standard service for IMS OBv2.1
-    Given I press "IMS OBv2.1"
-    And I should see "Create new service: IMS OBv2.1"
+  Scenario: Create, edit and delete standard service for OpenBadges
+    Given I press "OpenBadges"
+    And I should see "Create new service: OpenBadges"
     And I set the following fields to these values:
       | Client ID                  | thisistheclientid                         |
       | Client secret              | supersecret                               |
       | Service base URL           | https://dc.imsglobal.org/                 |
     When I press "Save changes"
     Then I should see "Changes saved"
-    And I should see "IMS OBv2.1"
-    And "Configured" "icon" should exist in the "IMS OBv2.1" "table_row"
-    And "Do not allow login" "icon" should exist in the "IMS OBv2.1" "table_row"
-    And "Service discovery successful" "icon" should exist in the "IMS OBv2.1" "table_row"
+    And I should see "OpenBadges"
+    And "Allow services" "icon" should exist in the "OpenBadges" "table_row"
+    And "Do not allow login" "icon" should exist in the "OpenBadges" "table_row"
+    And "Service discovery successful" "icon" should exist in the "OpenBadges" "table_row"
     And the "src" attribute of "table.admintable th img" "css_element" should contain "IMS-Global-Logo.png"
-    And I click on "Configure endpoints" "link" in the "IMS OBv2.1" "table_row"
+    And I click on "Configure endpoints" "link" in the "OpenBadges" "table_row"
     And I should see "https://dc.imsglobal.org/.well-known/badgeconnect.json" in the "discovery_endpoint" "table_row"
     And I should see "authorization_endpoint"
     And I follow "OAuth 2 services"
-    And I click on "Configure user field mappings" "link" in the "IMS OBv2.1" "table_row"
+    And I click on "Configure user field mappings" "link" in the "OpenBadges" "table_row"
     And I should not see "given_name"
     And I should not see "middle_name"
     And I follow "OAuth 2 services"
-    And I click on "Edit" "link" in the "IMS OBv2.1" "table_row"
+    And I click on "Edit" "link" in the "OpenBadges" "table_row"
     And I set the following fields to these values:
       | Name                       | IMS Global                                |
     And I press "Save changes"
@@ -187,8 +187,8 @@ Feature: Basic OAuth2 functionality
     When I press "Save changes"
     Then I should see "Changes saved"
     And I should see "Google custom"
-    And "Configured" "icon" should exist in the "Google custom" "table_row"
     And "Do not allow login" "icon" should exist in the "Google custom" "table_row"
+    And "Allow services" "icon" should exist in the "Google custom" "table_row"
     And "Service discovery successful" "icon" should exist in the "Google custom" "table_row"
     And the "src" attribute of "table.admintable th img" "css_element" should contain "favicon.ico"
     And I click on "Configure endpoints" "link" in the "Google custom" "table_row"
@@ -222,7 +222,7 @@ Feature: Basic OAuth2 functionality
     When I press "Save changes"
     Then I should see "Could not discover end points for identity issuer: Invalid custom service"
     And I should see "URL: https://dc.imsglobal.org/.well-known/openid-configuration"
-    And "Configured" "icon" should exist in the "Invalid custom service" "table_row"
+    And "Allow services" "icon" should exist in the "Invalid custom service" "table_row"
     And "Do not allow login" "icon" should exist in the "Invalid custom service" "table_row"
     And I should see "-" in the "Invalid custom service" "table_row"
     And I click on "Configure endpoints" "link" in the "Invalid custom service" "table_row"
@@ -237,8 +237,8 @@ Feature: Basic OAuth2 functionality
       | Name                       | Valid custom service                        |
       | Service base URL           | https://accounts.google.com/                |
     And I press "Save changes"
-    And "Configured" "icon" should exist in the "Valid custom" "table_row"
     And "Do not allow login" "icon" should exist in the "Valid custom" "table_row"
+    And "Allow services" "icon" should exist in the "Valid custom" "table_row"
     And "Service discovery successful" "icon" should exist in the "Valid custom" "table_row"
     And I click on "Edit" "link" in the "Valid custom service" "table_row"
     And I set the following fields to these values:
@@ -263,7 +263,7 @@ Feature: Basic OAuth2 functionality
     When I press "Save changes"
     And I should see "Changes saved"
     And I should see "Empty custom service"
-    And "Configured" "icon" should exist in the "Empty custom service" "table_row"
+    And "Allow services" "icon" should exist in the "Empty custom service" "table_row"
     And "Do not allow login" "icon" should exist in the "Empty custom service" "table_row"
     And I should see "-" in the "Empty custom service" "table_row"
     And I click on "Configure endpoints" "link" in the "Empty custom service" "table_row"
@@ -279,8 +279,8 @@ Feature: Basic OAuth2 functionality
       | Name                       | Valid custom service                      |
       | Service base URL           | https://accounts.google.com               |
     And I press "Save changes"
-    And "Configured" "icon" should exist in the "Valid custom" "table_row"
     And "Do not allow login" "icon" should exist in the "Valid custom" "table_row"
+    And "Allow services" "icon" should exist in the "Valid custom" "table_row"
     And "Service discovery successful" "icon" should exist in the "Valid custom" "table_row"
     And I click on "Edit" "link" in the "Valid custom service" "table_row"
     And I set the following fields to these values:
@@ -301,3 +301,59 @@ Feature: Basic OAuth2 functionality
     And I press "Continue"
     And I should see "Identity issuer deleted"
     And I should not see "Empty custom service"
+
+  Scenario: Create a standard service for Google and test form and UI for login only, services only and both
+    Given I press "Google"
+    And I should see "Create new service: Google"
+    # Create using 'Login page only' option.
+    And I set the following fields to these values:
+      | Name                       | Testing service                           |
+      | Client ID                  | thisistheclientid                         |
+      | Client secret              | supersecret                               |
+      | This service will be used  | Login page only                           |
+    When I press "Save changes"
+    Then I should see "Changes saved"
+    And I should see "Testing service"
+    And "Allow login" "icon" should exist in the "Testing service" "table_row"
+    And "Do not allow services" "icon" should exist in the "Testing service" "table_row"
+    And "Service discovery successful" "icon" should exist in the "Testing service" "table_row"
+    # Change to 'Internal services only'.
+    And I click on "Edit" "link" in the "Testing service" "table_row"
+    And I set the following fields to these values:
+      | This service will be used  | Internal services only                     |
+    And I press "Save changes"
+    And I should see "Changes saved"
+    And "Do not allow login" "icon" should exist in the "Testing service" "table_row"
+    And "Allow services" "icon" should exist in the "Testing service" "table_row"
+    # Change to 'Login page and internal services' and add a display name.
+    And I click on "Edit" "link" in the "Testing service" "table_row"
+    And I set the following fields to these values:
+      | This service will be used         | Login page and internal services     |
+      | Name displayed on the login page  | Google new display name              |
+    And I press "Save changes"
+    And I should see "Changes saved"
+    And "Allow login" "icon" should exist in the "Testing service" "table_row"
+    And "Allow services" "icon" should exist in the "Testing service" "table_row"
+    And I should see "Google new display name" in the "Testing service" "table_row"
+
+  Scenario: Create a login page only custom OIDC service
+    Given I press "Custom"
+    And I should see "Create new service: Custom"
+    And I set the following fields to these values:
+      | Name                              | Empty custom service                      |
+      | Client ID                         | thisistheclientid                         |
+      | Client secret                     | supersecret                               |
+      | This service will be used         | Login page only                           |
+      | Name displayed on the login page  | Custom display name                       |
+    When I press "Save changes"
+    And I should see "Changes saved"
+    And I should see "Empty custom service"
+    And I should see "Custom display name" in the "Empty custom service" "table_row"
+    And "Not configured" "icon" should exist in the "Empty custom service" "table_row"
+    And "Do not allow services" "icon" should exist in the "Empty custom service" "table_row"
+    And I click on "Edit" "link" in the "Empty custom service" "table_row"
+    And I set the following fields to these values:
+      | Service base URL           | https://accounts.google.com               |
+    And I press "Save changes"
+    And "Allow login" "icon" should exist in the "Empty custom service" "table_row"
+    And "Do not allow services" "icon" should exist in the "Empty custom service" "table_row"
index 4204c8b..cb56918 100644 (file)
@@ -142,9 +142,8 @@ class process {
         $this->standardfields = array('id', 'username', 'email', 'emailstop',
             'city', 'country', 'lang', 'timezone', 'mailformat',
             'maildisplay', 'maildigest', 'htmleditor', 'autosubscribe',
-            'institution', 'department', 'idnumber', 'skype',
-            'msn', 'aim', 'yahoo', 'icq', 'phone1', 'phone2', 'address',
-            'url', 'description', 'descriptionformat', 'password',
+            'institution', 'department', 'idnumber', 'phone1', 'phone2', 'address',
+            'description', 'descriptionformat', 'password',
             'auth',        // Watch out when changing auth type or using external auth plugins!
             'oldusername', // Use when renaming users - this is the original username.
             'suspended',   // 1 means suspend user account, 0 means activate user account, nothing means keep as is.
index 9e90d84..8dcf4ce 100644 (file)
@@ -300,10 +300,6 @@ class admin_uploaduser_form2 extends moodleform {
         $mform->addHelpButton('description', 'userdescription');
         $mform->setAdvanced('description');
 
-        $mform->addElement('text', 'url', get_string('webpage'), 'maxlength="255" size="50"');
-        $mform->setType('url', PARAM_URL);
-        $mform->setAdvanced('url');
-
         $mform->addElement('text', 'idnumber', get_string('idnumber'), 'maxlength="255" size="25"');
         $mform->setType('idnumber', core_user::get_property_type('idnumber'));
         $mform->setForceLtr('idnumber');
index 9d76b2e..01ebf39 100644 (file)
@@ -438,17 +438,7 @@ class manager {
         $filename = 'tour_export_' . $tour->get_id() . '_' . time() . '.json';
 
         // Force download.
-        header('Last-Modified: ' . gmdate('D, d M Y H:i:s', time()) . ' GMT');
-        header('Cache-Control: private, must-revalidate, pre-check=0, post-check=0, max-age=0');
-        header('Expires: ' . gmdate('D, d M Y H:i:s', 0) . 'GMT');
-        header('Pragma: no-cache');
-        header('Accept-Ranges: none');
-        header('Content-disposition: attachment; filename=' . $filename);
-        header('Content-length: ' . strlen($exportstring));
-        header('Content-type: text/calendar; charset=utf-8');
-
-        echo $exportstring;
-        die;
+        send_file($exportstring, $filename, 0, 0, true, true);
     }
 
     /**
index 4e5e8d3..ffa3f23 100644 (file)
@@ -48,12 +48,6 @@ if ($dataformat) {
                     'phone1'    => 'phone1',
                     'phone2'    => 'phone2',
                     'city'      => 'city',
-                    'url'       => 'url',
-                    'icq'       => 'icq',
-                    'skype'     => 'skype',
-                    'aim'       => 'aim',
-                    'yahoo'     => 'yahoo',
-                    'msn'       => 'msn',
                     'country'   => 'country');
 
     if ($extrafields = $DB->get_records('user_info_field')) {
index 6b0929e..3b0aced 100644 (file)
@@ -178,6 +178,33 @@ class prediction {
         \core\event\prediction_action_started::create($eventdata)->trigger();
     }
 
+    /**
+     * Get the executed actions.
+     *
+     * Actions could be filtered by actionname.
+     *
+     * @param array $actionnamefilter Limit the results obtained to this list of action names.
+     * @param int $userid the user id. Current user by default.
+     * @return array of actions.
+     */
+    public function get_executed_actions(array $actionnamefilter = null, int $userid = 0): array {
+        global $USER, $DB;
+
+        $conditions[] = "predictionid = :predictionid";
+        $params['predictionid'] = $this->get_prediction_data()->id;
+        if (!$userid) {
+            $userid = $USER->id;
+        }
+        $conditions[] = "userid = :userid";
+        $params['userid'] = $userid;
+        if ($actionnamefilter) {
+            list($actionsql, $actionparams) = $DB->get_in_or_equal($actionnamefilter, SQL_PARAMS_NAMED);
+            $conditions[] = "actionname $actionsql";
+            $params = $params + $actionparams;
+        }
+        return $DB->get_records_select('analytics_prediction_actions', implode(' AND ', $conditions), $params);
+    }
+
     /**
      * format_calculations
      *
index f8f2866..8aa6c24 100644 (file)
@@ -112,6 +112,86 @@ class analytics_prediction_actions_testcase extends advanced_testcase {
         $this->assertEquals(2, $DB->count_records('analytics_prediction_actions'));
     }
 
+    /**
+     * Data provider for test_get_executed_actions.
+     *
+     * @return  array
+     */
+    public function execute_actions_provider(): array {
+        return [
+            'Empty actions with no filter' => [
+                [],
+                [],
+                0
+            ],
+            'Empty actions with filter' => [
+                [],
+                [\core_analytics\prediction::ACTION_FIXED],
+                0
+            ],
+            'Multiple actions with no filter' => [
+                [
+                    \core_analytics\prediction::ACTION_FIXED,
+                    \core_analytics\prediction::ACTION_FIXED,
+                    \core_analytics\prediction::ACTION_INCORRECTLY_FLAGGED
+                ],
+                [],
+                3
+            ],
+            'Multiple actions applying filter' => [
+                [
+                    \core_analytics\prediction::ACTION_FIXED,
+                    \core_analytics\prediction::ACTION_FIXED,
+                    \core_analytics\prediction::ACTION_INCORRECTLY_FLAGGED
+                ],
+                [\core_analytics\prediction::ACTION_FIXED],
+                2
+            ],
+            'Multiple actions not applying filter' => [
+                [
+                    \core_analytics\prediction::ACTION_FIXED,
+                    \core_analytics\prediction::ACTION_FIXED,
+                    \core_analytics\prediction::ACTION_INCORRECTLY_FLAGGED
+                ],
+                [\core_analytics\prediction::ACTION_NOT_APPLICABLE],
+                0
+            ],
+            'Multiple actions with multiple filter' => [
+                [
+                    \core_analytics\prediction::ACTION_FIXED,
+                    \core_analytics\prediction::ACTION_FIXED,
+                    \core_analytics\prediction::ACTION_INCORRECTLY_FLAGGED
+                ],
+                [\core_analytics\prediction::ACTION_FIXED, \core_analytics\prediction::ACTION_INCORRECTLY_FLAGGED],
+                3
+            ],
+        ];
+    }
+
+    /**
+     * Tests for get_executed_actions() function.
+     *
+     * @dataProvider    execute_actions_provider
+     * @param   array   $actionstoexecute    An array of actions to execute
+     * @param   array   $actionnamefilter   Actions to filter
+     * @param   int     $returned             Number of actions returned
+     *
+     * @covers \core_analytics\prediction::get_executed_actions
+     */
+    public function test_get_executed_actions(array $actionstoexecute, array $actionnamefilter, int $returned) {
+
+        $this->setUser($this->teacher2);
+        list($ignored, $predictions) = $this->model->get_predictions($this->context, true);
+        $prediction = reset($predictions);
+        $target = $this->model->get_target();
+        foreach($actionstoexecute as $action) {
+            $prediction->action_executed($action, $target);
+        }
+
+        $filteredactions = $prediction->get_executed_actions($actionnamefilter);
+        $this->assertCount($returned, $filteredactions);
+    }
+
     /**
      * test_get_predictions
      */
index b23200c..0798670 100644 (file)
@@ -11,6 +11,8 @@ information provided here is intended especially for developers.
   by updating the lib/db/analytics.php file and bumping the core version.
 * Final deprecation - get_analysables(). Please see get_analysables_interator() instead.
   get_analysables_iterator() needs to be overridden by the child class.
+* A new function get_executed_actions() has been added to \core_analytics\prediction class
+  to get all (or filtered by action name) executed actions of a prediction
 
 === 3.8 ===
 
index c774de1..c1a95c6 100644 (file)
@@ -81,7 +81,6 @@ class provider implements
 
         $externalfields = [
                 'address' => 'privacy:metadata:mnet_external:address',
-                'aim' => 'privacy:metadata:mnet_external:aim',
                 'alternatename' => 'privacy:metadata:mnet_external:alternatename',
                 'autosubscribe' => 'privacy:metadata:mnet_external:autosubscribe',
                 'calendartype' => 'privacy:metadata:mnet_external:calendartype',
@@ -95,7 +94,6 @@ class provider implements
                 'firstaccess' => 'privacy:metadata:mnet_external:firstaccess',
                 'firstname' => 'privacy:metadata:mnet_external:firstname',
                 'firstnamephonetic' => 'privacy:metadata:mnet_external:firstnamephonetic',
-                'icq' => 'privacy:metadata:mnet_external:icq',
                 'id' => 'privacy:metadata:mnet_external:id',
                 'idnumber' => 'privacy:metadata:mnet_external:idnumber',
                 'imagealt' => 'privacy:metadata:mnet_external:imagealt',
@@ -108,19 +106,15 @@ class provider implements
                 'maildigest' => 'privacy:metadata:mnet_external:maildigest',
                 'maildisplay' => 'privacy:metadata:mnet_external:maildisplay',
                 'middlename' => 'privacy:metadata:mnet_external:middlename',
-                'msn' => 'privacy:metadata:mnet_external:msn',
                 'phone1' => 'privacy:metadata:mnet_external:phone1',
                 'pnone2' => 'privacy:metadata:mnet_external:phone2',
                 'picture' => 'privacy:metadata:mnet_external:picture',
                 'policyagreed' => 'privacy:metadata:mnet_external:policyagreed',
-                'skype' => 'privacy:metadata:mnet_external:skype',
                 'suspended' => 'privacy:metadata:mnet_external:suspended',
                 'timezone' => 'privacy:metadata:mnet_external:timezone',
                 'trackforums' => 'privacy:metadata:mnet_external:trackforums',
                 'trustbitmask' => 'privacy:metadata:mnet_external:trustbitmask',
-                'url' => 'privacy:metadata:mnet_external:url',
                 'username' => 'privacy:metadata:mnet_external:username',
-                'yahoo' => 'privacy:metadata:mnet_external:yahoo',
         ];
 
         $collection->add_external_location_link('moodle', $externalfields, 'privacy:metadata:external:moodle');
index 4836214..d07f3a6 100644 (file)
@@ -39,7 +39,6 @@ $string['pluginname'] = 'MNet authentication';
 $string['privacy:metadata:external:mahara'] = 'This plugin can send data externally to a linked Mahara application.';
 $string['privacy:metadata:external:moodle'] = 'This plugin can send data externally to a linked Moodle application.';
 $string['privacy:metadata:mnet_external:address'] = 'The address of the user.';
-$string['privacy:metadata:mnet_external:aim'] = 'The AIM identifier of the user';
 $string['privacy:metadata:mnet_external:alternatename'] = 'An alternative name for the user.';
 $string['privacy:metadata:mnet_external:autosubscribe'] = 'A preference as to if the user should be auto-subscribed to forums the user posts in.';
 $string['privacy:metadata:mnet_external:calendartype'] = 'A user preference for the type of calendar to use.';
@@ -53,7 +52,6 @@ $string['privacy:metadata:mnet_external:emailstop'] = 'A preference to stop emai
 $string['privacy:metadata:mnet_external:firstaccess'] = 'The time that this user first accessed the site.';
 $string['privacy:metadata:mnet_external:firstname'] = 'The first name of the user.';
 $string['privacy:metadata:mnet_external:firstnamephonetic'] = 'The phonetic details about the user\'s first name.';
-$string['privacy:metadata:mnet_external:icq'] = 'The ICQ number of the user.';
 $string['privacy:metadata:mnet_external:id'] = 'The user ID';
 $string['privacy:metadata:mnet_external:idnumber'] = 'An identification number given by the institution';
 $string['privacy:metadata:mnet_external:imagealt'] = 'Alternative text for the user\'s image.';
@@ -66,19 +64,15 @@ $string['privacy:metadata:mnet_external:lastnamephonetic'] = 'The phonetic detai
 $string['privacy:metadata:mnet_external:maildigest'] = 'A setting for the mail digest for this user.';
 $string['privacy:metadata:mnet_external:maildisplay'] = 'A preference for the user about displaying their email address to other users.';
 $string['privacy:metadata:mnet_external:middlename'] = 'The middle name of the user';
-$string['privacy:metadata:mnet_external:msn'] = 'The MSN identifier of the user';
 $string['privacy:metadata:mnet_external:phone1'] = 'A phone number for the user.';
 $string['privacy:metadata:mnet_external:phone2'] = 'An additional phone number for the user.';
 $string['privacy:metadata:mnet_external:picture'] = 'The picture details associated with this user.';
 $string['privacy:metadata:mnet_external:policyagreed'] = 'A flag to determine if the user has agreed to the site policy.';
-$string['privacy:metadata:mnet_external:skype'] = 'The Skype identifier of the user';
 $string['privacy:metadata:mnet_external:suspended'] = 'A flag to show if the user has been suspended on this system.';
 $string['privacy:metadata:mnet_external:timezone'] = 'The timezone of the user';
 $string['privacy:metadata:mnet_external:trackforums'] = 'A preference for forums and tracking them.';
 $string['privacy:metadata:mnet_external:trustbitmask'] = 'The trust bit mask';
-$string['privacy:metadata:mnet_external:url'] = 'A URL related to this user.';
 $string['privacy:metadata:mnet_external:username'] = 'The username for this user.';
-$string['privacy:metadata:mnet_external:yahoo'] = 'The Yahoo identifier of the user';
 $string['privacy:metadata:mnet_log'] = 'Details of remote actions carried out by a local user logged in a remote system.';
 $string['privacy:metadata:mnet_log:action'] = 'Action carried out by the user.';
 $string['privacy:metadata:mnet_log:cmid'] = 'ID of the course module.';
@@ -99,4 +93,12 @@ $string['privacy:metadata:mnet_session:token'] = 'Unique session identifier';
 $string['privacy:metadata:mnet_session:useragent'] = 'User agent used to access the remote system';
 $string['privacy:metadata:mnet_session:userid'] = 'ID of the user jumping to remote system.';
 $string['privacy:metadata:mnet_session:username'] = 'Username of the user jumping to remote system.';
-$string['unknownhost'] = 'Unknown host';
\ No newline at end of file
+$string['unknownhost'] = 'Unknown host';
+
+// Deprecated since Moodle 4.0.
+$string['privacy:metadata:mnet_external:aim'] = 'The AIM identifier of the user';
+$string['privacy:metadata:mnet_external:icq'] = 'The ICQ number of the user.';
+$string['privacy:metadata:mnet_external:msn'] = 'The MSN identifier of the user';
+$string['privacy:metadata:mnet_external:skype'] = 'The Skype identifier of the user';
+$string['privacy:metadata:mnet_external:url'] = 'A URL related to this user.';
+$string['privacy:metadata:mnet_external:yahoo'] = 'The Yahoo identifier of the user';
index e5b0789..5c3545e 100644 (file)
@@ -258,7 +258,6 @@ class api {
         $user->mnethostid = $CFG->mnet_localhost_id;
         $user->lastname = isset($userinfo['lastname']) ? $userinfo['lastname'] : '';
         $user->firstname = isset($userinfo['firstname']) ? $userinfo['firstname'] : '';
-        $user->url = isset($userinfo['url']) ? $userinfo['url'] : '';
         $user->alternatename = isset($userinfo['alternatename']) ? $userinfo['alternatename'] : '';
         $user->secret = random_string(15);
 
@@ -307,7 +306,6 @@ class api {
         $user->mnethostid = $CFG->mnet_localhost_id;
         $user->lastname = isset($userinfo['lastname']) ? $userinfo['lastname'] : '';
         $user->firstname = isset($userinfo['firstname']) ? $userinfo['firstname'] : '';
-        $user->url = isset($userinfo['url']) ? $userinfo['url'] : '';
         $user->alternatename = isset($userinfo['alternatename']) ? $userinfo['alternatename'] : '';
         $user->secret = random_string(15);
 
index 4152729..251adbf 100644 (file)
@@ -166,17 +166,6 @@ class auth extends \auth_plugin_base {
         return false;
     }
 
-    /**
-     * Do some checks on the identity provider before showing it on the login page.
-     * @param core\oauth2\issuer $issuer
-     * @return boolean
-     */
-    private function is_ready_for_login_page(\core\oauth2\issuer $issuer) {
-        return $issuer->get('enabled') &&
-                $issuer->is_configured() &&
-                !empty($issuer->get('showonloginpage'));
-    }
-
     /**
      * Return a list of identity providers to display on the login page.
      *
@@ -184,17 +173,17 @@ class auth extends \auth_plugin_base {
      * @return array List of arrays with keys url, iconurl and name.
      */
     public function loginpage_idp_list($wantsurl) {
-        $providers = \core\oauth2\api::get_all_issuers();
+        $providers = \core\oauth2\api::get_all_issuers(true);
         $result = [];
         if (empty($wantsurl)) {
             $wantsurl = '/';
         }
         foreach ($providers as $idp) {
-            if ($this->is_ready_for_login_page($idp)) {
+            if ($idp->is_available_for_login()) {
                 $params = ['id' => $idp->get('id'), 'wantsurl' => $wantsurl, 'sesskey' => sesskey()];
                 $url = new moodle_url('/auth/oauth2/login.php', $params);
                 $icon = $idp->get('image');
-                $result[] = ['url' => $url, 'iconurl' => $icon, 'name' => $idp->get('name')];
+                $result[] = ['url' => $url, 'iconurl' => $icon, 'name' => $idp->get_display_name()];
             }
         }
         return $result;
index 1243560..07a0428 100644 (file)
@@ -28,7 +28,7 @@ use html_table;
 use html_table_cell;
 use html_table_row;
 use html_writer;
-use auth\oauth2\linked_login;
+use auth_oauth2\linked_login;
 use moodle_url;
 
 defined('MOODLE_INTERNAL') || die();
@@ -43,7 +43,7 @@ class renderer extends plugin_renderer_base {
     /**
      * This function will render one beautiful table with all the linked_logins.
      *
-     * @param \auth\oauth2\linked_login[] $linkedlogins - list of all linked logins.
+     * @param linked_login[] $linkedlogins - list of all linked logins.
      * @return string HTML to output.
      */
     public function linked_logins_table($linkedlogins) {
index 146e261..30df5d4 100644 (file)
@@ -45,7 +45,7 @@ if ($action == 'new') {
     $issuerid = required_param('issuerid', PARAM_INT);
     $issuer = \core\oauth2\api::get_issuer($issuerid);
 
-    if (!$issuer->is_authentication_supported() || !$issuer->get('showonloginpage') || !$issuer->get('enabled')) {
+    if (!$issuer->is_available_for_login()) {
         throw new \moodle_exception('issuernologin', 'auth_oauth2');
     }
 
@@ -89,19 +89,20 @@ $linkedlogin = null;
 
 auth_oauth2\api::clean_orphaned_linked_logins();
 
-$issuers = \core\oauth2\api::get_all_issuers();
+$issuers = \core\oauth2\api::get_all_issuers(true);
 
 $anyshowinloginpage = false;
 $issuerbuttons = array();
 foreach ($issuers as $issuer) {
-    if (!$issuer->is_authentication_supported() || !$issuer->get('showonloginpage') || !$issuer->get('enabled')) {
+    if (!$issuer->is_available_for_login()) {
         continue;
     }
     $anyshowinloginpage = true;
 
     $addparams = ['action' => 'new', 'issuerid' => $issuer->get('id'), 'sesskey' => sesskey(), 'logout' => true];
     $addurl = new moodle_url('/auth/oauth2/linkedlogins.php', $addparams);
-    $issuerbuttons[$issuer->get('id')] = $renderer->single_button($addurl, get_string('createnewlinkedlogin', 'auth_oauth2', s($issuer->get('name'))));
+    $issuerbuttons[$issuer->get('id')] = $renderer->single_button($addurl, get_string('createnewlinkedlogin', 'auth_oauth2',
+        s($issuer->get_display_name())));
 }
 
 if (!$anyshowinloginpage) {
index 9abb2a3..949b5a1 100644 (file)
@@ -37,6 +37,9 @@ if (!\auth_oauth2\api::is_enabled()) {
 }
 
 $issuer = new \core\oauth2\issuer($issuerid);
+if (!$issuer->is_available_for_login()) {
+    throw new \moodle_exception('issuernologin', 'auth_oauth2');
+}
 
 $returnparams = ['wantsurl' => $wantsurl, 'sesskey' => sesskey(), 'id' => $issuerid];
 $returnurl = new moodle_url('/auth/oauth2/login.php', $returnparams);
index 37fda53..d6a311d 100644 (file)
@@ -49,12 +49,6 @@ class frontend extends \core_availability\frontend {
             'email' => \core_user\fields::get_display_name('email'),
             'city' => \core_user\fields::get_display_name('city'),
             'country' => \core_user\fields::get_display_name('country'),
-            'url' => \core_user\fields::get_display_name('url'),
-            'icq' => \core_user\fields::get_display_name('icq'),
-            'skype' => \core_user\fields::get_display_name('skype'),
-            'aim' => \core_user\fields::get_display_name('aim'),
-            'yahoo' => \core_user\fields::get_display_name('yahoo'),
-            'msn' => \core_user\fields::get_display_name('msn'),
             'idnumber' => \core_user\fields::get_display_name('idnumber'),
             'institution' => \core_user\fields::get_display_name('institution'),
             'department' => \core_user\fields::get_display_name('department'),
index d2ac8c5..2804661 100644 (file)
@@ -125,6 +125,8 @@ class backup_course_task extends backup_task {
             $this->add_step(new backup_course_logs_structure_step('course_logs', 'logs.xml'));
             // New log stores.
             $this->add_step(new backup_course_logstores_structure_step('course_logstores', 'logstores.xml'));
+            // Last access to course logs.
+            $this->add_step(new backup_course_loglastaccess_structure_step('course_loglastaccess', 'loglastaccess.xml'));
         }
 
         // Generate the course competencies.
index 478ae2a..673b58b 100644 (file)
@@ -99,6 +99,13 @@ class backup_root_task extends backup_task {
         $this->add_setting($roleassignments);
         $users->add_dependency($roleassignments);
 
+        // Define permission.
+        if ($this->plan->get_mode() == backup::MODE_IMPORT) {
+            $permissions = new backup_permissions_setting('permissions', base_setting::IS_BOOLEAN, false);
+            $permissions->set_ui(new backup_setting_ui_checkbox($permissions, get_string('rootsettingpermissions', 'backup')));
+            $this->add_setting($permissions);
+        }
+
         // Define activities
         $activities = new backup_activities_setting('activities', base_setting::IS_BOOLEAN, true);
         $activities->set_ui(new backup_setting_ui_checkbox($activities, get_string('rootsettingactivities', 'backup')));
index 59ea252..ca9224f 100644 (file)
@@ -65,6 +65,12 @@ class backup_filename_setting extends backup_generic_setting {
  */
 class backup_users_setting extends backup_generic_setting {}
 
+/**
+ * root setting to control if backup will include permission information by roles
+ */
+class backup_permissions_setting extends backup_generic_setting {
+}
+
 /**
  * root setting to control if backup will include group information depends on @backup_users_setting
  *
index 4339b0c..9f87ba4 100644 (file)
@@ -1360,11 +1360,10 @@ class backup_users_structure_step extends backup_structure_step {
 
         // Then, the fields potentially needing anonymization
         $anonfields = array(
-            'username', 'idnumber', 'email', 'icq', 'skype',
-            'yahoo', 'aim', 'msn', 'phone1',
+            'username', 'idnumber', 'email', 'phone1',
             'phone2', 'institution', 'department', 'address',
             'city', 'country', 'lastip', 'picture',
-            'url', 'description', 'descriptionformat', 'imagealt', 'auth');
+            'description', 'descriptionformat', 'imagealt', 'auth');
         $anonfields = array_merge($anonfields, \core_user\fields::get_name_fields());
 
         // Add anonymized fields to $userfields with custom final element
@@ -1639,6 +1638,55 @@ class backup_course_logstores_structure_step extends backup_structure_step {
     }
 }
 
+/**
+ * Structure step in charge of constructing the loglastaccess.xml file for the course logs.
+ *
+ * This backup step will backup the logs of the user_lastaccess table.
+ */
+class backup_course_loglastaccess_structure_step extends backup_structure_step {
+
+    /**
+     *  This function creates the structures for the loglastaccess.xml file.
+     *  Expected structure would look like this.
+     *  <loglastaccesses>
+     *      <loglastaccess id=2>
+     *          <userid>5</userid>
+     *          <timeaccess>1616887341</timeaccess>
+     *      </loglastaccess>
+     *  </loglastaccesses>
+     *
+     * @return backup_nested_element
+     */
+    protected function define_structure() {
+
+        // To know if we are including userinfo.
+        $userinfo = $this->get_setting_value('users');
+
+        // Define the structure of logstores container.
+        $lastaccesses = new backup_nested_element('lastaccesses');
+        $lastaccess = new backup_nested_element('lastaccess', array('id'), array('userid', 'timeaccess'));
+
+        // Define build tree.
+        $lastaccesses->add_child($lastaccess);
+
+        // This element should only happen if we are including user info.
+        if ($userinfo) {
+            // Define sources.
+            $lastaccess->set_source_sql('
+                SELECT id, userid, timeaccess
+                  FROM {user_lastaccess}
+                 WHERE courseid = ?',
+                array(backup::VAR_COURSEID));
+
+            // Define userid annotation to user.
+            $lastaccess->annotate_ids('user', 'userid');
+        }
+
+        // Return the root element (lastaccessess).
+        return $lastaccesses;
+    }
+}
+
 /**
  * Structure step in charge of constructing the logstores.xml file for the activity logs.
  *
index b0b6b76..83da7e1 100644 (file)
@@ -93,6 +93,8 @@ class restore_final_task extends restore_task {
             $this->add_step(new restore_course_logs_structure_step('course_logs', 'course/logs.xml'));
             // New log stores.
             $this->add_step(new restore_course_logstores_structure_step('course_logstores', 'course/logstores.xml'));
+            // Last access to course logs.
+            $this->add_step(new restore_course_loglastaccess_structure_step('course_loglastaccess', 'course/loglastaccess.xml'));
         }
 
         // Review all the executed tasks having one after_restore method
index 3b37acc..40345c1 100644 (file)
@@ -146,6 +146,19 @@ class restore_root_task extends restore_task {
         $this->add_setting($roleassignments);
         $users->add_dependency($roleassignments);
 
+        // Define permissions.
+        $defaultvalue = false;                      // Safer default.
+        $changeable = false;
+        // Enable when available, or key doesn't exist (backward compatibility).
+        if (!array_key_exists('permissions', $rootsettings) || !empty($rootsettings['permissions'])) {
+            $defaultvalue = true;
+            $changeable = true;
+        }
+        $permissions = new restore_permissions_setting('permissions', base_setting::IS_BOOLEAN, $defaultvalue);
+        $permissions->set_ui(new backup_setting_ui_checkbox($permissions, get_string('rootsettingpermissions', 'backup')));
+        $permissions->get_ui()->set_changeable($changeable);
+        $this->add_setting($permissions);
+
         // Define activitites
         $defaultvalue = false;                      // Safer default
         $changeable = false;
index 61124de..fb9e064 100644 (file)
@@ -43,6 +43,12 @@ class restore_generic_setting extends root_backup_setting {}
  */
 class restore_users_setting extends restore_generic_setting {}
 
+/**
+ * root setting to control if restore will create override permission information by roles
+ */
+class restore_permissions_setting extends restore_generic_setting {
+}
+
 /**
  * root setting to control if restore will create groups/grouping information. Depends on @restore_users_setting
  *
index 1397c74..7e51a1f 100644 (file)
@@ -2058,7 +2058,9 @@ class restore_ras_and_caps_structure_step extends restore_structure_step {
         if ($this->get_setting_value('role_assignments')) {
             $paths[] = new restore_path_element('assignment', '/roles/role_assignments/assignment');
         }
-        $paths[] = new restore_path_element('override', '/roles/role_overrides/override');
+        if ($this->get_setting_value('permissions')) {
+            $paths[] = new restore_path_element('override', '/roles/role_overrides/override');
+        }
 
         return $paths;
     }
@@ -3428,6 +3430,81 @@ class restore_course_logstores_structure_step extends restore_structure_step {
     }
 }
 
+/**
+ * Structure step in charge of restoring the loglastaccess.xml file for the course logs.
+ *
+ * This restore step will rebuild the table for user_lastaccess table.
+ */
+class restore_course_loglastaccess_structure_step extends restore_structure_step {
+
+    /**
+     * Conditionally decide if this step should be executed.
+     *
+     * This function checks the following parameter:
+     *
+     *   1. the loglastaccess.xml file exists
+     *
+     * @return bool true is safe to execute, false otherwise
+     */
+    protected function execute_condition() {
+        // Check it is included in the backup.
+        $fullpath = $this->task->get_taskbasepath();
+        $fullpath = rtrim($fullpath, '/') . '/' . $this->filename;
+        if (!file_exists($fullpath)) {
+            // Not found, can't restore loglastaccess.xml information.
+            return false;
+        }
+
+        return true;
+    }
+
+    /**
+     * Return the elements to be processed on restore of loglastaccess.
+     *
+     * @return restore_path_element[] array of elements to be processed on restore.
+     */
+    protected function define_structure() {
+
+        $paths = array();
+        // To know if we are including userinfo.
+        $userinfo = $this->get_setting_value('users');
+
+        if ($userinfo) {
+            $paths[] = new restore_path_element('lastaccess', '/lastaccesses/lastaccess');
+        }
+        // Return the paths wrapped.
+        return $paths;
+    }
+
+    /**
+     * Process the 'lastaccess' elements.
+     *
+     * @param array $data element data
+     */
+    protected function process_lastaccess($data) {
+        global $DB;
+
+        $data = (object)$data;
+
+        $data->courseid = $this->get_courseid();
+        if (!$data->userid = $this->get_mappingid('user', $data->userid)) {
+            return; // Nothing to do, not able to find the user to set the lastaccess time.
+        }
+
+        // Check if record does exist.
+        $exists = $DB->get_record('user_lastaccess', array('courseid' => $data->courseid, 'userid' => $data->userid));
+        if ($exists) {
+            // If the time of last access of the restore is newer, then replace and update.
+            if ($exists->timeaccess < $data->timeaccess) {
+                $exists->timeaccess = $data->timeaccess;
+                $DB->update_record('user_lastaccess', $exists);
+            }
+        } else {
+            $DB->insert_record('user_lastaccess', $data);
+        }
+    }
+}
+
 /**
  * Structure step in charge of restoring the logstores.xml file for the activity logs.
  *
index e4b71c8..98fdbb3 100644 (file)
@@ -60,6 +60,7 @@ class core_backup_cleanup_task_testcase extends advanced_testcase {
             $user->id
         );
         $controller->execute_plan();
+        $controller->destroy(); // Unset all structures, close files...
         return $controller->get_backupid();
     }
 
diff --git a/backup/tests/backup_restore_base_testcase.php b/backup/tests/backup_restore_base_testcase.php
new file mode 100644 (file)
index 0000000..8b12296
--- /dev/null
@@ -0,0 +1,123 @@
+<?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/>.
+
+/**
+ * Backup restore base tests.
+ *
+ * @package   core_backup
+ * @copyright Tomo Tsuyuki <tomotsuyuki@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->dirroot . '/backup/util/includes/backup_includes.php');
+require_once($CFG->dirroot . '/backup/util/includes/restore_includes.php');
+
+/**
+ * Basic testcase class for backup / restore functionality.
+ */
+abstract class core_backup_backup_restore_base_testcase extends advanced_testcase {
+
+    /**
+     * Setup test data.
+     */
+    protected function setUp(): void {
+        $this->resetAfterTest();
+        $this->setAdminUser();
+    }
+
+    /**
+     * Backup the course by general mode.
+     *
+     * @param  stdClass $course Course for backup.
+     * @return string Hash string ID from the backup.
+     * @throws coding_exception
+     * @throws moodle_exception
+     */
+    protected function perform_backup($course): string {
+        global $CFG, $USER;
+
+        $coursecontext = context_course::instance($course->id);
+
+        // Start backup process.
+        $bc = new backup_controller(backup::TYPE_1COURSE, $course->id, backup::FORMAT_MOODLE,
+                backup::INTERACTIVE_NO, backup::MODE_GENERAL, $USER->id);
+        $bc->execute_plan();
+        $backupid = $bc->get_backupid();
+        $bc->destroy();
+
+        // Get the backup file.
+        $fs = get_file_storage();
+        $files = $fs->get_area_files($coursecontext->id, 'backup', 'course', false, 'id ASC');
+        $backupfile = reset($files);
+
+        // Extract backup file.
+        $path = $CFG->tempdir . DIRECTORY_SEPARATOR . "backup" . DIRECTORY_SEPARATOR . $backupid;
+
+        $fp = get_file_packer('application/vnd.moodle.backup');
+        $fp->extract_to_pathname($backupfile, $path);
+
+        return $backupid;
+    }
+
+    /**
+     * Restore from backupid to course.
+     *
+     * @param  string   $backupid Hash string ID from backup.
+     * @param  stdClass $course Course which is restored for.
+     * @throws restore_controller_exception
+     */
+    protected function perform_restore($backupid, $course): void {
+        global $USER;
+
+        // Set up restore.
+        $rc = new restore_controller($backupid, $course->id,
+                backup::INTERACTIVE_NO, backup::MODE_GENERAL, $USER->id, backup::TARGET_EXISTING_ADDING);
+        // Execute restore.
+        $rc->execute_precheck();
+        $rc->execute_plan();
+        $rc->destroy();
+    }
+
+    /**
+     * Import course from course1 to course2.
+     *
+     * @param stdClass $course1 Course to be backuped up.
+     * @param stdClass $course2 Course to be restored.
+     * @throws restore_controller_exception
+     */
+    protected function perform_import($course1, $course2): void {
+        global $USER;
+
+        // Start backup process.
+        $bc = new backup_controller(backup::TYPE_1COURSE, $course1->id, backup::FORMAT_MOODLE,
+                backup::INTERACTIVE_NO, backup::MODE_IMPORT, $USER->id);
+        $backupid = $bc->get_backupid();
+        $bc->execute_plan();
+        $bc->destroy();
+
+        // Set up restore.
+        $rc = new restore_controller($backupid, $course2->id,
+                backup::INTERACTIVE_NO, backup::MODE_SAMESITE, $USER->id, backup::TARGET_EXISTING_ADDING);
+        // Execute restore.
+        $rc->execute_precheck();
+        $rc->execute_plan();
+        $rc->destroy();
+    }
+
+}
diff --git a/backup/tests/backup_restore_permission_test.php b/backup/tests/backup_restore_permission_test.php
new file mode 100644 (file)
index 0000000..42929a4
--- /dev/null
@@ -0,0 +1,156 @@
+<?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/>.
+
+/**
+ * Backup restore permission tests.
+ *
+ * @package   core_backup
+ * @copyright Tomo Tsuyuki <tomotsuyuki@catalyst-au.net>
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die();
+
+global $CFG;
+require_once('backup_restore_base_testcase.php');
+require_once($CFG->dirroot . '/backup/util/includes/backup_includes.php');
+require_once($CFG->dirroot . '/backup/util/includes/restore_includes.php');
+
+/**
+ * Testcase class for permission backup / restore functionality.
+ */
+class core_backup_backup_restore_permission_testcase extends core_backup_backup_restore_base_testcase {
+
+    /** @var stdClass A test course which is restored/imported from. */
+    protected $course1;
+
+    /** @var stdClass A test course which is restored/imported to. */
+    protected $course2;
+
+    /** @var stdClass A user for using in this test. */
+    protected $user;
+
+    /** @var string Capability name for using in this test. */
+    protected $capabilityname;
+
+    /** @var context_course Context instance for course1. */
+    protected $course1context;
+
+    /** @var context_course Context instance for course2. */
+    protected $course2context;
+
+    /**
+     * Setup test data.
+     */
+    protected function setUp(): void {
+        global $DB;
+
+        parent::setUp();
+        // Create a course with some availability data set.
+        $generator = $this->getDataGenerator();
+        $this->course1 = $generator->create_course();
+        $this->course1context = context_course::instance($this->course1->id);
+        $this->course2 = $generator->create_course();
+        $this->course2context = context_course::instance($this->course2->id);
+        $this->capabilityname = 'enrol/manual:enrol';
+        $this->user = $generator->create_user();
+
+        // Set additional permission for course 1.
+        $teacherrole = $DB->get_record('role', ['shortname' => 'teacher'], '*', MUST_EXIST);
+        role_change_permission($teacherrole->id, $this->course1context, $this->capabilityname, CAP_ALLOW);
+
+        // Enrol to the courses.
+        $generator->enrol_user($this->user->id, $this->course1->id, $teacherrole->id);
+        $generator->enrol_user($this->user->id, $this->course2->id, $teacherrole->id);
+    }
+
+    /**
+     * Test having settings.
+     */
+    public function test_having_settings(): void {
+        $this->assertEquals(0, get_config('backup', 'backup_import_permissions'));
+        $this->assertEquals(1, get_config('restore', 'restore_general_permissions'));
+    }
+
+    /**
+     * Test for restore with permission.
+     */
+    public function test_backup_restore_with_permission(): void {
+
+        // Set default setting to restore with permission.
+        set_config('restore_general_permissions', 1, 'restore');
+
+        // Confirm course1 has the capability for the user.
+        $this->assertTrue(has_capability($this->capabilityname, $this->course1context, $this->user));
+
+        // Confirm course2 does not have the capability for the user.
+        $this->assertFalse(has_capability($this->capabilityname, $this->course2context, $this->user));
+
+        // Perform backup and restore.
+        $backupid = $this->perform_backup($this->course1);
+        $this->perform_restore($backupid, $this->course2);
+
+        // Confirm course2 has the capability for the user.
+        $this->assertTrue(has_capability($this->capabilityname, $this->course2context, $this->user));
+    }
+
+    /**
+     * Test for backup / restore without restore permission.
+     */
+    public function test_backup_restore_without_permission(): void {
+
+        // Set default setting to restore without permission.
+        set_config('restore_general_permissions', 0, 'restore');
+
+        // Perform backup and restore.
+        $backupid = $this->perform_backup($this->course1);
+        $this->perform_restore($backupid, $this->course2);
+
+        // Confirm course2 does not have the capability for the user.
+        $this->assertFalse(has_capability($this->capabilityname, $this->course2context, $this->user));
+    }
+
+    /**
+     * Test for import with permission.
+     */
+    public function test_backup_import_with_permission(): void {
+
+        // Set default setting to restore with permission.
+        set_config('backup_import_permissions', 1, 'backup');
+
+        // Perform import.
+        $this->perform_import($this->course1, $this->course2);
+
+        // Confirm course2 does not have the capability for the user.
+        $this->assertTrue(has_capability($this->capabilityname, $this->course2context, $this->user));
+    }
+
+    /**
+     * Test for import without permission.
+     */
+    public function test_backup_import_without_permission(): void {
+
+        // Set default setting to restore without permission.
+        set_config('backup_import_permissions', 0, 'backup');
+
+        // Perform import.
+        $this->perform_import($this->course1, $this->course2);
+
+        // Confirm course2 does not have the capability for the user.
+        $this->assertFalse(has_capability($this->capabilityname, $this->course2context, $this->user));
+    }
+
+}
index 3d55558..825f55b 100644 (file)
@@ -1,7 +1,13 @@
 This files describes API changes in /backup/*,
 information provided here is intended especially for developers.
 
-=== 4.0 ===
+=== 3.11 ===
+
+ * New setting called "Include override permissions" has been implemented. The default
+   settings is OFF for import, and ON for restore.
+
+=== 3.10 ===
+
  * Local plugins can now hook into a backup and restore process of grade items by
    using define_grade_item_plugin_structure method (See MDL-69418).
 
index e5c0163..7b5cbd4 100644 (file)
@@ -577,6 +577,7 @@ abstract class backup_controller_dbops extends backup_dbops {
                         'backup_import_blocks'             => 'blocks',
                         'backup_import_filters'            => 'filters',
                         'backup_import_calendarevents'     => 'calendarevents',
+                        'backup_import_permissions'        => 'permissions',
                         'backup_import_questionbank'       => 'questionbank',
                         'backup_import_groups'             => 'groups',
                         'backup_import_competencies'       => 'competencies',
index 216da03..4cc7ee2 100644 (file)
@@ -146,6 +146,7 @@ abstract class restore_controller_dbops extends restore_dbops {
             'restore_general_users'              => 'users',
             'restore_general_enrolments'         => 'enrolments',
             'restore_general_role_assignments'   => 'role_assignments',
+            'restore_general_permissions'        => 'permissions',
             'restore_general_activities'         => 'activities',
             'restore_general_blocks'             => 'blocks',
             'restore_general_filters'            => 'filters',
index 9ccd887..74b7396 100644 (file)
@@ -98,26 +98,6 @@ class backup_anonymizer_helper {
         return 'anon' . $counter . '@doesntexist.invalid'; // Just a counter.
     }
 
-    public static function process_user_icq($value) {
-        return ''; // Clean icq
-    }
-
-    public static function process_user_skype($value) {
-        return ''; // Clean skype
-    }
-
-    public static function process_user_yahoo($value) {
-        return ''; // Clean yahoo
-    }
-
-    public static function process_user_aim($value) {
-        return ''; // Clean aim
-    }
-
-    public static function process_user_msn($value) {
-        return ''; // Clean msn
-    }
-
     public static function process_user_phone1($value) {
         return ''; // Clean phone1
     }
@@ -154,10 +134,6 @@ class backup_anonymizer_helper {
         return 0; // No picture
     }
 
-    public static function process_user_url($value) {
-        return ''; // No url
-    }
-
     public static function process_user_description($value) {
         return ''; // No user description
     }
index eb7507f..0417651 100644 (file)
@@ -1,11 +1,22 @@
-@core @core_backup @core_contentbank
+@core @core_backup @core_contentbank @core_h5p @contenttype_h5p @_file_upload @javascript
 Feature: Import course content bank content
   In order to import content from a course contentbank
   As a teacher
   I need to confirm that errors will not happen
 
   Background:
-    Given the following "courses" exist:
+    Given I log in as "admin"
+    And I am on site homepage
+    And I turn editing mode on
+    And I add the "Navigation" block if not present
+    And I configure the "Navigation" block
+    And I set the following fields to these values:
+      | Page contexts | Display throughout the entire site |
+    And I press "Save changes"
+    And I navigate to "H5P > Manage H5P content types" in site administration
+    And I upload "h5p/tests/fixtures/ipsums.h5p" file to "H5P content type" filemanager
+    And I click on "Upload H5P content types" "button" in the "#fitem_id_uploadlibraries" "css_element"
+    And the following "courses" exist:
       | fullname | shortname | category |
       | Course 1 | C1        | 0        |
       | Course 2 | C2        | 0        |
@@ -17,29 +28,36 @@ Feature: Import course content bank content
       | teacher1 | C1 | editingteacher |
       | teacher1 | C2 | editingteacher |
     And the following "contentbank content" exist:
-      | contextlevel | reference | contenttype     | user     | contentname |
-      | Course       | C1        | contenttype_h5p | teacher1 | ipsums.h5p  |
+      | contextlevel | reference | contenttype     | user     | contentname | filepath                        |
+      | Course       | C1        | contenttype_h5p | teacher1 | ipsums.h5p  | /h5p/tests/fixtures/ipsums.h5p  |
+    And I log out
     And I log in as "teacher1"
 
   Scenario: Import content bank content to another course
     Given I am on "Course 2" course homepage
+    And I expand "Site pages" node
     And I click on "Content bank" "link"
     And I should not see "ipsums.h5p"
     When I import "Course 1" course into "Course 2" course using this options:
+    And I expand "Site pages" node
     And I click on "Content bank" "link"
     Then I should see "ipsums.h5p"
     And I am on "Course 1" course homepage
+    And I expand "Site pages" node
     And I click on "Content bank" "link"
     And I should see "ipsums.h5p"
 
   Scenario: User could configure not to import content bank
     Given I am on "Course 2" course homepage
+    And I expand "Site pages" node
     And I click on "Content bank" "link"
     And I should not see "ipsums.h5p"
     When I import "Course 1" course into "Course 2" course using this options:
       | Initial | Include content bank content | 0 |
+    And I expand "Site pages" node
     And I click on "Content bank" "link"
     Then I should not see "ipsums.h5p"
     And I am on "Course 1" course homepage
+    And I expand "Site pages" node
     And I click on "Content bank" "link"
     And I should see "ipsums.h5p"
index 5adde7f..52c1452 100644 (file)
@@ -4,19 +4,21 @@ Feature: Import course's contents into another course
   As a teacher
   I need to import a course contents into another course selecting what I want to import
 
-  Scenario: Import course's contents to another course
+  Background:
     Given the following "courses" exist:
       | fullname | shortname | category |
-      | Course 1 | C1 | 0 |
-      | Course 2 | C2 | 0 |
+      | Course 1 | C1        | 0        |
+      | Course 2 | C2        | 0        |
     And the following "users" exist:
-      | username | firstname | lastname | email |
-      | teacher1 | Teacher | 1 | teacher1@example.com |
+      | username | firstname | lastname | email                |
+      | teacher1 | Teacher   | 1        | teacher1@example.com |
     And the following "course enrolments" exist:
-      | user | course | role |
-      | teacher1 | C1 | editingteacher |
-      | teacher1 | C2 | editingteacher |
-    And I log in as "teacher1"
+      | user     | course | role           |
+      | teacher1 | C1     | editingteacher |
+      | teacher1 | C2     | editingteacher |
+
+  Scenario: Import course's contents to another course
+    Given I log in as "teacher1"
     And I am on "Course 1" course homepage with editing mode on
     And I add a "Database" to section "1" and I fill the form with:
       | Name | Test database name |
@@ -32,3 +34,26 @@ Feature: Import course's contents into another course
     And I should see "Test forum name"
     And I should see "Comments" in the "Comments" "block"
     And I should see "Recent blog entries"
+
+  Scenario: Import process with permission option
+    Given the following "permission overrides" exist:
+      | capability         | permission | role    | contextlevel | reference |
+      | enrol/manual:enrol | Allow      | teacher | Course       | C1        |
+    And I log in as "teacher1"
+    When I import "Course 1" course into "Course 2" course using this options:
+      | Initial | Include override permissions | 1 |
+    And I navigate to "Users > Permissions" in current page administration
+    Then I should see "Non-editing teacher (1)"
+    And I set the field "Advanced role override" to "Non-editing teacher (1)"
+    And I press "Go"
+    And "enrol/manual:enrol" capability has "Allow" permission
+
+  Scenario: Import process without permission option
+    Given the following "permission overrides" exist:
+      | capability         | permission | role    | contextlevel | reference |
+      | enrol/manual:enrol | Allow      | teacher | Course       | C1        |
+    And I log in as "teacher1"
+    When I import "Course 1" course into "Course 2" course using this options:
+      | Initial | Include override permissions | 0 |
+    And I navigate to "Users > Permissions" in current page administration
+    Then I should see "Non-editing teacher (0)"
index d1bae45..fb32d18 100644 (file)
@@ -244,3 +244,29 @@ Feature: Restore Moodle 2 course backups
     And I should not see "Topic 16"
     And I should see "Test URL name" in the "Topic 3" "section"
     And I should see "Test forum name" in the "Topic 1" "section"
+
+  @javascript
+  Scenario: Restore a backup with override permission
+    Given the following "permission overrides" exist:
+      | capability         | permission | role           | contextlevel | reference |
+      | enrol/manual:enrol | Allow      | teacher        | Course       | C1        |
+    And I backup "Course 1" course using this options:
+      | Confirmation | Filename | test_backup.mbz |
+    When I restore "test_backup.mbz" backup into a new course using this options:
+      | Settings | Include override permissions | 1 |
+    Then I navigate to "Users > Permissions" in current page administration
+    And I should see "Non-editing teacher (1)"
+    And I set the field "Advanced role override" to "Non-editing teacher (1)"
+    And "enrol/manual:enrol" capability has "Allow" permission
+
+  @javascript
+  Scenario: Restore a backup without override permission
+    Given the following "permission overrides" exist:
+      | capability         | permission | role           | contextlevel | reference |
+      | enrol/manual:enrol | Allow      | teacher        | Course       | C1        |
+    And I backup "Course 1" course using this options:
+      | Confirmation | Filename | test_backup.mbz |
+    When I restore "test_backup.mbz" backup into a new course using this options:
+      | Settings | Include override permissions | 0 |
+    Then I navigate to "Users > Permissions" in current page administration
+    And I should see "Non-editing teacher (0)"
index ff77f49..0dceb33 100644 (file)
@@ -43,6 +43,10 @@ if ($persistedissuer) {
     $returnurl = new moodle_url('/badges/backpack-connect.php',
         ['action' => 'authorization', 'sesskey' => sesskey(), 'backpackid' => $backpackid]);
 
+    // If scope is not passed as parameter, use the issuer supported scopes.
+    if (empty($scope)) {
+        $scope = $issuer->get('scopessupported');
+    }
     $client = new core_badges\oauth2\client($issuer, $returnurl, $scope, $externalbackpack);
     if ($client) {
         if (!$client->is_logged_in()) {
index 575985b..8e635bc 100644 (file)
@@ -222,6 +222,7 @@ class client extends \core\oauth2\client {
         $callbackurl = self::callback_url();
 
         if ($granttype == 'authorization_code') {
+            $this->basicauth = true;
             $params = array('code' => $code,
                 'grant_type' => $granttype,
                 'redirect_uri' => $callbackurl->out(false),
@@ -236,7 +237,7 @@ class client extends \core\oauth2\client {
             );
         }
         if ($this->basicauth) {
-            $idsecret = urlencode($this->clientid) . ':' . urlencode($this->clientsecret);
+            $idsecret = $this->clientid . ':' . $this->clientsecret;
             $this->setHeader('Authorization: Basic ' . base64_encode($idsecret));
         } else {
             $params['client_id'] = $this->clientid;
@@ -244,11 +245,13 @@ class client extends \core\oauth2\client {
         }
         // 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');
+            $debuginfo = !empty($this->error) ? $this->error : $response;
+            throw new moodle_exception('oauth2refreshtokenerror', 'core_error', '', $this->info['http_code'], $debuginfo);
         }
 
+        $r = json_decode($response);
+
         if (is_null($r)) {
             throw new moodle_exception("Could not decode JSON token response");
         }
index 6091b62..9c02a19 100644 (file)
@@ -51,8 +51,8 @@ class award_criteria_profile extends award_criteria {
         $missing = array();
 
         // Note: cannot use user_get_default_fields() here because it is not possible to decide which fields user can modify.
-        $dfields = array('firstname', 'lastname', 'email', 'address', 'phone1', 'phone2', 'icq', 'skype', 'yahoo',
-                         'aim', 'msn', 'department', 'institution', 'description', 'picture', 'city', 'url', 'country');
+        $dfields = array('firstname', 'lastname', 'email', 'address', 'phone1', 'phone2',
+                         'department', 'institution', 'description', 'picture', 'city', 'country');
 
         $sql = "SELECT uf.id as fieldid, uf.name as name, ic.id as categoryid, ic.name as categoryname, uf.datatype
                 FROM {user_info_field} uf
index 1afadf4..ba16434 100644 (file)
@@ -350,18 +350,15 @@ class core_badges_renderer extends plugin_renderer_base {
             if (!empty($CFG->badges_allowexternalbackpack) && ($expiration > $now)
                 && $userbackpack = badges_get_user_backpack($USER->id)) {
 
-                $assertion = null;
                 if (badges_open_badges_backpack_api($userbackpack->id) == 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));
                 }
 
-                if (!is_null(assertion)) {
-                    $attributes = ['class' => 'btn btn-secondary m-1', 'role' => 'button'];
-                    $tobackpack = html_writer::link($assertion, get_string('addtobackpack', 'badges'), $attributes);
-                    $output .= $tobackpack;
-                }
+                $attributes = ['class' => 'btn btn-secondary m-1', 'role' => 'button'];
+                $tobackpack = html_writer::link($assertion, get_string('addtobackpack', 'badges'), $attributes);
+                $output .= $tobackpack;
             }
         }
         $output .= html_writer::end_tag('div');
index 3231db1..b44d2f5 100644 (file)
@@ -602,8 +602,8 @@ class badgeslib_test extends advanced_testcase {
         $criteria_overall = award_criteria::build(array('criteriatype' => BADGE_CRITERIA_TYPE_OVERALL, 'badgeid' => $badge->id));
         $criteria_overall->save(array('agg' => BADGE_CRITERIA_AGGREGATION_ANY));
         $criteria_overall1 = award_criteria::build(array('criteriatype' => BADGE_CRITERIA_TYPE_PROFILE, 'badgeid' => $badge->id));
-        $criteria_overall1->save(array('agg' => BADGE_CRITERIA_AGGREGATION_ALL, 'field_address' => 'address', 'field_aim' => 'aim',
-            'field_' . $customprofileid => $customprofileid));
+        $criteria_overall1->save(array('agg' => BADGE_CRITERIA_AGGREGATION_ALL, 'field_address' => 'address',
+            'field_department' => 'department', 'field_' . $customprofileid => $customprofileid));
 
         // Assert the badge will not be issued to the user as is.
         $badge = new badge($this->coursebadge);
@@ -612,7 +612,7 @@ class badgeslib_test extends advanced_testcase {
 
         // Set the required fields and make sure the badge got issued.
         $this->user->address = 'Test address';
-        $this->user->aim = '999999999';
+        $this->user->department = 'sillywalks';
         $sink = $this->redirectEmails();
         profile_save_data((object)array('id' => $this->user->id, 'profile_field_newfield' => 'X'));
         user_update_user($this->user, false);
index 714c9d0..c604a37 100644 (file)
@@ -84,26 +84,6 @@ class myprofile implements renderable, templatable {
             $data->useremail = obfuscate_mailto($USER->email, '');
         }
 
-        if (!empty($this->config->display_icq) && !empty($USER->icq)) {
-            $data->usericq = s($USER->icq);
-        }
-
-        if (!empty($this->config->display_skype) && !empty($USER->skype)) {
-            $data->userskype = s($USER->skype);
-        }
-
-        if (!empty($this->config->display_yahoo) && !empty($USER->yahoo)) {
-            $data->useryahoo = s($USER->yahoo);
-        }
-
-        if (!empty($this->config->display_aim) && !empty($USER->aim)) {
-            $data->useraim = s($USER->aim);
-        }
-
-        if (!empty($this->config->display_msn) && !empty($USER->msn)) {
-            $data->usermsn = s($USER->msn);
-        }
-
         if (!empty($this->config->display_phone1) && !empty($USER->phone1)) {
             $data->userphone1 = s($USER->phone1);
         }
index 0302647..6de49b2 100644 (file)
@@ -57,41 +57,6 @@ class block_myprofile_edit_form extends block_edit_form {
             $mform->setDefault('config_display_email', '1');
         }
 
-        $mform->addElement('selectyesno', 'config_display_icq', get_string('display_icq', 'block_myprofile'));
-        if (isset($this->block->config->display_icq)) {
-            $mform->setDefault('config_display_icq', $this->block->config->display_icq);
-        } else {
-            $mform->setDefault('config_display_icq', '0');
-        }
-
-        $mform->addElement('selectyesno', 'config_display_skype', get_string('display_skype', 'block_myprofile'));
-        if (isset($this->block->config->display_skype)) {
-            $mform->setDefault('config_display_skype', $this->block->config->display_skype);
-        } else {
-            $mform->setDefault('config_display_skype', '0');
-        }
-
-        $mform->addElement('selectyesno', 'config_display_yahoo', get_string('display_yahoo', 'block_myprofile'));
-        if (isset($this->block->config->display_yahoo)) {
-            $mform->setDefault('config_display_yahoo', $this->block->config->display_yahoo);
-        } else {
-            $mform->setDefault('config_display_yahoo', '0');
-        }
-
-        $mform->addElement('selectyesno', 'config_display_aim', get_string('display_aim', 'block_myprofile'));
-        if (isset($this->block->config->display_aim)) {
-            $mform->setDefault('config_display_aim', $this->block->config->display_aim);
-        } else {
-            $mform->setDefault('config_display_aim', '0');
-        }
-
-        $mform->addElement('selectyesno', 'config_display_msn', get_string('display_msn', 'block_myprofile'));
-        if (isset($this->block->config->display_msn)) {
-            $mform->setDefault('config_display_msn', $this->block->config->display_msn);
-        } else {
-            $mform->setDefault('config_display_msn', '0');
-        }
-
         $mform->addElement('selectyesno', 'config_display_phone1', get_string('display_phone1', 'block_myprofile'));
         if (isset($this->block->config->display_phone1)) {
             $mform->setDefault('config_display_phone1', $this->block->config->display_phone1);
index 5d886c2..e263d2c 100644 (file)
@@ -28,11 +28,6 @@ $string['display_picture'] = 'Display picture';
 $string['display_country'] = 'Display country';
 $string['display_city'] = 'Display city';
 $string['display_email'] = 'Display email';
-$string['display_icq'] = 'Display ICQ';
-$string['display_skype'] = 'Display Skype';
-$string['display_yahoo'] = 'Display Yahoo';
-$string['display_aim'] = 'Display AIM';
-$string['display_msn'] = 'Display MSN';
 $string['display_phone1'] = 'Display phone';
 $string['display_phone2'] = 'Display mobile phone';
 $string['display_institution'] = 'Display institution';
@@ -46,3 +41,10 @@ $string['myprofile:myaddinstance'] = 'Add a new logged in user block to Dashboar
 $string['myprofile_settings'] = 'Visible user information';
 $string['pluginname'] = 'Logged in user';
 $string['privacy:metadata'] = 'The Logged in user block only shows information about the logged in user and does not store data itself.';
+
+// Deprecated since Moodle 4.0.
+$string['display_icq'] = 'Display ICQ';
+$string['display_skype'] = 'Display Skype';
+$string['display_yahoo'] = 'Display Yahoo';
+$string['display_aim'] = 'Display AIM';
+$string['display_msn'] = 'Display MSN';
\ No newline at end of file
index e132b0d..d9505e7 100644 (file)
         * usercountry
         * usercity
         * useremail
-        * usericq
-        * userskype
-        * useryahoo
-        * useraim
-        * usermsn
         * userphone1
         * userphone2
         * userinstitution
         "usercountry": "Australia",
         "usercity": "Perth",
         "useremail": "<a href=''>john.doe@example.com</a>",
-        "usericq": "12345",
-        "userskype": "john.doe",
-        "useryahoo": "12345",
-        "useraim": "12345",
-        "usermsn": "12345",
         "userphone1": "123456789",
         "userphone2": "123456789",
         "userinstitution": "Institution",
              {{{ useremail }}}
         </div>
     {{/useremail}}
-    {{#usericq}}
-    <div class="myprofileitem icq">
-         <span>ICQ:</span>
-         {{ usericq }}
-    </div>
-    {{/usericq}}
-    {{#userskype}}
-    <div class="myprofileitem skype">
-         <span>Skype:</span>
-         {{ userskype }}
-    </div>
-    {{/userskype}}
-    {{#useryahoo}}
-    <div class="myprofileitem yahoo">
-         <span>Yahoo:</span>
-         {{ useryahoo }}
-    </div>
-    {{/useryahoo}}
-    {{#useraim}}
-    <div class="myprofileitem aim">
-         <span>AIM:</span>
-         {{ useraim }}
-    </div>
-    {{/useraim}}
-    {{#usermsn}}
-    <div class="myprofileitem msn">
-         <span>MSN:</span>
-         {{ usermsn }}
-    </div>
-    {{/usermsn}}
     {{#userphone1}}
     <div class="myprofileitem phone1">
          <span>{{#str}} phone1 {{/str}}:</span>
index 8bd076c..8801de3 100644 (file)
@@ -61,101 +61,6 @@ Feature: The logged in user block allows users to view their profile information
     And I press "Save changes"
     And I should see "teacher1@example.com" in the "Logged in user" "block"
 
-  Scenario: Configure the logged in user block to show / hide the users ICQ
-    Given the following "users" exist:
-      | username | firstname | lastname | email                | icq   |
-      | teacher1 | Teacher   | One      | teacher1@example.com | myicq |
-    And I log in as "teacher1"
-    And I press "Customise this page"
-    When I add the "Logged in user" block
-    And I configure the "Logged in user" block
-    And I set the following fields to these values:
-      | Display ICQ           | No |
-    And I press "Save changes"
-    Then I should see "Teacher One" in the "Logged in user" "block"
-    And I should not see "myicq" in the "Logged in user" "block"
-    And I configure the "Logged in user" block
-    And I set the following fields to these values:
-      | Display ICQ | Yes |
-    And I press "Save changes"
-    And I should see "myicq" in the "Logged in user" "block"
-
-  Scenario: Configure the logged in user block to show / hide the users Skype
-    Given the following "users" exist:
-      | username | firstname | lastname | email                | skype   |
-      | teacher1 | Teacher   | One      | teacher1@example.com | myskype |
-    And I log in as "teacher1"
-    And I press "Customise this page"
-    When I add the "Logged in user" block
-    And I configure the "Logged in user" block
-    And I set the following fields to these values:
-      | Display Skype         | No |
-    And I press "Save changes"
-    Then I should see "Teacher One" in the "Logged in user" "block"
-    And I should not see "myskype" in the "Logged in user" "block"
-    And I configure the "Logged in user" block
-    And I set the following fields to these values:
-      | Display Skype | Yes |
-    And I press "Save changes"
-    And I should see "myskype" in the "Logged in user" "block"
-
-  Scenario: Configure the logged in user block to show / hide the users Yahoo
-    Given the following "users" exist:
-      | username | firstname | lastname | email                | yahoo   |
-      | teacher1 | Teacher   | One      | teacher1@example.com | myyahoo |
-    And I log in as "teacher1"
-    And I press "Customise this page"
-    When I add the "Logged in user" block
-    And I configure the "Logged in user" block
-    And I set the following fields to these values:
-      | Display Yahoo         | No |
-    And I press "Save changes"
-    Then I should see "Teacher One" in the "Logged in user" "block"
-    And I should not see "myyahoo" in the "Logged in user" "block"
-    And I configure the "Logged in user" block
-    And I set the following fields to these values:
-      | Display Yahoo | Yes |
-    And I press "Save changes"
-    And I should see "myyahoo" in the "Logged in user" "block"
-
-  Scenario: Configure the logged in user block to show / hide the users AIM
-    Given the following "users" exist:
-      | username | firstname | lastname | email                | aim   |
-      | teacher1 | Teacher   | One      | teacher1@example.com | myaim |
-    And I log in as "teacher1"
-    And I press "Customise this page"
-    When I add the "Logged in user" block
-    And I configure the "Logged in user" block
-    And I set the following fields to these values:
-      | Display AIM           | No |
-    And I press "Save changes"
-    Then I should see "Teacher One" in the "Logged in user" "block"
-    And I should not see "myaim" in the "Logged in user" "block"
-    And I configure the "Logged in user" block
-    And I set the following fields to these values:
-      | Display AIM | Yes |
-    And I press "Save changes"
-    And I should see "myaim" in the "Logged in user" "block"
-
-  Scenario: Configure the logged in user block to show / hide the users MSN
-    Given the following "users" exist:
-      | username | firstname | lastname | email                | msn   |
-      | teacher1 | Teacher   | One      | teacher1@example.com | mymsn |
-    And I log in as "teacher1"
-    And I press "Customise this page"
-    When I add the "Logged in user" block
-    And I configure the "Logged in user" block
-    And I set the following fields to these values:
-      | Display MSN           | No |
-    And I press "Save changes"
-    Then I should see "Teacher One" in the "Logged in user" "block"
-    And I should not see "mymsn" in the "Logged in user" "block"
-    And I configure the "Logged in user" block
-    And I set the following fields to these values:
-      | Display MSN | Yes |
-    And I press "Save changes"
-    And I should see "mymsn" in the "Logged in user" "block"
-
   Scenario: Configure the logged in user block to show / hide the users phone
     Given the following "users" exist:
       | username | firstname | lastname | email                | phone1   |
index 99ad750..40707e5 100644 (file)
@@ -156,7 +156,7 @@ class completion_criteria_grade extends completion_criteria {
      * @return string
      */
     public function get_type_title() {
-        return get_string('grade');
+        return get_string('gradenoun');
     }
 
     /**
diff --git a/contentbank/amd/build/upload.min.js b/contentbank/amd/build/upload.min.js
new file mode 100644 (file)
index 0000000..9df927d
Binary files /dev/null and b/contentbank/amd/build/upload.min.js differ
diff --git a/contentbank/amd/build/upload.min.js.map b/contentbank/amd/build/upload.min.js.map
new file mode 100644 (file)
index 0000000..c14d2d7
Binary files /dev/null and b/contentbank/amd/build/upload.min.js.map differ
diff --git a/contentbank/amd/src/upload.js b/contentbank/amd/src/upload.js
new file mode 100644 (file)
index 0000000..fb1f7c8
--- /dev/null
@@ -0,0 +1,52 @@
+// 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/>.
+
+/**
+ * Module to handle AJAX interactions with content bank upload files.
+ *
+ * @module     core_contentbank/upload
+ * @copyright  2021 Sara Arjona <sara@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+import ModalForm from 'core_form/modalform';
+import {get_string as getString} from 'core/str';
+
+/**
+ * Initialize upload files to the content bank form as Modal form.
+ *
+ * @param {String} elementSelector
+ * @param {String} formClass
+ * @param {Integer} contextId
+ * @param {Integer} contentId
+ */
+export const initModal = (elementSelector, formClass, contextId, contentId) => {
+    const element = document.querySelector(elementSelector);
+    element.addEventListener('click', function(e) {
+        e.preventDefault();
+        const form = new ModalForm({
+            formClass,
+            args: {
+                contextid: contextId,
+                id: contentId,
+            },
+            modalConfig: {title: getString('upload', 'contentbank')},
+            returnFocus: e.target,
+        });
+        form.addEventListener(form.events.FORM_SUBMITTED, (event) => {
+            document.location = event.detail.returnurl;
+        });
+        form.show();
+    });
+};
diff --git a/contentbank/classes/form/upload_files.php b/contentbank/classes/form/upload_files.php
new file mode 100644 (file)
index 0000000..bbd729c
--- /dev/null
@@ -0,0 +1,228 @@
+<?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/>.
+
+namespace core_contentbank\form;
+
+/**
+ * Upload files to content bank form
+ *
+ * @package    core_contentbank
+ * @copyright  2020 Amaia Anabitarte <amaia@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class upload_files extends \core_form\dynamic_form {
+
+    /**
+     * Add elements to this form.
+     */
+    public function definition() {
+        $mform = $this->_form;
+
+        $mform->addElement('hidden', 'contextid');
+        $mform->setType('contextid', PARAM_INT);
+
+        $mform->addElement('hidden', 'id');
+        $mform->setType('id', PARAM_INT);
+
+        $mform->addElement('filepicker', 'file', get_string('file', 'core_contentbank'), null, $this->get_options());
+        $mform->addHelpButton('file', 'file', 'core_contentbank');
+        $mform->addRule('file', null, 'required');
+    }
+
+    /**
+     * Validate incoming data.
+     *
+     * @param array $data
+     * @param array $files
+     * @return array
+     */
+    public function validation($data, $files) {
+        $errors = array();
+        $draftitemid = $data['file'];
+        $options = $this->get_options();
+        if (file_is_draft_area_limit_reached($draftitemid, $options['areamaxbytes'])) {
+            $errors['file'] = get_string('userquotalimit', 'error');
+        }
+        return $errors;
+    }
+
+    /**
+     * Check if current user has access to this form, otherwise throw exception
+     *
+     * Sometimes permission check may depend on the action and/or id of the entity.
+     * If necessary, form data is available in $this->_ajaxformdata or
+     * by calling $this->optional_param()
+     */
+    protected function check_access_for_dynamic_submission(): void {
+        require_capability('moodle/contentbank:upload', $this->get_context_for_dynamic_submission());
+
+        // Check the context used by the content bank is allowed.
+        $cb = new \core_contentbank\contentbank();
+        if (!$cb->is_context_allowed($this->get_context_for_dynamic_submission())) {
+            throw new \moodle_exception('contextnotallowed', 'core_contentbank');
+        }
+
+        // If $id is defined, the file content will be replaced (instead of uploading a new one).
+        // Check that the user has the right permissions to replace this content file.
+        $id = $this->optional_param('id', null, PARAM_INT);
+        if ($id) {
+            $content = $cb->get_content_from_id($id);
+            $contenttype = $content->get_content_type_instance();
+            if (!$contenttype->can_manage($content) || !$contenttype->can_upload()) {
+                throw new \moodle_exception('nopermissions', 'error', '', null, get_string('replacecontent', 'contentbank'));
+            }
+        }
+    }
+
+    /**
+     * Returns form context
+     *
+     * If context depends on the form data, it is available in $this->_ajaxformdata or
+     * by calling $this->optional_param()
+     *
+     * @return \context
+     */
+    protected function get_context_for_dynamic_submission(): \context {
+        $contextid = $this->optional_param('contextid', null, PARAM_INT);
+        return \context::instance_by_id($contextid, MUST_EXIST);
+    }
+
+    /**
+     * File upload options
+     *
+     * @return array
+     * @throws \coding_exception
+     */
+    protected function get_options(): array {
+        global $CFG;
+
+        $maxbytes = $CFG->userquota;
+        $maxareabytes = $CFG->userquota;
+        if (has_capability('moodle/user:ignoreuserquota', $this->get_context_for_dynamic_submission())) {
+            $maxbytes = USER_CAN_IGNORE_FILE_SIZE_LIMITS;
+            $maxareabytes = FILE_AREA_MAX_BYTES_UNLIMITED;
+        }
+
+        $cb = new \core_contentbank\contentbank();
+        $id = $this->optional_param('id', null, PARAM_INT);
+        if ($id) {
+            $content = $cb->get_content_from_id($id);
+            $contenttype = $content->get_content_type_instance();
+            $extensions = $contenttype->get_manageable_extensions();
+            $acceptedtypes = implode(',', $extensions);
+        } else {
+            $acceptedtypes = $cb->get_supported_extensions_as_string($this->get_context_for_dynamic_submission());
+        }
+
+        return ['subdirs' => 1, 'maxbytes' => $maxbytes, 'maxfiles' => -1, 'accepted_types' => $acceptedtypes,
+            'areamaxbytes' => $maxareabytes];
+    }
+
+    /**
+     * Process the form submission, used if form was submitted via AJAX
+     *
+     * This method can return scalar values or arrays that can be json-encoded, they will be passed to the caller JS.
+     *
+     * Submission data can be accessed as: $this->get_data()
+     *
+     * @return mixed
+     */
+    public function process_dynamic_submission() {
+        global $USER;
+
+        // Get the file and create the content based on it.
+        $usercontext = \context_user::instance($USER->id);
+        $fs = get_file_storage();
+        $files = $fs->get_area_files($usercontext->id, 'user', 'draft', $this->get_data()->file, 'itemid, filepath,
+            filename', false);
+        if (!empty($files)) {
+            $file = reset($files);
+            $cb = new \core_contentbank\contentbank();
+            try {
+                if ($this->get_data()->id) {
+                    $content = $cb->get_content_from_id($this->get_data()->id);
+                    $contenttype = $content->get_content_type_instance();
+                    $content = $contenttype->replace_content($file, $content);
+                } else {
+                    $content = $cb->create_content_from_file($this->get_context_for_dynamic_submission(), $USER->id, $file);
+                }
+                $params = ['id' => $content->get_id(), 'contextid' => $this->get_context_for_dynamic_submission()->id];
+                $url = new \moodle_url('/contentbank/view.php', $params);
+            } catch (\Exception $e) {
+                // Redirect to the right page (depending on if content is new or existing) and display an error.
+                if ($this->get_data()->id) {
+                    $content = $cb->get_content_from_id($this->get_data()->id);
+                    $params = [
+                        'id' => $content->get_id(),
+                        'contextid' => $this->get_context_for_dynamic_submission()->id,
+                        'errormsg' => 'notvalidpackage',
+                    ];
+                    $url = new \moodle_url('/contentbank/view.php', $params);
+                } else {
+                    $url = new \moodle_url('/contentbank/index.php', [
+                        'contextid' => $this->get_context_for_dynamic_submission()->id,
+                        'errormsg' => 'notvalidpackage'],
+                    );
+                }
+            }
+
+            return ['returnurl' => $url->out(false)];
+        }
+
+        return null;
+    }
+
+    /**
+     * Load in existing data as form defaults
+     *
+     * Can be overridden to retrieve existing values from db by entity id and also
+     * to preprocess editor and filemanager elements
+     *
+     * Example:
+     *     $this->set_data(get_entity($this->_ajaxformdata['id']));
+     */
+    public function set_data_for_dynamic_submission(): void {
+        $data = (object)[
+            'contextid' => $this->optional_param('contextid', null, PARAM_INT),
+            'id' => $this->optional_param('id', null, PARAM_INT),
+        ];
+        $this->set_data($data);
+    }
+
+    /**
+     * Returns url to set in $PAGE->set_url() when form is being rendered or submitted via AJAX
+     *
+     * This is used in the form elements sensitive to the page url, such as Atto autosave in 'editor'
+     *
+     * If the form has arguments (such as 'id' of the element being edited), the URL should
+     * also have respective argument.
+     *
+     * @return \moodle_url
+     */
+    protected function get_page_url_for_dynamic_submission(): \moodle_url {
+        $params = ['contextid' => $this->get_context_for_dynamic_submission()->id];
+
+        $id = $this->optional_param('id', null, PARAM_INT);
+        if ($id) {
+            $url = '/contentbank/view.php';
+            $params['id'] = $id;
+        } else {
+            $url = '/contentbank/index.php';
+        }
+
+        return new \moodle_url($url, $params);
+    }
+}
index 4670423..ea41ffb 100644 (file)
@@ -24,9 +24,6 @@
 
 namespace contenttype_h5p;
 
-use stdClass;
-use html_writer;
-
 /**
  * H5P Content manager class
  *
@@