Merge branch 'MDL-63614_M37v1' of https://github.com/sbourget/moodle
authorEloy Lafuente (stronk7) <stronk7@moodle.org>
Wed, 6 Feb 2019 12:19:01 +0000 (13:19 +0100)
committerEloy Lafuente (stronk7) <stronk7@moodle.org>
Wed, 6 Feb 2019 12:19:01 +0000 (13:19 +0100)
688 files changed:
.travis.yml
admin/classes/task_log_table.php [new file with mode: 0644]
admin/cli/uninstall_plugins.php [new file with mode: 0644]
admin/customfields.php [new file with mode: 0644]
admin/environment.xml
admin/searchareas.php
admin/settings/courses.php
admin/settings/plugins.php
admin/settings/server.php
admin/settings/subsystems.php
admin/tasklogs.php [new file with mode: 0644]
admin/templates/tasklogs.mustache [new file with mode: 0644]
admin/tool/customlang/db/upgrade.php
admin/tool/dataprivacy/amd/build/effective_retention_period.min.js
admin/tool/dataprivacy/amd/src/effective_retention_period.js
admin/tool/dataprivacy/classes/expired_contexts_manager.php
admin/tool/dataprivacy/classes/external.php
admin/tool/dataprivacy/styles.css
admin/tool/dataprivacy/templates/form-user-selector-suggestion.mustache
admin/tool/dataprivacy/tests/behat/dataexport.feature
admin/tool/dataprivacy/tests/expired_contexts_test.php
admin/tool/dataprivacy/tests/external_test.php
admin/tool/dataprivacy/version.php
admin/tool/log/db/upgrade.php
admin/tool/log/store/database/db/upgrade.php
admin/tool/log/store/standard/db/upgrade.php
admin/tool/monitor/db/upgrade.php
admin/tool/task/cli/schedule_task.php
admin/tool/task/lang/en/tool_task.php
admin/tool/task/renderer.php
admin/tool/task/settings.php
admin/tool/usertours/db/upgrade.php
admin/tool/xmldb/lang/en/tool_xmldb.php
analytics/classes/local/indicator/base.php
analytics/classes/local/indicator/binary.php
analytics/classes/local/indicator/discrete.php
analytics/tests/fixtures/test_indicator_discrete.php [new file with mode: 0644]
analytics/tests/fixtures/test_indicator_random.php
analytics/tests/indicator_test.php [new file with mode: 0644]
analytics/tests/prediction_test.php
auth/cas/auth.php
auth/cas/cas_form.html [deleted file]
auth/cas/db/upgrade.php
auth/cas/lang/en/auth_cas.php
auth/cas/lang/en/deprecated.txt [new file with mode: 0644]
auth/cas/lib.php [new file with mode: 0644]
auth/cas/settings.php
auth/cas/version.php
auth/db/db/upgrade.php
auth/email/db/upgrade.php
auth/ldap/db/upgrade.php
auth/ldap/lang/en/auth_ldap.php
auth/manual/db/upgrade.php
auth/mnet/classes/privacy/provider.php
auth/mnet/db/upgrade.php
auth/mnet/lang/en/auth_mnet.php
auth/none/db/upgrade.php
auth/oauth2/classes/auth.php
auth/oauth2/classes/privacy/provider.php
auth/oauth2/db/upgrade.php
auth/shibboleth/auth.php
auth/shibboleth/db/upgrade.php
auth/shibboleth/index_form.html [deleted file]
auth/shibboleth/lang/en/auth_shibboleth.php
auth/shibboleth/login.php
auth/shibboleth/templates/login_form.mustache [new file with mode: 0644]
backup/moodle2/backup_root_task.class.php
backup/moodle2/backup_settingslib.php
backup/moodle2/backup_stepslib.php
backup/moodle2/restore_root_task.class.php
backup/moodle2/restore_settingslib.php
backup/moodle2/restore_stepslib.php
backup/util/ui/backup_ui_setting.class.php
badges/backpack_form.php
badges/renderer.php
blocks/admin_bookmarks/block_admin_bookmarks.php
blocks/admin_bookmarks/create.php
blocks/admin_bookmarks/delete.php
blocks/badges/db/upgrade.php
blocks/badges/lang/en/block_badges.php
blocks/calendar_month/db/upgrade.php
blocks/calendar_upcoming/db/upgrade.php
blocks/community/classes/privacy/provider.php
blocks/community/db/upgrade.php
blocks/completionstatus/db/upgrade.php
blocks/course_list/lang/en/block_course_list.php
blocks/course_summary/db/upgrade.php
blocks/html/classes/privacy/provider.php
blocks/html/db/upgrade.php
blocks/lp/db/access.php
blocks/lp/lang/en/block_lp.php
blocks/lp/upgrade.txt [new file with mode: 0644]
blocks/lp/version.php
blocks/myoverview/templates/view-cards.mustache
blocks/myoverview/templates/view-list.mustache
blocks/myoverview/templates/view-summary.mustache
blocks/myoverview/tests/behat/block_myoverview_dashboard.feature
blocks/navigation/block_navigation.php
blocks/navigation/db/upgrade.php
blocks/private_files/module.js
blocks/quiz_results/db/upgrade.php
blocks/recent_activity/db/upgrade.php
blocks/recentlyaccessedcourses/templates/view-cards.mustache
blocks/rss_client/classes/privacy/provider.php
blocks/rss_client/db/upgrade.php
blocks/rss_client/lang/en/block_rss_client.php
blocks/section_links/db/upgrade.php
blocks/selfcompletion/db/upgrade.php
blocks/settings/db/upgrade.php
blocks/timeline/amd/build/event_list.min.js
blocks/timeline/amd/src/event_list.js
cache/stores/memcached/lang/en/cachestore_memcached.php
cohort/tests/behat/upload_cohort_users.feature
comment/locallib.php
competency/tests/api_test.php
completion/classes/external.php
completion/tests/externallib_test.php
completion/upgrade.txt
composer.json
composer.lock
config-dist.php
course/classes/category.php
course/classes/customfield/course_handler.php [new file with mode: 0644]
course/classes/external/course_summary_exporter.php
course/classes/list_element.php
course/classes/search/customfield.php [new file with mode: 0644]
course/classes/search/mycourse.php
course/classes/search/section.php
course/customfield.php [new file with mode: 0644]
course/dndupload.js
course/dnduploadlib.php
course/edit_form.php
course/externallib.php
course/format/lib.php
course/lib.php
course/renderer.php
course/templates/coursecards.mustache
course/tests/behat/customfields_locked.feature [new file with mode: 0644]
course/tests/behat/customfields_visibility.feature [new file with mode: 0644]
course/tests/customfield_test.php [new file with mode: 0644]
course/tests/externallib_test.php
course/tests/search_test.php
course/togglecompletion.php
course/upgrade.txt
customfield/amd/build/form.min.js [new file with mode: 0644]
customfield/amd/src/form.js [new file with mode: 0644]
customfield/classes/api.php [new file with mode: 0644]
customfield/classes/category.php [new file with mode: 0644]
customfield/classes/category_controller.php [new file with mode: 0644]
customfield/classes/data.php [new file with mode: 0644]
customfield/classes/data_controller.php [new file with mode: 0644]
customfield/classes/event/category_created.php [new file with mode: 0644]
customfield/classes/event/category_deleted.php [new file with mode: 0644]
customfield/classes/event/category_updated.php [new file with mode: 0644]
customfield/classes/event/field_created.php [new file with mode: 0644]
customfield/classes/event/field_deleted.php [new file with mode: 0644]
customfield/classes/event/field_updated.php [new file with mode: 0644]
customfield/classes/field.php [new file with mode: 0644]
customfield/classes/field_config_form.php [new file with mode: 0644]
customfield/classes/field_controller.php [new file with mode: 0644]
customfield/classes/handler.php [new file with mode: 0644]
customfield/classes/output/field_data.php [new file with mode: 0644]
customfield/classes/output/management.php [new file with mode: 0644]
customfield/classes/output/renderer.php [new file with mode: 0644]
customfield/classes/privacy/customfield_provider.php [new file with mode: 0644]
customfield/classes/privacy/provider.php [new file with mode: 0644]
customfield/edit.php [new file with mode: 0644]
customfield/externallib.php [new file with mode: 0644]
customfield/field/checkbox/classes/data_controller.php [new file with mode: 0644]
customfield/field/checkbox/classes/field_controller.php [new file with mode: 0644]
customfield/field/checkbox/classes/privacy/provider.php [new file with mode: 0644]
customfield/field/checkbox/lang/en/customfield_checkbox.php [new file with mode: 0644]
customfield/field/checkbox/tests/behat/field.feature [new file with mode: 0644]
customfield/field/checkbox/tests/plugin_test.php [new file with mode: 0644]
customfield/field/checkbox/version.php [new file with mode: 0644]
customfield/field/date/classes/data_controller.php [new file with mode: 0644]
customfield/field/date/classes/field_controller.php [new file with mode: 0644]
customfield/field/date/classes/privacy/provider.php [new file with mode: 0644]
customfield/field/date/lang/en/customfield_date.php [new file with mode: 0644]
customfield/field/date/lib.php [new file with mode: 0644]
customfield/field/date/tests/behat/field.feature [new file with mode: 0644]
customfield/field/date/tests/plugin_test.php [new file with mode: 0644]
customfield/field/date/version.php [new file with mode: 0644]
customfield/field/select/classes/data_controller.php [new file with mode: 0644]
customfield/field/select/classes/field_controller.php [new file with mode: 0644]
customfield/field/select/classes/privacy/provider.php [new file with mode: 0644]
customfield/field/select/lang/en/customfield_select.php [new file with mode: 0644]
customfield/field/select/tests/behat/field.feature [new file with mode: 0644]
customfield/field/select/tests/plugin_test.php [new file with mode: 0644]
customfield/field/select/version.php [new file with mode: 0644]
customfield/field/text/classes/data_controller.php [new file with mode: 0644]
customfield/field/text/classes/field_controller.php [new file with mode: 0644]
customfield/field/text/classes/privacy/provider.php [new file with mode: 0644]
customfield/field/text/lang/en/customfield_text.php [new file with mode: 0644]
customfield/field/text/tests/behat/field.feature [new file with mode: 0644]
customfield/field/text/tests/plugin_test.php [new file with mode: 0644]
customfield/field/text/version.php [new file with mode: 0644]
customfield/field/textarea/classes/data_controller.php [new file with mode: 0644]
customfield/field/textarea/classes/field_controller.php [new file with mode: 0644]
customfield/field/textarea/classes/privacy/provider.php [new file with mode: 0644]
customfield/field/textarea/lang/en/customfield_textarea.php [new file with mode: 0644]
customfield/field/textarea/lib.php [new file with mode: 0644]
customfield/field/textarea/tests/behat/default_value.feature [new file with mode: 0644]
customfield/field/textarea/tests/behat/field.feature [new file with mode: 0644]
customfield/field/textarea/tests/plugin_test.php [new file with mode: 0644]
customfield/field/textarea/version.php [new file with mode: 0644]
customfield/lib.php [new file with mode: 0644]
customfield/templates/field_data.mustache [new file with mode: 0644]
customfield/templates/list.mustache [new file with mode: 0644]
customfield/tests/api_test.php [new file with mode: 0644]
customfield/tests/behat/edit_categories.feature [new file with mode: 0644]
customfield/tests/behat/edit_fields_settings.feature [new file with mode: 0644]
customfield/tests/behat/required_field.feature [new file with mode: 0644]
customfield/tests/behat/unique_field.feature [new file with mode: 0644]
customfield/tests/category_controller_test.php [new file with mode: 0644]
customfield/tests/data_controller_test.php [new file with mode: 0644]
customfield/tests/field_controller_test.php [new file with mode: 0644]
customfield/tests/fixtures/test_instance_form.php [new file with mode: 0644]
customfield/tests/generator/lib.php [new file with mode: 0644]
customfield/tests/generator_test.php [new file with mode: 0644]
customfield/tests/privacy_test.php [new file with mode: 0644]
enrol/classes/privacy/provider.php
enrol/database/classes/task/sync_enrolments.php [new file with mode: 0644]
enrol/database/cli/sync.php
enrol/database/db/tasks.php [new file with mode: 0644]
enrol/database/db/upgrade.php
enrol/database/lang/en/enrol_database.php
enrol/database/upgrade.txt [new file with mode: 0644]
enrol/database/version.php
enrol/flatfile/db/upgrade.php
enrol/guest/db/upgrade.php
enrol/imsenterprise/db/upgrade.php
enrol/ldap/lang/en/enrol_ldap.php
enrol/lti/db/upgrade.php
enrol/manual/db/upgrade.php
enrol/mnet/db/upgrade.php
enrol/paypal/db/upgrade.php
enrol/self/db/upgrade.php
files/converter/unoconv/lang/en/fileconverter_unoconv.php
filter/mathjaxloader/db/upgrade.php
filter/mediaplugin/db/upgrade.php
filter/tex/db/upgrade.php
grade/classes/privacy/provider.php
grade/export/txt/tests/behat/export.feature
grade/export/xml/tests/behat/export.feature
grade/grading/form/guide/db/upgrade.php
grade/grading/form/rubric/db/upgrade.php
grade/grading/form/rubric/styles.css
grade/report/grader/lang/en/gradereport_grader.php
grade/report/user/db/upgrade.php
group/autogroup.php
group/autogroup_form.php
group/externallib.php
group/import.php
group/overview.php
group/tests/behat/auto_creation.feature
group/tests/behat/groups_import.feature
group/tests/externallib_test.php
group/tests/fixtures/groups_import.csv
install/lang/el/langconfig.php
install/lang/eu/admin.php
install/lang/eu/error.php
install/lang/eu/install.php
install/lang/hr/error.php
install/lang/pt/admin.php
install/lang/ro/install.php
lang/en/admin.php
lang/en/backup.php
lang/en/course.php
lang/en/customfield.php [new file with mode: 0644]
lang/en/deprecated.txt
lang/en/error.php
lang/en/grades.php
lang/en/message.php
lang/en/moodle.php
lang/en/plugin.php
lang/en/role.php
lang/en/search.php
lib/accesslib.php
lib/adminlib.php
lib/amd/build/form-autocomplete.min.js
lib/amd/build/icon_system_fontawesome.min.js
lib/amd/build/inplace_editable.min.js
lib/amd/build/modal.min.js
lib/amd/build/str.min.js
lib/amd/build/templates.min.js
lib/amd/src/form-autocomplete.js
lib/amd/src/icon_system_fontawesome.js
lib/amd/src/inplace_editable.js
lib/amd/src/modal.js
lib/amd/src/str.js
lib/amd/src/templates.js
lib/antivirus/clamav/db/upgrade.php
lib/behat/behat_base.php
lib/behat/form_field/behat_form_autocomplete.php
lib/behat/form_field/behat_form_field.php
lib/behat/lib.php
lib/blocklib.php
lib/classes/component.php
lib/classes/filetypes.php
lib/classes/output/external.php
lib/classes/output/icon_system_fontawesome.php
lib/classes/output/mustache_template_source_loader.php [new file with mode: 0644]
lib/classes/plugin_manager.php
lib/classes/plugininfo/customfield.php [new file with mode: 0644]
lib/classes/privacy/provider.php
lib/classes/shutdown_manager.php
lib/classes/task/database_logger.php [new file with mode: 0644]
lib/classes/task/logging_trait.php [new file with mode: 0644]
lib/classes/task/logmanager.php [new file with mode: 0644]
lib/classes/task/manager.php
lib/classes/task/messaging_cleanup_task.php
lib/classes/task/task_log_cleanup_task.php [new file with mode: 0644]
lib/classes/task/task_logger.php [new file with mode: 0644]
lib/classes/user.php
lib/cronlib.php
lib/db/access.php
lib/db/caches.php
lib/db/install.xml
lib/db/services.php
lib/db/tasks.php
lib/db/upgrade.php
lib/editor/atto/classes/privacy/provider.php
lib/editor/atto/db/upgrade.php
lib/editor/atto/plugins/equation/db/upgrade.php
lib/editor/atto/tests/privacy_provider_test.php [moved from lib/editor/atto/tests/privacy_provider.php with 100% similarity]
lib/editor/tinymce/db/upgrade.php
lib/editor/tinymce/plugins/spellchecker/db/upgrade.php
lib/evalmath/evalmath.class.php
lib/evalmath/readme_moodle.txt
lib/filebrowser/file_info_context_course.php
lib/formslib.php
lib/gradelib.php
lib/grouplib.php
lib/moodlelib.php
lib/outputcomponents.php
lib/outputlib.php
lib/outputrenderers.php
lib/outputrequirementslib.php
lib/phpmailer/README_MOODLE.txt
lib/phpmailer/src/PHPMailer.php
lib/phpminimumversionlib.php
lib/questionlib.php
lib/setuplib.php
lib/statslib.php
lib/tablelib.php
lib/templates/form_autocomplete_input.mustache
lib/templates/pix_icon.mustache
lib/templates/popover_region.mustache
lib/templates/url_select.mustache
lib/testing/generator/data_generator.php
lib/testing/tests/generator_test.php
lib/tests/accesslib_test.php
lib/tests/adhoc_task_test.php
lib/tests/behat/behat_data_generators.php
lib/tests/behat/behat_general.php
lib/tests/component_test.php
lib/tests/environment_test.php
lib/tests/gradelib_test.php
lib/tests/grouplib_test.php
lib/tests/mathslib_test.php
lib/tests/mustache_template_source_loader_test.php [new file with mode: 0644]
lib/tests/output_external_test.php [deleted file]
lib/tests/outputrequirementslib_test.php
lib/tests/questionlib_test.php
lib/tests/statslib_test.php
lib/tests/task_database_logger_test.php [new file with mode: 0644]
lib/tests/task_logging_test.php [new file with mode: 0644]
lib/tests/user_test.php
lib/typo3/class.t3lib_div.php
lib/typo3/readme_moodle.txt
lib/upgrade.txt
lib/userkey/tests/privacy_provider_test.php [moved from lib/userkey/tests/privacy_provider.php with 100% similarity]
media/player/videojs/lang/en/media_videojs.php
message/amd/build/message_drawer_view_conversation_patcher.min.js
message/amd/build/message_drawer_view_conversation_state_manager.min.js
message/amd/build/message_drawer_view_search.min.js
message/amd/src/message_drawer_view_conversation_patcher.js
message/amd/src/message_drawer_view_conversation_state_manager.js
message/amd/src/message_drawer_view_search.js
message/classes/api.php
message/classes/helper.php
message/classes/search/base_message.php
message/classes/task/migrate_message_data.php
message/externallib.php
message/lib.php
message/output/airnotifier/classes/privacy/provider.php
message/output/email/db/upgrade.php
message/output/email/message_output_email.php
message/output/jabber/db/upgrade.php
message/output/jabber/lang/en/message_jabber.php
message/output/popup/db/upgrade.php
message/templates/message_drawer_contacts_list.mustache
message/templates/message_drawer_messages_list.mustache
message/templates/message_drawer_non_contacts_list.mustache
message/templates/message_drawer_view_contact_body_content.mustache
message/templates/message_drawer_view_overview_section.mustache
message/templates/message_drawer_view_overview_section_group_messages.mustache
message/templates/message_drawer_view_overview_section_messages.mustache
message/templates/message_drawer_view_search_body.mustache
message/templates/message_drawer_view_search_results_content.mustache
message/tests/api_test.php
message/tests/externallib_test.php
message/tests/helper_test.php [new file with mode: 0644]
message/tests/migrate_message_data_task_test.php
message/tests/search_received_test.php
message/tests/search_sent_test.php
mod/assign/amd/build/grading_navigation.min.js
mod/assign/amd/build/grading_navigation_user_info.min.js
mod/assign/amd/build/participant_selector.min.js
mod/assign/amd/src/grading_navigation.js
mod/assign/amd/src/grading_navigation_user_info.js
mod/assign/amd/src/participant_selector.js
mod/assign/backup/moodle2/backup_assign_stepslib.php
mod/assign/classes/event/remove_submission_form_viewed.php [new file with mode: 0644]
mod/assign/classes/output/grading_app.php
mod/assign/db/access.php
mod/assign/db/install.xml
mod/assign/db/upgrade.php
mod/assign/externallib.php
mod/assign/feedback/comments/db/upgrade.php
mod/assign/feedback/comments/locallib.php
mod/assign/feedback/editpdf/ajax.php
mod/assign/feedback/editpdf/classes/combined_document.php
mod/assign/feedback/editpdf/classes/document_services.php
mod/assign/feedback/editpdf/classes/renderer.php
mod/assign/feedback/editpdf/classes/task/convert_submissions.php
mod/assign/feedback/editpdf/db/upgrade.php
mod/assign/feedback/editpdf/lang/en/assignfeedback_editpdf.php
mod/assign/feedback/editpdf/styles.css
mod/assign/feedback/editpdf/yui/build/moodle-assignfeedback_editpdf-editor/moodle-assignfeedback_editpdf-editor-debug.js
mod/assign/feedback/editpdf/yui/build/moodle-assignfeedback_editpdf-editor/moodle-assignfeedback_editpdf-editor-min.js
mod/assign/feedback/editpdf/yui/build/moodle-assignfeedback_editpdf-editor/moodle-assignfeedback_editpdf-editor.js
mod/assign/feedback/editpdf/yui/src/editor/js/editor.js
mod/assign/feedback/editpdf/yui/src/editor/js/globals.js
mod/assign/feedback/editpdf/yui/src/editor/meta/editor.json
mod/assign/feedback/file/db/upgrade.php
mod/assign/gradingbatchoperationsform.php
mod/assign/gradingtable.php
mod/assign/lang/en/assign.php
mod/assign/lib.php
mod/assign/locallib.php
mod/assign/mod_form.php
mod/assign/module.js
mod/assign/renderer.php
mod/assign/settings.php
mod/assign/styles.css
mod/assign/submission/comments/db/upgrade.php
mod/assign/submission/file/db/upgrade.php
mod/assign/submission/file/locallib.php
mod/assign/submission/onlinetext/db/upgrade.php
mod/assign/submission/onlinetext/locallib.php
mod/assign/submissionplugin.php
mod/assign/templates/grading_actions.mustache
mod/assign/templates/grading_navigation_user_selector.mustache
mod/assign/tests/behat/grading_status.feature
mod/assign/tests/behat/group_submission.feature
mod/assign/tests/behat/hide_grader.feature [new file with mode: 0644]
mod/assign/tests/behat/page_titles.feature [new file with mode: 0644]
mod/assign/tests/behat/remove_submission.feature [new file with mode: 0644]
mod/assign/tests/externallib_test.php
mod/assign/tests/locallib_test.php
mod/assign/upgrade.txt
mod/assign/version.php
mod/assignment/db/upgrade.php
mod/book/db/upgrade.php
mod/book/locallib.php
mod/book/tests/behat/show_hide_chapters.feature
mod/book/tool/print/classes/output/print_book_chapter_page.php [new file with mode: 0644]
mod/book/tool/print/classes/output/print_book_page.php [new file with mode: 0644]
mod/book/tool/print/classes/output/renderer.php [new file with mode: 0644]
mod/book/tool/print/index.php
mod/book/tool/print/locallib.php
mod/book/tool/print/print.css
mod/book/tool/print/templates/print_book.mustache [new file with mode: 0644]
mod/book/tool/print/templates/print_book_chapter.mustache [new file with mode: 0644]
mod/chat/db/upgrade.php
mod/choice/db/upgrade.php
mod/data/db/upgrade.php
mod/data/lib.php
mod/feedback/db/upgrade.php
mod/feedback/tests/behat/coursemapping.feature
mod/folder/db/upgrade.php
mod/folder/lib.php
mod/folder/module.js
mod/forum/classes/task/cron_task.php
mod/forum/classes/task/send_user_digests.php [new file with mode: 0644]
mod/forum/classes/task/send_user_notifications.php [new file with mode: 0644]
mod/forum/db/upgrade.php
mod/forum/deprecatedlib.php
mod/forum/lang/en/forum.php
mod/forum/lib.php
mod/forum/post.php
mod/forum/templates/forum_post_emaildigestfull_textemail.mustache
mod/forum/tests/behat/no_groups_in_course.feature
mod/forum/tests/cron_trait.php [new file with mode: 0644]
mod/forum/tests/generator_trait.php [new file with mode: 0644]
mod/forum/tests/lib_test.php
mod/forum/tests/mail_group_test.php [new file with mode: 0644]
mod/forum/tests/mail_test.php
mod/forum/tests/maildigest_test.php
mod/forum/tests/qanda_test.php [new file with mode: 0644]
mod/glossary/db/upgrade.php
mod/glossary/editcategories.php
mod/glossary/lang/en/glossary.php
mod/glossary/lib.php
mod/glossary/locallib.php
mod/glossary/tests/behat/behat_mod_glossary.php
mod/glossary/tests/behat/categories.feature
mod/glossary/tests/behat/entries_require_approval.feature
mod/imscp/db/upgrade.php
mod/label/db/upgrade.php
mod/lesson/continue.php
mod/lesson/db/upgrade.php
mod/lesson/lib.php
mod/lesson/locallib.php
mod/lesson/pagetypes/branchtable.php
mod/lesson/pagetypes/cluster.php
mod/lesson/pagetypes/endofbranch.php
mod/lesson/pagetypes/endofcluster.php
mod/lti/db/upgrade.php
mod/page/db/upgrade.php
mod/quiz/classes/question/bank/add_action_column.php
mod/quiz/db/upgrade.php
mod/quiz/lang/en/quiz.php
mod/quiz/locallib.php
mod/quiz/report/overview/db/upgrade.php
mod/quiz/report/statistics/db/upgrade.php
mod/quiz/settings.php
mod/quiz/styles.css
mod/quiz/tests/behat/editing_add_from_question_bank.feature
mod/quiz/tests/quiz_question_bank_view_test.php [new file with mode: 0644]
mod/resource/db/upgrade.php
mod/resource/locallib.php
mod/scorm/db/install.xml
mod/scorm/db/upgrade.php
mod/scorm/lib.php
mod/scorm/version.php
mod/survey/db/upgrade.php
mod/url/db/upgrade.php
mod/wiki/db/upgrade.php
mod/wiki/module.js
mod/workshop/classes/external.php
mod/workshop/db/upgrade.php
mod/workshop/form/accumulative/db/upgrade.php
mod/workshop/form/comments/db/upgrade.php
mod/workshop/form/numerrors/db/upgrade.php
mod/workshop/form/rubric/db/upgrade.php
mod/workshop/lib.php
mod/workshop/tests/external_test.php
phpunit.xml.dist
pix/e/file-text.png [new file with mode: 0644]
pix/e/file-text.svg [new file with mode: 0644]
pix/g/g1.png [new file with mode: 0644]
pix/g/g1.svg [new file with mode: 0644]
pix/i/customfield.png [new file with mode: 0644]
pix/i/customfield.svg [new file with mode: 0644]
pix/i/flagged.svg [new file with mode: 0644]
pix/i/unflagged.svg [new file with mode: 0644]
portfolio/boxnet/db/upgrade.php
portfolio/classes/privacy/provider.php
portfolio/googledocs/db/upgrade.php
portfolio/mahara/lang/en/portfolio_mahara.php
portfolio/picasa/db/upgrade.php
privacy/classes/local/request/moodle_content_writer.php
question/behaviour/behaviourbase.php
question/behaviour/manualgraded/db/upgrade.php
question/behaviour/manualgraded/tests/walkthrough_test.php
question/classes/bank/delete_action_column.php
question/classes/bank/preview_action_column.php
question/engine/bank.php
question/engine/tests/helpers.php
question/import_form.php
question/tests/bank_view_test.php
question/type/calculated/db/upgrade.php
question/type/calculated/edit_calculated_form.php
question/type/calculatedmulti/edit_calculatedmulti_form.php
question/type/ddimageortext/amd/build/form.min.js
question/type/ddimageortext/amd/build/question.min.js
question/type/ddimageortext/amd/src/form.js
question/type/ddimageortext/amd/src/question.js
question/type/ddimageortext/edit_ddimageortext_form.php
question/type/ddimageortext/lang/en/qtype_ddimageortext.php
question/type/ddimageortext/tests/edit_form_test.php [new file with mode: 0644]
question/type/ddimageortext/tests/helper.php
question/type/ddimageortext/tests/walkthrough_test.php
question/type/ddmarker/amd/build/form.min.js
question/type/ddmarker/amd/src/form.js
question/type/ddmarker/db/upgrade.php
question/type/ddmarker/edit_ddmarker_form.php
question/type/ddmarker/tests/edit_form_test.php [new file with mode: 0644]
question/type/ddwtos/amd/build/ddwtos.min.js
question/type/ddwtos/amd/src/ddwtos.js
question/type/ddwtos/tests/behat/preview.feature
question/type/ddwtos/tests/edit_form_test.php
question/type/ddwtos/tests/helper.php
question/type/essay/db/upgrade.php
question/type/gapselect/edit_form_base.php
question/type/gapselect/renderer.php
question/type/gapselect/rendererbase.php
question/type/gapselect/tests/edit_form_test.php
question/type/gapselect/tests/helper.php
question/type/gapselect/tests/walkthrough_test.php
question/type/match/db/upgrade.php
question/type/match/renderer.php
question/type/multianswer/db/upgrade.php
question/type/multichoice/db/upgrade.php
question/type/numerical/db/upgrade.php
question/type/random/classes/task/remove_unused_questions.php [new file with mode: 0644]
question/type/random/db/tasks.php [new file with mode: 0644]
question/type/random/db/upgrade.php
question/type/random/lang/en/qtype_random.php
question/type/random/tests/cleanup_task_test.php [new file with mode: 0644]
question/type/random/version.php
question/type/randomsamatch/db/upgrade.php
question/type/shortanswer/db/upgrade.php
report/completion/index.php
report/performance/lang/en/report_performance.php
report/progress/index.php
report/security/lang/en/report_security.php
report/security/locallib.php
repository/boxnet/db/upgrade.php
repository/classes/privacy/provider.php
repository/dropbox/db/upgrade.php
repository/googledocs/db/upgrade.php
repository/onedrive/classes/privacy/provider.php
repository/picasa/db/upgrade.php
search/classes/area_category.php [new file with mode: 0644]
search/classes/base.php
search/classes/base_block.php
search/classes/base_mod.php
search/classes/manager.php
search/classes/output/form/search.php
search/classes/output/renderer.php
search/engine/solr/settings.php
search/index.php
search/tests/area_category_test.php [new file with mode: 0644]
search/tests/base_test.php
search/tests/manager_test.php
tag/tests/external_test.php
theme/boost/config.php
theme/boost/scss/editor.scss [new file with mode: 0644]
theme/boost/scss/moodle/admin.scss
theme/boost/scss/moodle/blocks.scss
theme/boost/scss/moodle/core.scss
theme/boost/scss/moodle/course.scss
theme/boost/scss/moodle/forms.scss
theme/boost/scss/moodle/modules.scss
theme/boost/scss/moodle/question.scss
theme/boost/scss/preset/default.scss
theme/boost/style/moodle.css
theme/boost/templates/core/form_autocomplete_input.mustache
theme/boost/templates/core_form/element-template.mustache
theme/boost/templates/mod_assign/grading_actions.mustache
theme/boost/templates/mod_assign/grading_navigation_user_selector.mustache
theme/boost/templates/navbar.mustache
theme/boost/upgrade.txt
theme/bootstrapbase/less/moodle/blocks.less
theme/bootstrapbase/less/moodle/bs4-compat.less
theme/bootstrapbase/less/moodle/forms.less
theme/bootstrapbase/less/moodle/question.less
theme/bootstrapbase/style/moodle.css
theme/bootstrapbase/templates/block_myoverview/course-action-menu.mustache
theme/bootstrapbase/templates/core_message/message_drawer_view_conversation_header_content_type_private.mustache
theme/bootstrapbase/templates/core_message/message_drawer_view_overview_section.mustache
theme/more/db/upgrade.php
theme/upgrade.txt
user/amd/build/status_field.min.js
user/amd/src/status_field.js
user/classes/output/myprofile/renderer.php
user/classes/output/status_field.php
user/classes/participants_table.php
user/classes/privacy/provider.php
user/classes/search/user.php
user/index.php
user/lib.php
user/profile/field/checkbox/classes/privacy/provider.php
user/profile/field/datetime/classes/privacy/provider.php
user/profile/field/menu/classes/privacy/provider.php
user/profile/field/text/classes/privacy/provider.php
user/profile/field/textarea/classes/privacy/provider.php
user/templates/status_details.mustache
user/templates/status_field.mustache
user/tests/behat/edit_user_enrolment.feature
user/tests/search_test.php
userpix/index.php
version.php
webservice/classes/privacy/provider.php

index aa0db16..ec1b68c 100644 (file)
@@ -14,7 +14,7 @@ language: php
 php:
     # We only run the highest and lowest supported versions to reduce the load on travis-ci.org.
     - 7.2
-    - 7.0
+    - 7.1
 
 addons:
   postgresql: "9.6"
@@ -63,7 +63,7 @@ matrix:
         # Exclude it on all versions except for 7.2
 
         - env: DB=mysqli   TASK=PHPUNIT
-          php: 7.0
+          php: 7.1
 
 cache:
     directories:
diff --git a/admin/classes/task_log_table.php b/admin/classes/task_log_table.php
new file mode 100644 (file)
index 0000000..2bb892d
--- /dev/null
@@ -0,0 +1,288 @@
+<?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/>.
+
+/**
+ * Task log table.
+ *
+ * @package    core_admin
+ * @copyright  2018 Andrew Nicols <andrew@nicols.co.uk>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace core_admin;
+
+defined('MOODLE_INTERNAL') || die();
+
+require_once($CFG->libdir . '/tablelib.php');
+
+/**
+ * Table to display list of task logs.
+ *
+ * @copyright  2018 Andrew Nicols <andrew@nicols.co.uk>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class task_log_table extends \table_sql {
+
+    /**
+     * Constructor for the task_log table.
+     *
+     * @param   string      $filter
+     * @param   int         $resultfilter
+     */
+    public function __construct(string $filter = '', int $resultfilter = null) {
+        global $DB;
+
+        if (-1 === $resultfilter) {
+            $resultfilter = null;
+        }
+
+        parent::__construct('tasklogs');
+
+        $columnheaders = [
+            'classname'  => get_string('name'),
+            'type'       => get_string('tasktype', 'admin'),
+            'userid'     => get_string('user', 'admin'),
+            'timestart'  => get_string('task_starttime', 'admin'),
+            'duration'   => get_string('task_duration', 'admin'),
+            'db'         => get_string('task_dbstats', 'admin'),
+            'result'     => get_string('task_result', 'admin'),
+            'actions'    => '',
+        ];
+        $this->define_columns(array_keys($columnheaders));
+        $this->define_headers(array_values($columnheaders));
+
+        // The name column is a header.
+        $this->define_header_column('classname');
+
+        // This table is not collapsible.
+        $this->collapsible(false);
+
+        // The actions class should not wrap. Use the BS text utility class.
+        $this->column_class('actions', 'text-nowrap');
+
+        // Allow pagination.
+        $this->pageable(true);
+
+        // Allow sorting. Default to sort by timestarted DESC.
+        $this->sortable(true, 'timestart', SORT_DESC);
+
+        // Add filtering.
+        $where = [];
+        $params = [];
+        if (!empty($filter)) {
+            $orwhere = [];
+            $filter = str_replace('\\', '\\\\', $filter);
+
+            // Check the class name.
+            $orwhere[] = $DB->sql_like('classname', ':classfilter', false, false);
+            $params['classfilter'] = '%' . $DB->sql_like_escape($filter) . '%';
+
+            $orwhere[] = $DB->sql_like('output', ':outputfilter', false, false);
+            $params['outputfilter'] = '%' . $DB->sql_like_escape($filter) . '%';
+
+            $where[] = "(" . implode(' OR ', $orwhere) . ")";
+        }
+
+        if (null !== $resultfilter) {
+            $where[] = 'tl.result = :result';
+            $params['result'] = $resultfilter;
+        }
+
+        $where = implode(' AND ', $where);
+
+        $this->set_sql('', '', $where, $params);
+    }
+
+    /**
+     * Query the db. Store results in the table object for use by build_table.
+     *
+     * @param int $pagesize size of page for paginated displayed table.
+     * @param bool $useinitialsbar do you want to use the initials bar. Bar
+     * will only be used if there is a fullname column defined for the table.
+     */
+    public function query_db($pagesize, $useinitialsbar = true) {
+        global $DB;
+
+        // Fetch the attempts.
+        $sort = $this->get_sql_sort();
+        if ($sort) {
+            $sort = "ORDER BY $sort";
+        }
+
+        $extrafields = get_extra_user_fields(\context_system::instance());
+        $userfields = \user_picture::fields('u', $extrafields, 'userid2', 'user');
+
+        $where = '';
+        if (!empty($this->sql->where)) {
+            $where = "WHERE {$this->sql->where}";
+        }
+
+        $sql = "SELECT
+                    tl.*,
+                    tl.dbreads + tl.dbwrites AS db,
+                    tl.timeend - tl.timestart AS duration,
+                    {$userfields}
+                FROM {task_log} tl
+           LEFT JOIN {user} u ON u.id = tl.userid
+                {$where}
+                {$sort}";
+
+        $this->pagesize($pagesize, $DB->count_records_sql("SELECT COUNT('x') FROM {task_log} tl {$where}", $this->sql->params));
+        if (!$this->is_downloading()) {
+            $this->rawdata = $DB->get_records_sql($sql, $this->sql->params, $this->get_page_start(), $this->get_page_size());
+        } else {
+            $this->rawdata = $DB->get_records_sql($sql, $this->sql->params);
+        }
+    }
+
+    /**
+     * Format the name cell.
+     *
+     * @param   \stdClass $row
+     * @return  string
+     */
+    public function col_classname($row) : string {
+        $output = '';
+        if (class_exists($row->classname)) {
+            $task = new $row->classname;
+            if ($task instanceof \core\task\scheduled_task) {
+                $output = $task->get_name();
+            }
+        }
+
+        $output .= \html_writer::tag('div', "\\{$row->classname}", [
+                'class' => 'task-class',
+            ]);
+        return $output;
+    }
+
+    /**
+     * Format the type cell.
+     *
+     * @param   \stdClass $row
+     * @return  string
+     */
+    public function col_type($row) : string {
+        if (\core\task\database_logger::TYPE_SCHEDULED == $row->type) {
+            return get_string('task_type:scheduled', 'admin');
+        } else {
+            return get_string('task_type:adhoc', 'admin');
+        }
+    }
+
+    /**
+     * Format the timestart cell.
+     *
+     * @param   \stdClass $row
+     * @return  string
+     */
+    public function col_result($row) : string {
+        if ($row->result) {
+            return get_string('task_result:failed', 'admin');
+        } else {
+            return get_string('success');
+        }
+    }
+
+    /**
+     * Format the timestart cell.
+     *
+     * @param   \stdClass $row
+     * @return  string
+     */
+    public function col_timestart($row) : string {
+        return userdate($row->timestart, get_string('strftimedatetimeshort', 'langconfig'));
+    }
+
+    /**
+     * Format the duration cell.
+     *
+     * @param   \stdClass $row
+     * @return  string
+     */
+    public function col_duration($row) : string {
+        $duration = round($row->timeend - $row->timestart, 2);
+
+        if (empty($duration)) {
+            // The format_time function returns 'now' when the difference is exactly 0.
+            // Note: format_time performs concatenation in exactly this fashion so we should do this for consistency.
+            return '0 ' . get_string('secs', 'moodle');
+        }
+
+        return format_time($duration);
+    }
+
+    /**
+     * Format the DB details cell.
+     *
+     * @param   \stdClass $row
+     * @return  string
+     */
+    public function col_db($row) : string {
+        $output = '';
+
+        $output .= \html_writer::div(get_string('task_stats:dbreads', 'admin', $row->dbreads));
+        $output .= \html_writer::div(get_string('task_stats:dbwrites', 'admin', $row->dbwrites));
+
+        return $output;
+    }
+
+    /**
+     * Format the actions cell.
+     *
+     * @param   \stdClass $row
+     * @return  string
+     */
+    public function col_actions($row) : string {
+        global $OUTPUT;
+
+        $actions = [];
+
+        $url = new \moodle_url('/admin/tasklogs.php', ['logid' => $row->id]);
+
+        // Quick view.
+        $actions[] = $OUTPUT->action_icon(
+            $url,
+            new \pix_icon('e/search', get_string('view')),
+            new \popup_action('click', $url)
+        );
+
+        // Download.
+        $actions[] = $OUTPUT->action_icon(
+            new \moodle_url($url, ['download' => true]),
+            new \pix_icon('t/download', get_string('download'))
+        );
+
+        return implode('&nbsp;', $actions);
+    }
+
+    /**
+     * Format the user cell.
+     *
+     * @param   \stdClass $row
+     * @return  string
+     */
+    public function col_userid($row) : string {
+        if (empty($row->userid)) {
+            return '';
+        }
+
+        $user = (object) [];
+        username_load_fields_from_object($user, $row, 'user');
+
+        return fullname($user);
+    }
+}
diff --git a/admin/cli/uninstall_plugins.php b/admin/cli/uninstall_plugins.php
new file mode 100644 (file)
index 0000000..e093d89
--- /dev/null
@@ -0,0 +1,163 @@
+<?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/>.
+
+/**
+ * CLI script to uninstall plugins.
+ *
+ * @package    core
+ * @subpackage cli
+ * @copyright  2018 Dmitrii Metelkin <dmitriim@catalyst-au.net>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+define('CLI_SCRIPT', true);
+
+require(__DIR__ . '/../../config.php');
+require_once($CFG->libdir . '/clilib.php');
+require_once($CFG->libdir . '/adminlib.php');
+
+$help = "Command line tool to uninstall plugins.
+
+Options:
+    -h --help                   Print this help.
+    --show-all                  Displays a list of all installed plugins.
+    --show-missing              Displays a list of plugins missing from disk.
+    --purge-missing             Uninstall all missing from disk plugins.
+    --plugins=<plugin name>     A comma separated list of plugins to be uninstalled. E.g. mod_assign,mod_forum
+    --run                       Execute uninstall. If this option is not set, then the script will be run in a dry mode.
+
+Examples:
+
+    # php uninstall_plugins.php  --show-all
+        Prints tab-separated list of all installed plugins.
+
+    # php uninstall_plugins.php  --show-missing
+        Prints tab-separated list of all missing from disk plugins.
+
+    # php uninstall_plugins.php  --purge-missing
+        A dry run of uninstalling all missing plugins.
+
+    # php uninstall_plugins.php  --purge-missing --run
+        Run uninstall of all missing plugins.
+
+    # php uninstall_plugins.php  --plugins=mod_assign,mod_forum
+        A dry run of uninstalling mod_assign and mod_forum plugins.
+
+    # php uninstall_plugins.php  --plugins=mod_assign,mod_forum --run
+        Run uninstall for mod_assign and mod_forum plugins.
+";
+
+list($options, $unrecognised) = cli_get_params([
+    'help' => false,
+    'show-all' => false,
+    'show-missing' => false,
+    'purge-missing' => false,
+    'plugins' => false,
+    'run' => false,
+], [
+    'h' => 'help'
+]);
+
+if ($unrecognised) {
+    $unrecognised = implode(PHP_EOL.'  ', $unrecognised);
+    cli_error(get_string('cliunknowoption', 'core_admin', $unrecognised));
+}
+
+if ($options['help']) {
+    cli_writeln($help);
+    exit(0);
+}
+
+$pluginman = core_plugin_manager::instance();
+$plugininfo = $pluginman->get_plugins();
+
+if ($options['show-all'] || $options['show-missing']) {
+    foreach ($plugininfo as $type => $plugins) {
+        foreach ($plugins as $name => $plugin) {
+            $pluginstring = $plugin->component . "\t" . $plugin->displayname;
+
+            if ($options['show-all']) {
+                cli_writeln($pluginstring);
+            } else {
+                if ($plugin->get_status() === core_plugin_manager::PLUGIN_STATUS_MISSING) {
+                    cli_writeln($pluginstring);
+                }
+            }
+        }
+    }
+
+    exit(0);
+}
+
+if ($options['purge-missing']) {
+    foreach ($plugininfo as $type => $plugins) {
+        foreach ($plugins as $name => $plugin) {
+            if ($plugin->get_status() === core_plugin_manager::PLUGIN_STATUS_MISSING) {
+
+                $pluginstring = $plugin->component . "\t" . $plugin->displayname;
+
+                if ($pluginman->can_uninstall_plugin($plugin->component)) {
+                    if ($options['run']) {
+                        cli_writeln('Uninstalling: ' . $pluginstring);
+
+                        $progress = new progress_trace_buffer(new text_progress_trace(), true);
+                        $pluginman->uninstall_plugin($plugin->component, $progress);
+                        $progress->finished();
+                        cli_write($progress->get_buffer());
+                    } else {
+                        cli_writeln('Will be uninstalled: ' . $pluginstring);
+                    }
+                } else {
+                    cli_writeln('Can not be uninstalled: ' . $pluginstring);
+                }
+            }
+        }
+    }
+
+    exit(0);
+}
+
+if ($options['plugins']) {
+    $components = explode(',', $options['plugins']);
+    foreach ($components as $component) {
+        $plugin = $pluginman->get_plugin_info($component);
+
+        if (is_null($plugin)) {
+            cli_writeln('Unknown plugin: ' . $component);
+        } else {
+            $pluginstring = $plugin->component . "\t" . $plugin->displayname;
+
+            if ($pluginman->can_uninstall_plugin($plugin->component)) {
+                if ($options['run']) {
+                    cli_writeln('Uninstalling: ' . $pluginstring);
+                    $progress = new progress_trace_buffer(new text_progress_trace(), true);
+                    $pluginman->uninstall_plugin($plugin->component, $progress);
+                    $progress->finished();
+                    cli_write($progress->get_buffer());
+                } else {
+                    cli_writeln('Will be uninstalled: ' . $pluginstring);
+                }
+            } else {
+                cli_writeln('Can not be uninstalled: ' . $pluginstring);
+            }
+        }
+    }
+
+    exit(0);
+}
+
+cli_writeln($help);
+exit(0);
diff --git a/admin/customfields.php b/admin/customfields.php
new file mode 100644 (file)
index 0000000..2f7681e
--- /dev/null
@@ -0,0 +1,62 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * Allows the admin to enable, disable and uninstall custom fields
+ *
+ * @package    core_admin
+ * @copyright  2018 Daniel Neis Araujo
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+require_once('../config.php');
+require_once($CFG->libdir.'/adminlib.php');
+
+$action  = required_param('action', PARAM_ALPHANUMEXT);
+$customfieldname = required_param('field', PARAM_PLUGIN);
+
+$syscontext = context_system::instance();
+$PAGE->set_url('/admin/customfields.php');
+$PAGE->set_context($syscontext);
+
+require_login();
+require_capability('moodle/site:config', $syscontext);
+require_sesskey();
+
+$return = new moodle_url('/admin/settings.php', array('section' => 'managecustomfields'));
+
+$customfieldplugins = core_plugin_manager::instance()->get_plugins_of_type('customfield');
+$sortorder = array_flip(array_keys($customfieldplugins));
+
+if (!isset($customfieldplugins[$customfieldname])) {
+    print_error('customfieldnotfound', 'error', $return, $customfieldname);
+}
+
+switch ($action) {
+    case 'disable':
+        if ($customfieldplugins[$customfieldname]->is_enabled()) {
+            set_config('disabled', 1, 'customfield_'. $customfieldname);
+            core_plugin_manager::reset_caches();
+        }
+        break;
+    case 'enable':
+        if (!$customfieldplugins[$customfieldname]->is_enabled()) {
+            unset_config('disabled', 'customfield_'. $customfieldname);
+            core_plugin_manager::reset_caches();
+        }
+        break;
+}
+redirect($return);
index 40e66be..ca20d0c 100644 (file)
       </CUSTOM_CHECK>
     </CUSTOM_CHECKS>
   </MOODLE>
+  <MOODLE version="3.7" requires="3.2">
+    <UNICODE level="required">
+      <FEEDBACK>
+        <ON_ERROR message="unicoderequired" />
+      </FEEDBACK>
+    </UNICODE>
+    <DATABASE level="required">
+      <VENDOR name="mariadb" version="5.5.31" />
+      <VENDOR name="mysql" version="5.6" />
+      <VENDOR name="postgres" version="9.4" />
+      <VENDOR name="mssql" version="10.0" />
+      <VENDOR name="oracle" version="11.2" />
+    </DATABASE>
+    <PHP version="7.1.0" level="required">
+    </PHP>
+    <PCREUNICODE level="optional">
+      <FEEDBACK>
+        <ON_CHECK message="pcreunicodewarning" />
+      </FEEDBACK>
+    </PCREUNICODE>
+    <PHP_EXTENSIONS>
+      <PHP_EXTENSION name="iconv" level="required">
+        <FEEDBACK>
+          <ON_ERROR message="iconvrequired" />
+        </FEEDBACK>
+      </PHP_EXTENSION>
+      <PHP_EXTENSION name="mbstring" level="optional">
+        <FEEDBACK>
+          <ON_CHECK message="mbstringrecommended" />
+        </FEEDBACK>
+      </PHP_EXTENSION>
+      <PHP_EXTENSION name="curl" level="required">
+        <FEEDBACK>
+          <ON_ERROR message="curlrequired" />
+        </FEEDBACK>
+      </PHP_EXTENSION>
+      <PHP_EXTENSION name="openssl" level="required">
+        <FEEDBACK>
+          <ON_ERROR message="opensslrequired" />
+        </FEEDBACK>
+      </PHP_EXTENSION>
+      <PHP_EXTENSION name="tokenizer" level="optional">
+        <FEEDBACK>
+          <ON_CHECK message="tokenizerrecommended" />
+        </FEEDBACK>
+      </PHP_EXTENSION>
+      <PHP_EXTENSION name="xmlrpc" level="optional">
+        <FEEDBACK>
+          <ON_CHECK message="xmlrpcrecommended" />
+        </FEEDBACK>
+      </PHP_EXTENSION>
+      <PHP_EXTENSION name="soap" level="optional">
+        <FEEDBACK>
+          <ON_CHECK message="soaprecommended" />
+        </FEEDBACK>
+      </PHP_EXTENSION>
+      <PHP_EXTENSION name="ctype" level="required">
+        <FEEDBACK>
+          <ON_ERROR message="ctyperequired" />
+        </FEEDBACK>
+      </PHP_EXTENSION>
+      <PHP_EXTENSION name="zip" level="required">
+        <FEEDBACK>
+          <ON_ERROR message="ziprequired" />
+        </FEEDBACK>
+      </PHP_EXTENSION>
+      <PHP_EXTENSION name="zlib" level="required">
+      </PHP_EXTENSION>
+      <PHP_EXTENSION name="gd" level="required">
+        <FEEDBACK>
+          <ON_ERROR message="gdrequired" />
+        </FEEDBACK>
+      </PHP_EXTENSION>
+      <PHP_EXTENSION name="simplexml" level="required">
+        <FEEDBACK>
+          <ON_ERROR message="simplexmlrequired" />
+        </FEEDBACK>
+      </PHP_EXTENSION>
+      <PHP_EXTENSION name="spl" level="required">
+        <FEEDBACK>
+          <ON_ERROR message="splrequired" />
+        </FEEDBACK>
+      </PHP_EXTENSION>
+      <PHP_EXTENSION name="pcre" level="required">
+      </PHP_EXTENSION>
+      <PHP_EXTENSION name="dom" level="required">
+      </PHP_EXTENSION>
+      <PHP_EXTENSION name="xml" level="required">
+      </PHP_EXTENSION>
+      <PHP_EXTENSION name="xmlreader" level="required">
+      </PHP_EXTENSION>
+      <PHP_EXTENSION name="intl" level="required">
+        <FEEDBACK>
+          <ON_ERROR message="intlrequired" />
+        </FEEDBACK>
+      </PHP_EXTENSION>
+      <PHP_EXTENSION name="json" level="required">
+      </PHP_EXTENSION>
+      <PHP_EXTENSION name="hash" level="required"/>
+      <PHP_EXTENSION name="fileinfo" level="required"/>
+    </PHP_EXTENSIONS>
+    <PHP_SETTINGS>
+      <PHP_SETTING name="memory_limit" value="96M" level="required">
+        <FEEDBACK>
+          <ON_ERROR message="settingmemorylimit" />
+        </FEEDBACK>
+      </PHP_SETTING>
+      <PHP_SETTING name="file_uploads" value="1" level="optional">
+        <FEEDBACK>
+          <ON_CHECK message="settingfileuploads" />
+        </FEEDBACK>
+      </PHP_SETTING>
+      <PHP_SETTING name="opcache.enable" value="1" level="optional">
+        <FEEDBACK>
+          <ON_CHECK message="opcacherecommended" />
+        </FEEDBACK>
+      </PHP_SETTING>
+    </PHP_SETTINGS>
+    <CUSTOM_CHECKS>
+      <CUSTOM_CHECK file="lib/upgradelib.php" function="check_database_storage_engine" level="required">
+        <FEEDBACK>
+          <ON_ERROR message="unsupporteddbstorageengine" />
+        </FEEDBACK>
+      </CUSTOM_CHECK>
+      <CUSTOM_CHECK file="question/engine/upgrade/upgradelib.php" function="quiz_attempts_upgraded" level="required">
+        <FEEDBACK>
+          <ON_ERROR message="quizattemptsupgradedmessage" />
+        </FEEDBACK>
+      </CUSTOM_CHECK>
+      <CUSTOM_CHECK file="lib/upgradelib.php" function="check_slasharguments" level="optional">
+        <FEEDBACK>
+          <ON_CHECK message="slashargumentswarning" />
+        </FEEDBACK>
+      </CUSTOM_CHECK>
+      <CUSTOM_CHECK file="lib/upgradelib.php" function="check_database_tables_row_format" level="optional">
+        <FEEDBACK>
+          <ON_CHECK message="unsupporteddbtablerowformat" />
+        </FEEDBACK>
+      </CUSTOM_CHECK>
+      <CUSTOM_CHECK file="lib/upgradelib.php" function="check_unoconv_version" level="optional">
+        <FEEDBACK>
+          <ON_CHECK message="unoconvwarning" />
+        </FEEDBACK>
+      </CUSTOM_CHECK>
+      <CUSTOM_CHECK file="lib/upgradelib.php" function="check_libcurl_version" level="optional">
+        <FEEDBACK>
+          <ON_CHECK message="libcurlwarning" />
+        </FEEDBACK>
+      </CUSTOM_CHECK>
+      <CUSTOM_CHECK file="lib/upgradelib.php" function="check_mysql_file_format" level="required">
+        <FEEDBACK>
+          <ON_ERROR message="unsupporteddbfileformat" />
+        </FEEDBACK>
+      </CUSTOM_CHECK>
+      <CUSTOM_CHECK file="lib/upgradelib.php" function="check_mysql_file_per_table" level="required">
+        <FEEDBACK>
+          <ON_ERROR message="unsupporteddbfilepertable" />
+        </FEEDBACK>
+      </CUSTOM_CHECK>
+      <CUSTOM_CHECK file="lib/upgradelib.php" function="check_mysql_large_prefix" level="required">
+        <FEEDBACK>
+          <ON_ERROR message="unsupporteddblargeprefix" />
+        </FEEDBACK>
+      </CUSTOM_CHECK>
+      <CUSTOM_CHECK file="lib/upgradelib.php" function="check_is_https" level="optional">
+        <FEEDBACK>
+          <ON_CHECK message="ishttpswarning" />
+        </FEEDBACK>
+      </CUSTOM_CHECK>
+      <CUSTOM_CHECK file="lib/upgradelib.php" function="check_mysql_incomplete_unicode_support" level="optional">
+        <FEEDBACK>
+          <ON_CHECK message="incompleteunicodesupport" />
+        </FEEDBACK>
+      </CUSTOM_CHECK>
+      <CUSTOM_CHECK file="lib/upgradelib.php" function="check_sixtyfour_bits" level="optional">
+        <FEEDBACK>
+          <ON_CHECK message="sixtyfourbitswarning" />
+        </FEEDBACK>
+      </CUSTOM_CHECK>
+    </CUSTOM_CHECKS>
+  </MOODLE>
 </COMPATIBILITY_MATRIX>
index 911a86b..fb5b0a3 100644 (file)
@@ -153,6 +153,7 @@ $table = new html_table();
 $table->id = 'core-search-areas';
 $table->head = [
     get_string('searcharea', 'search'),
+    get_string('searchareacategories', 'search'),
     get_string('enable'),
     get_string('newestdocindexed', 'admin'),
     get_string('searchlastrun', 'admin'),
@@ -165,6 +166,14 @@ foreach ($searchareas as $area) {
     $areaid = $area->get_area_id();
     $columns = array(new html_table_cell($area->get_visible_name()));
 
+    $areacategories = [];
+    foreach (\core_search\manager::get_search_area_categories() as $category) {
+        if (key_exists($areaid, $category->get_areas())) {
+            $areacategories[] = $category->get_visiblename();
+        }
+    }
+    $columns[] = new html_table_cell(implode(', ', $areacategories));
+
     if ($area->is_enabled()) {
         $columns[] = $OUTPUT->action_icon(admin_searcharea_action_url('disable', $areaid),
             new pix_icon('t/hide', get_string('disable'), 'moodle', array('title' => '', 'class' => 'iconsmall')),
index f23bf5f..739c96e 100644 (file)
@@ -37,6 +37,12 @@ if ($hassiteconfig or has_any_capability($capabilities, $systemcontext)) {
             array('moodle/category:manage', 'moodle/course:create')
         )
     );
+    $ADMIN->add('courses',
+        new admin_externalpage('course_customfield', new lang_string('course_customfield', 'admin'),
+            $CFG->wwwroot . '/course/customfield.php',
+            array('moodle/course:configurecustomfields')
+        )
+    );
     $ADMIN->add('courses',
         new admin_externalpage('addcategory', new lang_string('addcategory', 'admin'),
             new moodle_url('/course/editcategory.php', array('parent' => 0)),
index 9a2202f..616d10e 100644 (file)
@@ -59,6 +59,18 @@ if ($hassiteconfig) {
         $plugin->load_settings($ADMIN, 'formatsettings', $hassiteconfig);
     }
 
+    // Custom fields.
+    $ADMIN->add('modules', new admin_category('customfieldsettings', new lang_string('customfields', 'core_customfield')));
+    $temp = new admin_settingpage('managecustomfields', new lang_string('managecustomfields', 'core_admin'));
+    $temp->add(new admin_setting_managecustomfields());
+    $ADMIN->add('customfieldsettings', $temp);
+    $plugins = core_plugin_manager::instance()->get_plugins_of_type('customfield');
+    core_collator::asort_objects_by_property($plugins, 'displayname');
+    foreach ($plugins as $plugin) {
+        /** @var \core\plugininfo\customfield $plugin */
+        $plugin->load_settings($ADMIN, 'customfieldsettings', $hassiteconfig);
+    }
+
     // blocks
     $ADMIN->add('modules', new admin_category('blocksettings', new lang_string('blocks')));
     $ADMIN->add('blocksettings', new admin_page_manageblocks());
@@ -573,6 +585,25 @@ if ($hassiteconfig) {
             new lang_string('searchallavailablecourses_desc', 'admin'),
             0, $options));
 
+    // Search display options.
+    $temp->add(new admin_setting_heading('searchdisplay', new lang_string('searchdisplay', 'admin'), ''));
+    $temp->add(new admin_setting_configcheckbox('searchenablecategories',
+        new lang_string('searchenablecategories', 'admin'),
+        new lang_string('searchenablecategories_desc', 'admin'),
+        0));
+    $options = [];
+    foreach (\core_search\manager::get_search_area_categories() as $category) {
+        $options[$category->get_name()] = $category->get_visiblename();
+    }
+    $temp->add(new admin_setting_configselect('searchdefaultcategory',
+        new lang_string('searchdefaultcategory', 'admin'),
+        new lang_string('searchdefaultcategory_desc', 'admin'),
+        \core_search\manager::SEARCH_AREA_CATEGORY_ALL, $options));
+    $temp->add(new admin_setting_configcheckbox('searchhideallcategory',
+        new lang_string('searchhideallcategory', 'admin'),
+        new lang_string('searchhideallcategory_desc', 'admin'),
+        0));
+
     $ADMIN->add('searchplugins', $temp);
     $ADMIN->add('searchplugins', new admin_externalpage('searchareas', new lang_string('searchareas', 'admin'),
         new moodle_url('/admin/searchareas.php')));
index f4c24c0..96c7871 100644 (file)
@@ -4,7 +4,6 @@
 
 if ($hassiteconfig) { // speedup for non-admins, add all caps used on this page
 
-
 // "systempaths" settingpage
 $temp = new admin_settingpage('systempaths', new lang_string('systempaths','admin'));
 $temp->add(new admin_setting_configexecutable('pathtophp', new lang_string('pathtophp', 'admin'),
@@ -212,6 +211,92 @@ $temp->add(new admin_setting_configtext('curltimeoutkbitrate', new lang_string('
 $ADMIN->add('server', $temp);
 
 
+$ADMIN->add('server', new admin_category('taskconfig', new lang_string('taskadmintitle', 'admin')));
+$temp = new admin_settingpage('taskprocessing', new lang_string('taskprocessing','admin'));
+$temp->add(
+    new admin_setting_configtext(
+        'task_scheduled_concurrency_limit',
+        new lang_string('task_scheduled_concurrency_limit', 'admin'),
+        new lang_string('task_scheduled_concurrency_limit_desc', 'admin'),
+        3,
+        PARAM_INT
+    )
+);
+
+$temp->add(
+    new admin_setting_configduration(
+        'task_scheduled_max_runtime',
+        new lang_string('task_scheduled_max_runtime', 'admin'),
+        new lang_string('task_scheduled_max_runtime_desc', 'admin'),
+        30 * MINSECS
+    )
+);
+
+$temp->add(
+    new admin_setting_configtext(
+        'task_adhoc_concurrency_limit',
+        new lang_string('task_adhoc_concurrency_limit', 'admin'),
+        new lang_string('task_adhoc_concurrency_limit_desc', 'admin'),
+        3,
+        PARAM_INT
+    )
+);
+
+$temp->add(
+    new admin_setting_configduration(
+        'task_adhoc_max_runtime',
+        new lang_string('task_adhoc_max_runtime', 'admin'),
+        new lang_string('task_adhoc_max_runtime_desc', 'admin'),
+        30 * MINSECS
+    )
+);
+$ADMIN->add('taskconfig', $temp);
+
+$temp = new admin_settingpage('tasklogging', new lang_string('tasklogging','admin'));
+$temp->add(
+    new admin_setting_configselect(
+        'task_logmode',
+        new lang_string('task_logmode', 'admin'),
+        new lang_string('task_logmode_desc', 'admin'),
+        \core\task\logmanager::MODE_ALL,
+        [
+            \core\task\logmanager::MODE_ALL => new lang_string('task_logmode_all', 'admin'),
+            \core\task\logmanager::MODE_FAILONLY => new lang_string('task_logmode_failonly', 'admin'),
+            \core\task\logmanager::MODE_NONE => new lang_string('task_logmode_none', 'admin'),
+        ]
+    )
+);
+
+if (\core\task\logmanager::uses_standard_settings()) {
+    $temp->add(
+        new admin_setting_configduration(
+            'task_logretention',
+            new \lang_string('task_logretention', 'admin'),
+            new \lang_string('task_logretention_desc', 'admin'),
+            28 * DAYSECS
+        )
+    );
+
+    $temp->add(
+        new admin_setting_configtext(
+            'task_logretainruns',
+            new \lang_string('task_logretainruns', 'admin'),
+            new \lang_string('task_logretainruns_desc', 'admin'),
+            20,
+            PARAM_INT
+        )
+    );
+}
+$ADMIN->add('taskconfig', $temp);
+
+if (\core\task\logmanager::uses_standard_settings()) {
+    $ADMIN->add('taskconfig', new admin_externalpage(
+        'tasklogs',
+        new lang_string('tasklogs','admin'),
+        "{$CFG->wwwroot}/{$CFG->admin}/tasklogs.php"
+    ));
+}
+
 // E-mail settings.
 $ADMIN->add('server', new admin_category('email', new lang_string('categoryemail', 'admin')));
 
index db26385..a108b42 100644 (file)
@@ -21,8 +21,28 @@ if ($hassiteconfig) { // speedup for non-admins, add all caps used on this page
         0)
     );
 
-    $options = array(DAYSECS=>new lang_string('secondstotime86400'), WEEKSECS=>new lang_string('secondstotime604800'), 2620800=>new lang_string('nummonths', 'moodle', 1), 15724800=>new lang_string('nummonths', 'moodle', 6),0=>new lang_string('never'));
-    $optionalsubsystems->add(new admin_setting_configselect('messagingdeletereadnotificationsdelay', new lang_string('messagingdeletereadnotificationsdelay', 'admin'), new lang_string('configmessagingdeletereadnotificationsdelay', 'admin'), 604800, $options));
+    $options = array(
+        DAYSECS => new lang_string('secondstotime86400'),
+        WEEKSECS => new lang_string('secondstotime604800'),
+        2620800 => new lang_string('nummonths', 'moodle', 1),
+        7862400 => new lang_string('nummonths', 'moodle', 3),
+        15724800 => new lang_string('nummonths', 'moodle', 6),
+        0 => new lang_string('never')
+    );
+    $optionalsubsystems->add(new admin_setting_configselect(
+        'messagingdeletereadnotificationsdelay',
+        new lang_string('messagingdeletereadnotificationsdelay', 'admin'),
+        new lang_string('configmessagingdeletereadnotificationsdelay', 'admin'),
+        604800,
+        $options)
+    );
+    $optionalsubsystems->add(new admin_setting_configselect(
+        'messagingdeleteallnotificationsdelay',
+        new lang_string('messagingdeleteallnotificationsdelay', 'admin'),
+        new lang_string('configmessagingdeleteallnotificationsdelay', 'admin'),
+        2620800,
+        $options)
+    );
 
     $optionalsubsystems->add(new admin_setting_configcheckbox('messagingallowemailoverride', new lang_string('messagingallowemailoverride', 'admin'), new lang_string('configmessagingallowemailoverride','admin'), 0));
 
diff --git a/admin/tasklogs.php b/admin/tasklogs.php
new file mode 100644 (file)
index 0000000..6684a7e
--- /dev/null
@@ -0,0 +1,92 @@
+<?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/>.
+
+/**
+ * Task log.
+ *
+ * @package    admin
+ * @copyright  2018 Andrew Nicols <andrew@nicols.co.uk>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+require_once(__DIR__ . '/../config.php');
+require_once("{$CFG->libdir}/adminlib.php");
+require_once("{$CFG->libdir}/tablelib.php");
+require_once("{$CFG->libdir}/filelib.php");
+
+$filter = optional_param('filter', '', PARAM_RAW);
+$result = optional_param('result', null, PARAM_INT);
+
+$pageurl = new \moodle_url('/admin/tasklogs.php');
+$pageurl->param('filter', $filter);
+
+$PAGE->set_url($pageurl);
+$PAGE->set_context(context_system::instance());
+$PAGE->set_pagelayout('admin');
+$strheading = get_string('tasklogs', 'tool_task');
+$PAGE->set_title($strheading);
+$PAGE->set_heading($strheading);
+
+require_login();
+
+require_capability('moodle/site:config', context_system::instance());
+admin_externalpage_setup('tasklogs');
+
+$logid = optional_param('logid', null, PARAM_INT);
+$download = optional_param('download', false, PARAM_BOOL);
+
+if (null !== $logid) {
+    $log = $DB->get_record('task_log', ['id' => $logid], '*', MUST_EXIST);
+
+    if ($download) {
+        $filename = str_replace('\\', '_', $log->classname) . "-{$log->id}.log";
+        header("Content-Disposition: attachment; filename=\"{$filename}\"");
+    }
+
+    readstring_accel($log->output, 'text/plain', false);
+    exit;
+}
+
+$renderer = $PAGE->get_renderer('tool_task');
+
+echo $OUTPUT->header();
+echo $OUTPUT->render_from_template('core_admin/tasklogs', (object) [
+    'action' => $pageurl->out(),
+    'filter' => $filter,
+    'resultfilteroptions' => [
+        (object) [
+            'value' => -1,
+            'title' => get_string('all'),
+            'selected' => (-1 === $result),
+        ],
+        (object) [
+            'value' => 0,
+            'title' => get_string('success'),
+            'selected' => (0 === $result),
+        ],
+        (object) [
+            'value' => 1,
+            'title' => get_string('task_result:failed', 'admin'),
+            'selected' => (1 === $result),
+        ],
+    ],
+]);
+
+$table = new \core_admin\task_log_table($filter, $result);
+$table->baseurl = $pageurl;
+$table->out(100, false);
+
+echo $OUTPUT->footer();
diff --git a/admin/templates/tasklogs.mustache b/admin/templates/tasklogs.mustache
new file mode 100644 (file)
index 0000000..c3180a2
--- /dev/null
@@ -0,0 +1,34 @@
+{{!
+    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 core_admin/tasklogs
+
+    Task Logs template.
+}}
+<form class="form-inline" method="GET" action="{{{action}}}">
+    <label class="sr-only" for="tasklog-filter">{{#str}}filter{{/str}}</label>
+    <input class="form-control" type="text" id="tasklog-filter" name="filter" value="{{{filter}}}">
+
+    <label class="sr-only" for="tasklog-resultfilter">{{#str}}resultfilter, admin{{/str}}</label>
+    <select class="form-control custom-select" name="result" id="tasklog-resultfilter">
+        {{#resultfilteroptions}}
+        <option value="{{{value}}}"{{#selected}} selected="selected"{{/selected}}>{{title}}</option>
+        {{/resultfilteroptions}}
+    </select>
+
+    <input class="btn btn-primary" type="submit" value="{{#str}}filter{{/str}}">
+</form>
index 5849311..53fa80a 100644 (file)
@@ -29,9 +29,6 @@ defined('MOODLE_INTERNAL') || die();
 function xmldb_tool_customlang_upgrade($oldversion) {
     global $CFG;
 
-    // Automatically generated Moodle v3.2.0 release upgrade line.
-    // Put any upgrade step following this.
-
     // Automatically generated Moodle v3.3.0 release upgrade line.
     // Put any upgrade step following this.
 
index 48f7d3c..1fba508 100644 (file)
Binary files a/admin/tool/dataprivacy/amd/build/effective_retention_period.min.js and b/admin/tool/dataprivacy/amd/build/effective_retention_period.min.js differ
index 8d587ca..7e3a102 100644 (file)
@@ -26,8 +26,7 @@ define(['jquery'],
 
         var SELECTORS = {
             PURPOSE_SELECT: '#id_purposeid',
-            RETENTION_FIELD_BOOST: '#id_error_retention_current',
-            RETENTION_FIELD_CLEAN: '#fitem_id_retention_current [data-fieldtype=static]',
+            RETENTION_FIELD: '#fitem_id_retention_current [data-fieldtype=static]',
         };
 
         /**
@@ -65,18 +64,7 @@ define(['jquery'],
             $(SELECTORS.PURPOSE_SELECT).on('change', function(ev) {
                 var selected = $(ev.currentTarget).val();
                 var selectedPurpose = this.purposeRetentionPeriods[selected];
-
-                var cleanSelector = $(SELECTORS.RETENTION_FIELD_CLEAN);
-                if (cleanSelector.length > 0) {
-                    cleanSelector.text(selectedPurpose);
-                } else {
-                    var boostSelector = $(SELECTORS.RETENTION_FIELD_BOOST);
-                    var retentionField = boostSelector.siblings();
-                    if (retentionField.length > 0) {
-                        retentionField.text(selectedPurpose);
-                    }
-                }
-
+                $(SELECTORS.RETENTION_FIELD).text(selectedPurpose);
             }.bind(this));
         };
 
index 90e7b2a..7672da3 100644 (file)
@@ -920,9 +920,17 @@ class expired_contexts_manager {
      * @return  bool
      */
     protected static function is_course_context_expired_or_unprotected_for_user(\context $context, \stdClass $user) {
-        $expiryrecords = self::get_nested_expiry_info_for_courses($context->path);
 
-        $info = $expiryrecords[$context->path]->info;
+        if ($context->get_course_context()->instanceid == SITEID) {
+            // The is an activity in the site course (front page).
+            $purpose = data_registry::get_effective_contextlevel_value(CONTEXT_SYSTEM, 'purpose');
+            $info = static::get_expiry_info($purpose);
+
+        } else {
+            $expiryrecords = self::get_nested_expiry_info_for_courses($context->path);
+            $info = $expiryrecords[$context->path]->info;
+        }
+
         if ($info->is_fully_expired()) {
             // This context is fully expired.
             return true;
index 3dd839e..20c28f9 100644 (file)
@@ -689,6 +689,7 @@ class external extends external_api {
      * @throws restricted_context_exception
      */
     public static function get_users($query) {
+        global $DB;
         $params = external_api::validate_parameters(self::get_users_parameters(), [
             'query' => $query
         ]);
@@ -703,15 +704,30 @@ class external extends external_api {
         // Exclude admins and guest user.
         $excludedusers = array_keys(get_admins()) + [guest_user()->id];
         $sort = 'lastname ASC, firstname ASC';
-        $fields = 'id, email, ' . $allusernames;
-        $users = get_users(true, $query, true, $excludedusers, $sort, '', '', 0, 30, $fields);
+        $fields = 'id,' . $allusernames;
+
+        $extrafields = get_extra_user_fields($context);
+        if (!empty($extrafields)) {
+            $fields .= ',' . implode(',', $extrafields);
+        }
+
+        list($sql, $params) = users_search_sql($query, '', false, $extrafields, $excludedusers);
+        $users = $DB->get_records_select('user', $sql, $params, $sort, $fields, 0, 30);
+
         $useroptions = [];
         foreach ($users as $user) {
-            $useroptions[$user->id] = (object)[
+            $useroption = (object)[
                 'id' => $user->id,
-                'fullname' => fullname($user),
-                'email' => $user->email
+                'fullname' => fullname($user)
             ];
+            $useroption->extrafields = [];
+            foreach ($extrafields as $extrafield) {
+                $useroption->extrafields[] = (object)[
+                    'name' => $extrafield,
+                    'value' => $user->$extrafield
+                ];
+            }
+            $useroptions[$user->id] = $useroption;
         }
 
         return $useroptions;
@@ -729,7 +745,13 @@ class external extends external_api {
             [
                 'id' => new external_value(core_user::get_property_type('id'), 'ID of the user'),
                 'fullname' => new external_value(core_user::get_property_type('firstname'), 'The fullname of the user'),
-                'email' => new external_value(core_user::get_property_type('email'), 'The user\'s email address', VALUE_OPTIONAL),
+                'extrafields' => new external_multiple_structure(
+                    new external_single_structure([
+                            'name' => new external_value(PARAM_TEXT, 'Name of the extrafield.'),
+                            'value' => new external_value(PARAM_TEXT, 'Value of the extrafield.')
+                        ]
+                    ), 'List of extra fields', VALUE_OPTIONAL
+                )
             ]
         ));
     }
index 7a7015a..e510ae0 100644 (file)
     overflow-y: scroll;
 }
 
-dd a.contactdpo {
-    /* Reverting dd's left margin */
-    margin-left: -10px;
-}
-
-.card dd a.contactdpo {
-    /* Reverting dd's left margin */
-    margin-left: inherit;
-}
-
 [data-region="data-requests-table"] .moodle-actionmenu {
     min-width: 150px;
 }
index 759650c..0f8443d 100644 (file)
     Example context (json):
     {
         "fullname": "Admin User",
-        "email": "admin@example.com"
+        "extrafields": [
+            {
+                "name": "email",
+                "value": "admin@example.com"
+            },
+            {
+                "name": "phone1",
+                "value": "0123456789"
+            }
+        ]
     }
 }}
 <span>
     <span>{{fullname}}</span>
-    <span><small>{{email}}</small></span>
+    {{#extrafields}}
+        <span><small>{{value}}</small></span>
+    {{/extrafields}}
 </span>
index 1947f62..78751cc 100644 (file)
@@ -6,21 +6,29 @@ Feature: Data export from the privacy API
 
   Background:
     Given the following "users" exist:
-      | username | firstname      | lastname |
-      | victim   | Victim User    | 1        |
-      | parent   | Long-suffering | Parent   |
+      | username  | firstname      | lastname  | institution |
+      | victim    | Victim User    | 1         | University1 |
+      | victim2   | Victim User    | 2         | University2 |
+      | requester | The            | Requester | University3 |
+      | parent    | Long-suffering | Parent    |             |
     And the following "roles" exist:
       | shortname | name  | archetype |
       | tired     | Tired |           |
     And the following "permission overrides" exist:
-      | capability                                   | permission | role  | contextlevel | reference |
-      | tool/dataprivacy:makedatarequestsforchildren | Allow      | tired | System       |           |
+      | capability                                   | permission | role    | contextlevel | reference |
+      | tool/dataprivacy:makedatarequestsforchildren | Allow      | tired   | System       |           |
+      | tool/dataprivacy:managedatarequests          | Allow      | manager | System       |           |
+      | moodle/site:viewuseridentity                 | Prevent    | manager | System       |           |
     And the following "role assigns" exist:
       | user   | role  | contextlevel | reference |
       | parent | tired | User         | victim    |
+    And the following "system role assigns" exist:
+      | user      | role    | contextlevel |
+      | requester | manager | User         |
     And the following config values are set as admin:
       | contactdataprotectionofficer | 1  | tool_dataprivacy |
       | privacyrequestexpiry         | 55 | tool_dataprivacy |
+      | dporoles                     | 1  | tool_dataprivacy |
     And the following data privacy "categories" exist:
       | name          |
       | Site category |
@@ -127,3 +135,19 @@ Feature: Data export from the privacy API
 
     And I should see "Expired" in the "Victim User 1" "table_row"
     And I should not see "Actions"
+
+  @javascript
+  Scenario: Test search for user using extra field.
+    Given the following "permission overrides" exist:
+      | capability                   | permission | role    | contextlevel | reference |
+      | moodle/site:viewuseridentity | Allow      | manager | System       |           |
+    And the following config values are set as admin:
+      | showuseridentity | institution |
+    And I log in as "requester"
+    And I navigate to "Users > Privacy and policies > Data requests" in site administration
+    And I follow "New request"
+    And I set the field "Search" to "University1"
+    Then I should see "Victim User 1"
+    When I reload the page
+    And I set the field "Search" to "University2"
+    Then I should see "Victim User 2"
index 08ffb80..6deb919 100644 (file)
@@ -2222,6 +2222,40 @@ class tool_dataprivacy_expired_contexts_testcase extends advanced_testcase {
         $this->assertTrue(expired_contexts_manager::is_context_expired_or_unprotected_for_user($blockcontext, $user));
     }
 
+    /**
+     * Test the is_context_expired functions when supplied with the front page course.
+     */
+    public function test_is_context_expired_frontpage() {
+        $this->resetAfterTest();
+
+        $purposes = $this->setup_basics('PT1H', 'PT1H', 'P1D');
+
+        $frontcourse = get_site();
+        $frontcoursecontext = \context_course::instance($frontcourse->id);
+
+        $sitenews = $this->getDataGenerator()->create_module('forum', ['course' => $frontcourse->id]);
+        $cm = get_coursemodule_from_instance('forum', $sitenews->id);
+        $sitenewscontext = \context_module::instance($cm->id);
+
+        $user = $this->getDataGenerator()->create_user(['lastaccess' => time() - YEARSECS]);
+
+        $this->assertFalse(expired_contexts_manager::is_context_expired($frontcoursecontext));
+        $this->assertFalse(expired_contexts_manager::is_context_expired($sitenewscontext));
+
+        $this->assertTrue(expired_contexts_manager::is_context_expired_or_unprotected_for_user($frontcoursecontext, $user));
+        $this->assertTrue(expired_contexts_manager::is_context_expired_or_unprotected_for_user($sitenewscontext, $user));
+
+        // Protecting the course contextlevel does not impact the front page.
+        $purposes->course->set('protected', 1)->save();
+        $this->assertTrue(expired_contexts_manager::is_context_expired_or_unprotected_for_user($frontcoursecontext, $user));
+        $this->assertTrue(expired_contexts_manager::is_context_expired_or_unprotected_for_user($sitenewscontext, $user));
+
+        // Protecting the system contextlevel affects the front page, too.
+        $purposes->system->set('protected', 1)->save();
+        $this->assertFalse(expired_contexts_manager::is_context_expired_or_unprotected_for_user($frontcoursecontext, $user));
+        $this->assertFalse(expired_contexts_manager::is_context_expired_or_unprotected_for_user($sitenewscontext, $user));
+    }
+
     /**
      * Test the is_context_expired functions when supplied with an expired course.
      */
index b17811a..7cb7df7 100644 (file)
@@ -970,4 +970,143 @@ class tool_dataprivacy_external_testcase extends externallib_advanced_testcase {
         $this->expectException(required_capability_exception::class);
         $result = external::bulk_deny_data_requests([$requestid1]);
     }
+
+    /**
+     * Test for external::get_users(), case search using non-identity field without
+     * facing any permission problem.
+     *
+     * @throws coding_exception
+     * @throws dml_exception
+     * @throws invalid_parameter_exception
+     * @throws required_capability_exception
+     * @throws restricted_context_exception
+     */
+    public function test_get_users_using_using_non_identity() {
+        $this->resetAfterTest();
+        $context = context_system::instance();
+        $requester = $this->getDataGenerator()->create_user();
+        $role = $this->getDataGenerator()->create_role();
+        role_assign($role, $requester->id, $context);
+        assign_capability('tool/dataprivacy:managedatarequests', CAP_ALLOW, $role, $context);
+        $this->setUser($requester);
+
+        $this->getDataGenerator()->create_user([
+            'firstname' => 'First Student'
+        ]);
+        $student2 = $this->getDataGenerator()->create_user([
+            'firstname' => 'Second Student'
+        ]);
+
+        $results = external::get_users('Second');
+        $this->assertCount(1, $results);
+        $this->assertEquals((object)[
+            'id' => $student2->id,
+            'fullname' => fullname($student2),
+            'extrafields' => []
+        ], $results[$student2->id]);
+    }
+
+    /**
+     * Test for external::get_users(), case search using identity field but
+     * don't have "moodle/site:viewuseridentity" permission.
+     *
+     * @throws coding_exception
+     * @throws dml_exception
+     * @throws invalid_parameter_exception
+     * @throws required_capability_exception
+     * @throws restricted_context_exception
+     */
+    public function test_get_users_using_identity_without_permission() {
+        global $CFG;
+
+        $this->resetAfterTest();
+        $CFG->showuseridentity = 'institution';
+
+        // Create requester user and assign correct capability.
+        $context = context_system::instance();
+        $requester = $this->getDataGenerator()->create_user();
+        $role = $this->getDataGenerator()->create_role();
+        role_assign($role, $requester->id, $context);
+        assign_capability('tool/dataprivacy:managedatarequests', CAP_ALLOW, $role, $context);
+        $this->setUser($requester);
+
+        $this->getDataGenerator()->create_user([
+            'institution' => 'University1'
+        ]);
+
+        $results = external::get_users('University1');
+        $this->assertEmpty($results);
+    }
+
+    /**
+     * Test for external::get_users(), case search using disabled identity field
+     * even they have "moodle/site:viewuseridentity" permission.
+     *
+     * @throws coding_exception
+     * @throws dml_exception
+     * @throws invalid_parameter_exception
+     * @throws required_capability_exception
+     * @throws restricted_context_exception
+     */
+    public function test_get_users_using_field_not_in_identity() {
+        $this->resetAfterTest();
+
+        $context = context_system::instance();
+        $requester = $this->getDataGenerator()->create_user();
+        $role = $this->getDataGenerator()->create_role();
+        role_assign($role, $requester->id, $context);
+        assign_capability('tool/dataprivacy:managedatarequests', CAP_ALLOW, $role, $context);
+        assign_capability('moodle/site:viewuseridentity', CAP_ALLOW, $role, $context);
+        $this->setUser($requester);
+
+        $this->getDataGenerator()->create_user([
+            'institution' => 'University1'
+        ]);
+
+        $results = external::get_users('University1');
+        $this->assertEmpty($results);
+    }
+
+    /**
+     * Test for external::get_users(), case search using enabled identity field
+     * with "moodle/site:viewuseridentity" permission.
+     *
+     * @throws coding_exception
+     * @throws dml_exception
+     * @throws invalid_parameter_exception
+     * @throws required_capability_exception
+     * @throws restricted_context_exception
+     */
+    public function test_get_users() {
+        global $CFG;
+        $this->resetAfterTest();
+        $CFG->showuseridentity = 'institution';
+        $context = context_system::instance();
+        $requester = $this->getDataGenerator()->create_user();
+        $role = $this->getDataGenerator()->create_role();
+        role_assign($role, $requester->id, $context);
+        assign_capability('tool/dataprivacy:managedatarequests', CAP_ALLOW, $role, $context);
+        assign_capability('moodle/site:viewuseridentity', CAP_ALLOW, $role, $context);
+        $this->setUser($requester);
+
+        $student1 = $this->getDataGenerator()->create_user([
+            'institution' => 'University1'
+        ]);
+        $this->getDataGenerator()->create_user([
+            'institution' => 'University2'
+        ]);
+
+        $results = external::get_users('University1');
+        $this->assertCount(1, $results);
+        $this->assertEquals((object)[
+            'id' => $student1->id,
+            'fullname' => fullname($student1),
+            'extrafields' => [
+                0 => (object)[
+                    'name' => 'institution',
+                    'value' => 'University1'
+                ]
+            ]
+        ], $results[$student1->id]);
+    }
 }
index a56cc8a..411f0ac 100644 (file)
@@ -24,6 +24,6 @@
 
 defined('MOODLE_INTERNAL') || die;
 
-$plugin->version   = 2018120300;
+$plugin->version   = 2019011500;
 $plugin->requires  = 2018112800;        // Moodle 3.5dev (Build 2018031600) and upwards.
 $plugin->component = 'tool_dataprivacy';
index 70a76cc..bbf1961 100644 (file)
@@ -33,9 +33,6 @@ defined('MOODLE_INTERNAL') || die();
 function xmldb_tool_log_upgrade($oldversion) {
     global $CFG;
 
-    // Automatically generated Moodle v3.2.0 release upgrade line.
-    // Put any upgrade step following this.
-
     // Automatically generated Moodle v3.3.0 release upgrade line.
     // Put any upgrade step following this.
 
index 0804ce8..04137ab 100644 (file)
@@ -27,9 +27,6 @@ defined('MOODLE_INTERNAL') || die();
 function xmldb_logstore_database_upgrade($oldversion) {
     global $CFG;
 
-    // Automatically generated Moodle v3.2.0 release upgrade line.
-    // Put any upgrade step following this.
-
     // Automatically generated Moodle v3.3.0 release upgrade line.
     // Put any upgrade step following this.
 
index 14946e8..d6cd26d 100644 (file)
@@ -27,9 +27,6 @@ defined('MOODLE_INTERNAL') || die();
 function xmldb_logstore_standard_upgrade($oldversion) {
     global $CFG;
 
-    // Automatically generated Moodle v3.2.0 release upgrade line.
-    // Put any upgrade step following this.
-
     // Automatically generated Moodle v3.3.0 release upgrade line.
     // Put any upgrade step following this.
 
index ee22b83..f8579c9 100644 (file)
@@ -33,26 +33,6 @@ defined('MOODLE_INTERNAL') || die();
 function xmldb_tool_monitor_upgrade($oldversion) {
     global $CFG, $DB;
 
-    $dbman = $DB->get_manager();
-
-    if ($oldversion < 2016052305) {
-
-        // Define field inactivedate to be added to tool_monitor_subscriptions.
-        $table = new xmldb_table('tool_monitor_subscriptions');
-        $field = new xmldb_field('inactivedate', XMLDB_TYPE_INTEGER, '10', null, true, null, 0, 'lastnotificationsent');
-
-        // Conditionally launch add field inactivedate.
-        if (!$dbman->field_exists($table, $field)) {
-            $dbman->add_field($table, $field);
-        }
-
-        // Monitor savepoint reached.
-        upgrade_plugin_savepoint(true, 2016052305, 'tool', 'monitor');
-    }
-
-    // Automatically generated Moodle v3.2.0 release upgrade line.
-    // Put any upgrade step following this.
-
     if ($oldversion < 2017021300) {
 
         // Delete "orphaned" subscriptions.
index 0de826c..cf03700 100644 (file)
@@ -129,6 +129,7 @@ if ($execute = $options['execute']) {
     $predbqueries = $DB->perf_get_queries();
     $pretime = microtime(true);
 
+    \core\task\logmanager::start_logging($task);
     $fullname = $task->get_name() . ' (' . get_class($task) . ')';
     mtrace('Execute scheduled task: ' . $fullname);
     // NOTE: it would be tricky to move this code to \core\task\manager class,
index b1172e0..a28b478 100644 (file)
@@ -34,7 +34,7 @@ $string['disabled'] = 'Disabled';
 $string['disabled_help'] = 'Disabled scheduled tasks are not executed from cron, however they can still be executed manually via the CLI tool.';
 $string['edittaskschedule'] = 'Edit task schedule: {$a}';
 $string['enablerunnow'] = 'Allow \'Run now\' for scheduled tasks';
-$string['enablerunnow_desc'] = 'Allows administrators to run a single scheduled task immediately, rather than waiting for it to run as scheduled. The task runs on the web server, so some sites may wish to disable this feature to avoid potential performance issues.';
+$string['enablerunnow_desc'] = 'Allows administrators to run a single scheduled task immediately, rather than waiting for it to run as scheduled. The feature requires \'Path to PHP CLI\' (pathtophp) to be set in System paths. The task runs on the web server, so you may wish to disable this feature to avoid potential performance issues.';
 $string['faildelay'] = 'Fail delay';
 $string['lastruntime'] = 'Last run';
 $string['nextruntime'] = 'Next run';
@@ -48,6 +48,7 @@ $string['runpattern'] = 'Run pattern';
 $string['scheduledtasks'] = 'Scheduled tasks';
 $string['scheduledtaskchangesdisabled'] = 'Modifications to the list of scheduled tasks have been prevented in Moodle configuration';
 $string['taskdisabled'] = 'Task disabled';
+$string['tasklogs'] = 'Task logs';
 $string['taskscheduleday'] = 'Day';
 $string['taskscheduleday_help'] = 'Day of month field for task schedule. The field uses the same format as unix cron. Some examples are:<br/><ul><li><strong>*</strong> Every day</li><li><strong>*/2</strong> Every 2nd day</li><li><strong>1</strong> The first of every month</li><li><strong>1,15</strong> The first and fifteenth of every month</li></ul>';
 $string['taskscheduledayofweek'] = 'Day of week';
@@ -59,4 +60,4 @@ $string['taskscheduleminute_help'] = 'Minute field for task schedule. The field
 $string['taskschedulemonth'] = 'Month';
 $string['taskschedulemonth_help'] = 'Month field for task schedule. The field uses the same format as unix cron. Some examples are:<br/><ul><li><strong>*</strong> Every month</li><li><strong>*/2</strong> Every second month</li><li><strong>1</strong> Every January</li><li><strong>1,5</strong> Every January and May</li></ul>';
 $string['privacy:metadata'] = 'The Scheduled task configuration plugin does not store any personal data.';
-
+$string['viewlogs'] = 'View logs for {$a}';
index 88dbdd4..3afd20c 100644 (file)
@@ -41,20 +41,33 @@ class tool_task_renderer extends plugin_renderer_base {
     public function scheduled_tasks_table($tasks) {
         global $CFG;
 
+        $showloglink = \core\task\logmanager::has_log_report();
+
         $table = new html_table();
-        $table->head  = array(get_string('name'),
-                              get_string('component', 'tool_task'),
-                              get_string('edit'),
-                              get_string('lastruntime', 'tool_task'),
-                              get_string('nextruntime', 'tool_task'),
-                              get_string('taskscheduleminute', 'tool_task'),
-                              get_string('taskschedulehour', 'tool_task'),
-                              get_string('taskscheduleday', 'tool_task'),
-                              get_string('taskscheduledayofweek', 'tool_task'),
-                              get_string('taskschedulemonth', 'tool_task'),
-                              get_string('faildelay', 'tool_task'),
-                              get_string('default', 'tool_task'));
+        $table->head = [
+            get_string('name'),
+            get_string('component', 'tool_task'),
+            get_string('edit'),
+            get_string('logs'),
+            get_string('lastruntime', 'tool_task'),
+            get_string('nextruntime', 'tool_task'),
+            get_string('taskscheduleminute', 'tool_task'),
+            get_string('taskschedulehour', 'tool_task'),
+            get_string('taskscheduleday', 'tool_task'),
+            get_string('taskscheduledayofweek', 'tool_task'),
+            get_string('taskschedulemonth', 'tool_task'),
+            get_string('faildelay', 'tool_task'),
+            get_string('default', 'tool_task'),
+        ];
+
         $table->attributes['class'] = 'admintable generaltable';
+        $table->colclasses = [];
+
+        if (!$showloglink) {
+            // Hide the log links.
+            $table->colclasses['3'] = 'hidden';
+        }
+
         $data = array();
         $yes = get_string('yes');
         $no = get_string('no');
@@ -72,6 +85,14 @@ class tool_task_renderer extends plugin_renderer_base {
                 $editlink = $this->render(new pix_icon('t/locked', get_string('scheduledtaskchangesdisabled', 'tool_task')));
             }
 
+            $loglink = '';
+            if ($showloglink) {
+                $loglink = $this->action_icon(
+                    \core\task\logmanager::get_url_for_task_class(get_class($task)),
+                    new pix_icon('e/file-text', get_string('viewlogs', 'tool_task', $task->get_name())
+                ));
+            }
+
             $namecell = new html_table_cell($task->get_name() . "\n" . html_writer::tag('span', '\\'.get_class($task),
                 array('class' => 'task-class text-ltr')));
             $namecell->header = true;
@@ -125,6 +146,7 @@ class tool_task_renderer extends plugin_renderer_base {
                         $namecell,
                         $componentcell,
                         new html_table_cell($editlink),
+                        new html_table_cell($loglink),
                         new html_table_cell($lastrun . $runnow),
                         new html_table_cell($nextrun),
                         new html_table_cell($task->get_minute()),
@@ -136,11 +158,11 @@ class tool_task_renderer extends plugin_renderer_base {
                         new html_table_cell($customised)));
 
             // Cron-style values must always be LTR.
-            $row->cells[5]->attributes['class'] = 'text-ltr';
             $row->cells[6]->attributes['class'] = 'text-ltr';
             $row->cells[7]->attributes['class'] = 'text-ltr';
             $row->cells[8]->attributes['class'] = 'text-ltr';
             $row->cells[9]->attributes['class'] = 'text-ltr';
+            $row->cells[10]->attributes['class'] = 'text-ltr';
 
             if ($disabled) {
                 $row->attributes['class'] = 'disabled';
index ac75859..ac9858e 100644 (file)
 defined('MOODLE_INTERNAL') || die;
 
 if ($hassiteconfig) {
-    $ADMIN->add('server', new admin_externalpage('scheduledtasks', new lang_string('scheduledtasks','tool_task'), "$CFG->wwwroot/$CFG->admin/tool/task/scheduledtasks.php"));
+    $ADMIN->add(
+        'taskconfig',
+        new admin_externalpage(
+            'scheduledtasks',
+            new lang_string('scheduledtasks', 'tool_task'),
+            "$CFG->wwwroot/$CFG->admin/tool/task/scheduledtasks.php"
+        )
+    );
 }
index 9bdf9ee..a8582de 100644 (file)
@@ -35,9 +35,6 @@ use tool_usertours\manager;
 function xmldb_tool_usertours_upgrade($oldversion) {
     global $CFG, $DB;
 
-    // Automatically generated Moodle v3.2.0 release upgrade line.
-    // Put any upgrade step following this.
-
     // Automatically generated Moodle v3.3.0 release upgrade line.
     // Put any upgrade step following this.
 
index 9dd0838..d531a86 100644 (file)
@@ -215,8 +215,8 @@ $string['yesmissingindexesfound'] = '<p>Some missing indexes have been found in
 <p>After doing that, it\'s highly recommended to execute this utility again to check that no more missing indexes are found.</p>';
 $string['yeswrongdefaultsfound'] = '<p>Some inconsistent defaults have been found in your DB. Here are their details and the needed SQL statements to be executed with your favourite SQL interface to fix them all. Remember to backup your data first!</p>
 <p>After doing that, it\'s highly recommended to execute this utility again to check that no more inconsistent defaults are found.</p>';
-$string['yeswrongintsfound'] = '<p>Some wrong integers have been found in your DB. Here are their details and the needed SQL statements to be executed with your favourite SQL interface to create all them. Remember to backup your data first!</p>
-<p>After doing that, it\'s highly recommended to execute this utility again to check that no more wrong integers are found.</p>';
+$string['yeswrongintsfound'] = '<p>Some wrong integers have been found in your DB. Here are their details and the needed SQL statements to be executed with your favourite SQL interface to fix them. Remember to backup your data first!</p>
+<p>After fixing them, it is highly recommended to execute this utility again to check that no more wrong integers are found.</p>';
 $string['yeswrongoraclesemanticsfound'] = '<p>Some Oracle columns using BYTE semantics have been found in your DB. Here are their details and the needed SQL statements to be executed with your favourite SQL interface to create all them. Remember to backup your data first!</p>
 <p>After doing that, it\'s highly recommended to execute this utility again to check that no more wrong semantics are found.</p>';
 $string['privacy:metadata'] = 'The XMLDB editor plugin does not store any personal data.';
index 5cd88b9..4a0d576 100644 (file)
@@ -169,10 +169,7 @@ abstract class base extends \core_analytics\calculable {
 
             if (!is_null($calculatedvalue)) {
                 $notnulls[$sampleid] = $sampleid;
-                if ($calculatedvalue > self::MAX_VALUE || $calculatedvalue < self::MIN_VALUE) {
-                    throw new \coding_exception('Calculated values should be higher than ' . self::MIN_VALUE .
-                        ' and lower than ' . self::MAX_VALUE . ' ' . $calculatedvalue . ' received');
-                }
+                $this->validate_calculated_value($calculatedvalue);
             }
 
             $calculations[$sampleid] = $calculatedvalue;
@@ -182,4 +179,19 @@ abstract class base extends \core_analytics\calculable {
 
         return array($features, $newcalculations, $notnulls);
     }
+
+    /**
+     * Validates the calculated value.
+     *
+     * @throws \coding_exception
+     * @param float $calculatedvalue
+     * @return true
+     */
+    protected function validate_calculated_value($calculatedvalue) {
+        if ($calculatedvalue > self::MAX_VALUE || $calculatedvalue < self::MIN_VALUE) {
+            throw new \coding_exception('Calculated values should be higher than ' . self::MIN_VALUE .
+                ' and lower than ' . self::MAX_VALUE . ' ' . $calculatedvalue . ' received');
+        }
+        return true;
+    }
 }
index a730996..2e4d4a9 100644 (file)
@@ -41,9 +41,7 @@ abstract class binary extends discrete {
      * @return array
      */
     public static final function get_classes() {
-        // It does not really matter, all \core_analytics\local\indicator\discrete get_classes calls have been overwriten as we
-        // only need 1 column here.
-        return array(0);
+        return [-1, 1];
     }
 
     /**
index 8a8aa66..b04b4c6 100644 (file)
@@ -52,8 +52,7 @@ abstract class discrete extends base {
     public static function get_feature_headers() {
         $fullclassname = '\\' . get_called_class();
 
-        $headers = array($fullclassname);
-        foreach (self::get_classes() as $class) {
+        foreach (static::get_classes() as $class) {
             $headers[] = $fullclassname . '/' . $class;
         }
 
@@ -116,26 +115,45 @@ abstract class discrete extends base {
      */
     protected function to_features($calculatedvalues) {
 
-        $classes = self::get_classes();
+        $classes = static::get_classes();
 
         foreach ($calculatedvalues as $sampleid => $calculatedvalue) {
 
-            $classindex = array_search($calculatedvalue, $classes, true);
+            // Using intval as it may come as a float from the db.
+            $classindex = array_search(intval($calculatedvalue), $classes, true);
 
-            if (!$classindex) {
-                throw new \coding_exception(get_class($this) . ' calculated "' . $calculatedvalue .
-                    '" which is not one of its defined classes (' . json_encode($classes) . ')');
+            if ($classindex === false && !is_null($calculatedvalue)) {
+                throw new \coding_exception(get_class($this) . ' calculated value "' . $calculatedvalue .
+                    '" is not one of its defined classes (' . json_encode($classes) . ')');
             }
 
             // We transform the calculated value into multiple features, one for each of the possible classes.
             $features = array_fill(0, count($classes), 0);
 
             // 1 to the selected value.
-            $features[$classindex] = 1;
+            if (!is_null($calculatedvalue)) {
+                $features[$classindex] = 1;
+            }
 
             $calculatedvalues[$sampleid] = $features;
         }
 
         return $calculatedvalues;
     }
+
+    /**
+     * Validates the calculated value.
+     *
+     * @param float $calculatedvalue
+     * @return true
+     */
+    protected function validate_calculated_value($calculatedvalue) {
+
+        // Using intval as it may come as a float from the db.
+        if (!in_array(intval($calculatedvalue), static::get_classes())) {
+            throw new \coding_exception(get_class($this) . ' calculated value "' . $calculatedvalue .
+                '" is not one of its defined classes (' . json_encode(static::get_classes()) . ')');
+        }
+        return true;
+    }
 }
diff --git a/analytics/tests/fixtures/test_indicator_discrete.php b/analytics/tests/fixtures/test_indicator_discrete.php
new file mode 100644 (file)
index 0000000..983b655
--- /dev/null
@@ -0,0 +1,90 @@
+<?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/>.
+
+/**
+ * Test indicator.
+ *
+ * @package   core_analytics
+ * @copyright 2019 David Monllao {@link http://www.davidmonllao.com}
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * Test indicator.
+ *
+ * @package   core_analytics
+ * @copyright 2019 David Monllao {@link http://www.davidmonllao.com}
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class test_indicator_discrete extends \core_analytics\local\indicator\discrete {
+
+    /**
+     * Returns the name.
+     *
+     * If there is a corresponding '_help' string this will be shown as well.
+     *
+     * @return \lang_string
+     */
+    public static function get_name() : \lang_string {
+        // Using a string that exists and contains a corresponding '_help' string.
+        return new \lang_string('allowstealthmodules');
+    }
+
+    /**
+     * The different classes this discrete indicator provides.
+     * @return [type] [description]
+     */
+    protected static function get_classes() {
+        return [0, 1, 2, 3, 4];
+    }
+
+    /**
+     * Just for testing.
+     *
+     * @param  float $value
+     * @param  string $subtype
+     * @return string
+     */
+    public function get_calculation_outcome($value, $subtype = false) {
+        return self::OUTCOME_OK;
+    }
+
+    /**
+     * Custom indicator calculated value display as otherwise we would display meaningless numbers to users.
+     *
+     * @param  float  $value
+     * @param  string $subtype
+     * @return string
+     */
+    public function get_display_value($value, $subtype = false) {
+        return $value;
+    }
+
+    /**
+     * calculate_sample
+     *
+     * @param int $sampleid
+     * @param string $sampleorigin
+     * @param int $starttime
+     * @param int $endtime
+     * @return float
+     */
+    protected function calculate_sample($sampleid, $sampleorigin, $starttime = false, $endtime = false) {
+        return 4;
+    }
+}
index 8b934dd..19106a6 100644 (file)
@@ -31,7 +31,7 @@ defined('MOODLE_INTERNAL') || die();
  * @copyright 2017 David Monllaó {@link http://www.davidmonllao.com}
  * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  */
-class test_indicator_random extends \core_analytics\local\indicator\binary {
+class test_indicator_random extends \core_analytics\local\indicator\linear {
 
     /**
      * Returns a lang_string object representing the name for the indicator.
diff --git a/analytics/tests/indicator_test.php b/analytics/tests/indicator_test.php
new file mode 100644 (file)
index 0000000..add3f3d
--- /dev/null
@@ -0,0 +1,99 @@
+<?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/>.
+
+/**
+ * Unit tests for the indicator API.
+ *
+ * @package   core_analytics
+ * @copyright 2019 David Monllaó {@link http://www.davidmonllao.com}
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die();
+
+require_once(__DIR__ . '/fixtures/test_indicator_max.php');
+require_once(__DIR__ . '/fixtures/test_indicator_discrete.php');
+require_once(__DIR__ . '/fixtures/test_indicator_min.php');
+
+/**
+ * Unit tests for the model.
+ *
+ * @package   core_analytics
+ * @copyright 2017 David Monllaó {@link http://www.davidmonllao.com}
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class analytics_indicator_testcase extends advanced_testcase {
+
+    /**
+     * test_validate_calculated_value
+     *
+     * @param string $indicatorclass
+     * @param array $returnedvalue
+     * @dataProvider validate_calculated_value
+     * @return null
+     */
+    public function test_validate_calculated_value($indicatorclass, $returnedvalue) {
+        $indicator = new $indicatorclass();
+        list($values, $unused) = $indicator->calculate([1], 'notrelevanthere');
+        $this->assertEquals($returnedvalue, $values[0]);
+    }
+
+    /**
+     * Data provider for test_validate_calculated_value
+     *
+     * @return array
+     */
+    public function validate_calculated_value() {
+        return [
+            'max' => ['test_indicator_max', [1]],
+            'min' => ['test_indicator_min', [-1]],
+            'discrete' => ['test_indicator_discrete', [0, 0, 0, 0, 1]],
+        ];
+    }
+
+    /**
+     * test_validate_calculated_value_exceptions
+     *
+     * @param string $indicatorclass
+     * @param string $willreturn
+     * @dataProvider validate_calculated_value_exceptions
+     * @expectedException \coding_exception
+     * @return null
+     */
+    public function test_validate_calculated_value_exceptions($indicatorclass, $willreturn) {
+
+        $indicator = new $indicatorclass();
+        $indicatormock = $this->getMockBuilder(get_class($indicator))
+            ->setMethods(['calculate_sample'])
+            ->getMock();
+        $indicatormock->method('calculate_sample')->willReturn($willreturn);
+        list($values, $unused) = $indicatormock->calculate([1], 'notrelevanthere');
+
+    }
+
+    /**
+     * Data provider for test_validate_calculated_value_exceptions
+     *
+     * @return array
+     */
+    public function validate_calculated_value_exceptions() {
+        return [
+            'max' => ['test_indicator_max', 2],
+            'min' => ['test_indicator_min', -2],
+            'discrete' => ['test_indicator_discrete', 7],
+        ];
+    }
+}
index 34d82f7..7d74868 100644 (file)
@@ -452,7 +452,7 @@ class core_analytics_prediction_testcase extends advanced_testcase {
         $indicator = $this->getMockBuilder('test_indicator_max')->setMethods(['calculate_sample'])->getMock();
         $indicator->expects($this->never())->method('calculate_sample');
 
-        $existingcalcs = array(111 => 1, 222 => 0.5);
+        $existingcalcs = array(111 => 1, 222 => -1);
         $sampleids = array(111 => 111, 222 => 222);
         list($values, $unused) = $indicator->calculate($sampleids, $sampleorigin, $starttime, $endtime, $existingcalcs);
     }
index 5eb8434..56e5a7d 100644 (file)
@@ -130,21 +130,10 @@ class auth_plugin_cas extends auth_plugin_ldap {
             }
 
             $authCAS = optional_param('authCAS', '', PARAM_RAW);
-            if ($authCAS == 'NOCAS') {
+            if ($authCAS != 'CAS') {
                 return;
             }
-            // Show authentication form for multi-authentication.
-            // Test pgtIou parameter for proxy mode (https connection in background from CAS server to the php server).
-            if ($authCAS != 'CAS' && !isset($_GET['pgtIou'])) {
-                $PAGE->set_url('/login/index.php');
-                $PAGE->navbar->add($CASform);
-                $PAGE->set_title("$site->fullname: $CASform");
-                $PAGE->set_heading($site->fullname);
-                echo $OUTPUT->header();
-                include($CFG->dirroot.'/auth/cas/cas_form.html');
-                echo $OUTPUT->footer();
-                exit();
-            }
+
         }
 
         // Connection to CAS server
@@ -363,4 +352,35 @@ class auth_plugin_cas extends auth_plugin_ldap {
             phpCAS::logoutWithRedirectService($backurl);
         }
     }
+
+    /**
+     * Return a list of identity providers to display on the login page.
+     *
+     * @param string|moodle_url $wantsurl The requested URL.
+     * @return array List of arrays with keys url, iconurl and name.
+     */
+    public function loginpage_idp_list($wantsurl) {
+        if (empty($this->config->hostname)) {
+            // CAS is not configured.
+            return [];
+        }
+
+        $iconurl = moodle_url::make_pluginfile_url(
+            context_system::instance()->id,
+            'auth_cas',
+            'logo',
+            null,
+            '/',
+            $this->config->auth_logo);
+
+        return [
+            [
+                'url' => new moodle_url(get_login_url(), [
+                        'authCAS' => 'CAS',
+                    ]),
+                'iconurl' => $iconurl,
+                'name' => format_string($this->config->auth_name),
+            ],
+        ];
+    }
 }
diff --git a/auth/cas/cas_form.html b/auth/cas/cas_form.html
deleted file mode 100644 (file)
index 52319a3..0000000
+++ /dev/null
@@ -1,11 +0,0 @@
-<div class="loginbox clearfix">
-<div class="loginpanel">
-<div>
-<a href="<?php echo get_login_url() . '?authCAS=CAS';?>"><?php print_string('accesCAS', 'auth_cas');?></a>
-</div>
-<br/>
-<div>
-<a href="<?php echo get_login_url() . '?authCAS=NOCAS';?>"><?php print_string('accesNOCAS', 'auth_cas');?></a>
-</div>
-</div>
-</div>
index 469661c..4df297c 100644 (file)
@@ -32,9 +32,6 @@ defined('MOODLE_INTERNAL') || die();
 function xmldb_auth_cas_upgrade($oldversion) {
     global $CFG;
 
-    // Automatically generated Moodle v3.2.0 release upgrade line.
-    // Put any upgrade step following this.
-
     if ($oldversion < 2017020700) {
         // Convert info in config plugins from auth/cas to auth_cas.
         upgrade_fix_config_auth_plugin_names('cas');
index a7c3662..3e465f9 100644 (file)
  * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  */
 
-$string['accesCAS'] = 'CAS users';
-$string['accesNOCAS'] = 'other users';
+$string['auth_cas_auth_name'] = 'Authentication method name';
+$string['auth_cas_auth_name_description'] = 'Provide a name for the CAS authentication method that is familiar to your users.';
+$string['auth_cas_auth_logo'] = 'Authentication method logo';
+$string['auth_cas_auth_logo_description'] = 'Provide a logo for the CAS authentication method that is familiar to your users.';
 $string['auth_cas_auth_user_create'] = 'Create users externally';
+$string['auth_cas_auth_service'] = 'CAS';
 $string['auth_cas_baseuri'] = 'URI of the server (nothing if no baseUri)<br />For example, if the CAS server responds to host.domaine.fr/CAS/ then<br />cas_baseuri = CAS/';
 $string['auth_cas_baseuri_key'] = 'Base URI';
 $string['auth_cas_broken_password'] = 'You cannot proceed without changing your password, however there is no available page for changing it. Please contact your Moodle Administrator.';
@@ -75,3 +78,7 @@ $string['noldapserver'] = 'No LDAP server configured for CAS! Syncing disabled.'
 $string['pluginname'] = 'CAS server (SSO)';
 $string['synctask'] = 'CAS users sync job';
 $string['privacy:metadata'] = 'The CAS server (SSO) authentication plugin does not store any personal data.';
+
+// Deprecated since Moodle 3.7.
+$string['accesCAS'] = 'CAS users';
+$string['accesNOCAS'] = 'other users';
diff --git a/auth/cas/lang/en/deprecated.txt b/auth/cas/lang/en/deprecated.txt
new file mode 100644 (file)
index 0000000..6854e8e
--- /dev/null
@@ -0,0 +1,2 @@
+accesCAS,auth_cas
+accesNOCAS,auth_cas
diff --git a/auth/cas/lib.php b/auth/cas/lib.php
new file mode 100644 (file)
index 0000000..7127556
--- /dev/null
@@ -0,0 +1,67 @@
+<?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/>.
+
+/**
+ * Authentication Plugin: CAS Authentication.
+ *
+ * Authentication using CAS (Central Authentication Server).
+ *
+ * @package     auth_cas
+ * @copyright   2018 Fabrice Ménard <menard.fabrice@gmail.com>
+ * @license     http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die;
+
+/**
+ * Serves the logo file settings.
+ *
+ * @param   stdClass $course course object
+ * @param   stdClass $cm course module object
+ * @param   stdClass $context context object
+ * @param   string $filearea file area
+ * @param   array $args extra arguments
+ * @param   bool $forcedownload whether or not force download
+ * @param   array $options additional options affecting the file serving
+ * @return  bool false|void
+ */
+function auth_cas_pluginfile($course, $cm, $context, $filearea, $args, $forcedownload, array $options = []) {
+    if ($context->contextlevel != CONTEXT_SYSTEM) {
+        return false;
+    }
+
+    if ($filearea !== 'logo' ) {
+        return false;
+    }
+
+    // Extract the filename / filepath from the $args array.
+    $filename = array_pop($args);
+    if (!$args) {
+        $filepath = '/';
+    } else {
+        $filepath = '/' . implode('/', $args) . '/';
+    }
+
+    // Retrieve the file from the Files API.
+    $itemid = 0;
+    $fs = get_file_storage();
+    $file = $fs->get_file($context->id, 'auth_cas', $filearea, $itemid, $filepath, $filename);
+    if (!$file) {
+        return false; // The file does not exist.
+    }
+
+    send_stored_file($file, null, 0, $forcedownload, $options);
+}
index 2bd7434..5434984 100644 (file)
@@ -45,6 +45,20 @@ if ($ADMIN->fulltree) {
         $settings->add(new admin_setting_heading('auth_cas/casserversettings',
                 new lang_string('auth_cas_server_settings', 'auth_cas'), ''));
 
+        // Authentication method name.
+        $settings->add(new admin_setting_configtext('auth_cas/auth_name',
+                get_string('auth_cas_auth_name', 'auth_cas'),
+                get_string('auth_cas_auth_name_description', 'auth_cas'),
+                get_string('auth_cas_auth_service', 'auth_cas'),
+                PARAM_RAW_TRIMMED));
+
+        // Authentication method logo.
+        $opts = array('accepted_types' => array('.png', '.jpg', '.gif', '.webp', '.tiff', '.svg'));
+        $settings->add(new admin_setting_configstoredfile('auth_cas/auth_logo',
+                 get_string('auth_cas_auth_logo', 'auth_cas'),
+                 get_string('auth_cas_auth_logo_description', 'auth_cas'), 'logo', 0, $opts));
+
+
         // Hostname.
         $settings->add(new admin_setting_configtext('auth_cas/hostname',
                 get_string('auth_cas_hostname_key', 'auth_cas'),
index f735c62..7c47f6a 100644 (file)
@@ -26,7 +26,7 @@
 
 defined('MOODLE_INTERNAL') || die();
 
-$plugin->version   = 2018120300;        // The current plugin version (Date: YYYYMMDDXX)
+$plugin->version   = 2018121400;        // The current plugin version (Date: YYYYMMDDXX)
 $plugin->requires  = 2018112800;        // Requires this Moodle version
 $plugin->component = 'auth_cas';        // Full name of the plugin (used for diagnostics)
 
index 7955ffe..f860220 100644 (file)
@@ -32,9 +32,6 @@ defined('MOODLE_INTERNAL') || die();
 function xmldb_auth_db_upgrade($oldversion) {
     global $CFG, $DB;
 
-    // Automatically generated Moodle v3.2.0 release upgrade line.
-    // Put any upgrade step following this.
-
     if ($oldversion < 2017032800) {
         // Convert info in config plugins from auth/db to auth_db
         upgrade_fix_config_auth_plugin_names('db');
index f00705b..f636d6f 100644 (file)
@@ -32,9 +32,6 @@ defined('MOODLE_INTERNAL') || die();
 function xmldb_auth_email_upgrade($oldversion) {
     global $CFG, $DB;
 
-    // Automatically generated Moodle v3.2.0 release upgrade line.
-    // Put any upgrade step following this.
-
     if ($oldversion < 2017020700) {
         // Convert info in config plugins from auth/email to auth_email.
         upgrade_fix_config_auth_plugin_names('email');
index 0a4c344..6c57255 100644 (file)
@@ -32,9 +32,6 @@ defined('MOODLE_INTERNAL') || die();
 function xmldb_auth_ldap_upgrade($oldversion) {
     global $CFG;
 
-    // Automatically generated Moodle v3.2.0 release upgrade line.
-    // Put any upgrade step following this.
-
     if ($oldversion < 2017020700) {
         // Convert info in config plugins from auth/ldap to auth_ldap.
         upgrade_fix_config_auth_plugin_names('ldap');
index 6d92020..13f87ce 100644 (file)
@@ -48,16 +48,16 @@ $string['auth_ldap_expiration_warning_key'] = 'Expiry warning';
 $string['auth_ldap_expireattr_desc'] = 'Optional: Overrides the LDAP attribute that stores password expiry time.';
 $string['auth_ldap_expireattr_key'] = 'Expiry attribute';
 $string['auth_ldapextrafields'] = 'These fields are optional.  You can choose to pre-fill some Moodle user fields with information from the <b>LDAP fields</b> that you specify here. <p>If you leave these fields blank, then nothing will be transferred from LDAP and Moodle defaults will be used instead.</p><p>In either case, the user will be able to edit all of these fields after they log in.</p>';
-$string['auth_ldap_graceattr_desc'] = 'Optional: Overrides  gracelogin attribute';
+$string['auth_ldap_graceattr_desc'] = 'Optional: Overrides grace login attribute';
 $string['auth_ldap_gracelogin_key'] = 'Grace login attribute';
-$string['auth_ldap_gracelogins_desc'] = 'Enable LDAP gracelogin support. After password has expired user can login until gracelogin count is 0. Enabling this setting displays grace login message if password is expired.';
+$string['auth_ldap_gracelogins_desc'] = 'Enable LDAP grace login support. After password has expired, user can log in until grace login count is 0. Enabling this setting displays grace login message if password has expired.';
 $string['auth_ldap_gracelogins_key'] = 'Grace logins';
 $string['auth_ldap_groupecreators'] = 'List of groups or contexts whose members are allowed to create groups. Separate multiple groups with \';\'. Usually something like \'cn=teachers,ou=staff,o=myorg\'';
 $string['auth_ldap_groupecreators_key'] = 'Group creators';
 $string['auth_ldap_host_url'] = 'Specify LDAP host in URL-form like \'ldap://ldap.myorg.com/\' or \'ldaps://ldap.myorg.com/\'. Separate multiple servers with \';\' to get failover support.';
 $string['auth_ldap_host_url_key'] = 'Host URL';
 $string['auth_ldap_changepasswordurl_key'] = 'Password-change URL';
-$string['auth_ldap_ldap_encoding'] = 'Specify encoding used by LDAP server. Most probably utf-8, MS AD v2 uses default platform encoding such as cp1252, cp1250, etc.';
+$string['auth_ldap_ldap_encoding'] = 'Encoding used by the LDAP server, most likely utf-8. If LDAP v2 is selected, Active Directory uses its configured encoding, such as cp1252 or cp1250.';
 $string['auth_ldap_ldap_encoding_key'] = 'LDAP encoding';
 $string['auth_ldap_login_settings'] = 'Login settings';
 $string['auth_ldap_memberattribute'] = 'Optional: Overrides user member attribute, when users belongs to a group. Usually \'member\'';
@@ -121,7 +121,7 @@ $string['didntfindexpiretime'] = 'password_expire() didn\'t find expiration time
 $string['didntgetusersfromldap'] = "Did not get any users from LDAP -- error? -- exiting\n";
 $string['gotcountrecordsfromldap'] = "Got {\$a} records from LDAP\n";
 $string['ldapnotconfigured'] = 'The LDAP host url is currently not configured';
-$string['morethanoneuser'] = 'Strange! More than one user record found in ldap. Only using the first one.';
+$string['morethanoneuser'] = 'More than one user record found in LDAP. Using only the first one.';
 $string['needbcmath'] = 'You need the BCMath extension to use expired password checking with Active Directory.';
 $string['needmbstring'] = 'You need the mbstring extension to change passwords in Active Directory';
 $string['nodnforusername'] = 'Error in user_update_password(). No DN for: {$a->username}';
@@ -152,7 +152,7 @@ $string['updateremfailamb'] = 'Failed to update LDAP with ambiguous field {$a->k
 $string['updateremfailfield'] = 'Failed to update LDAP with non-existent field (\'{$a->ldapkey}\'). Key ({$a->key}) - old Moodle value: \'{$a->ouvalue}\' new value: \'{$a->nuvalue}\'';
 $string['updatepasserror'] = 'Error in user_update_password(). Error code: {$a->errno}; Error string: {$a->errstring}';
 $string['updatepasserrorexpire'] = 'Error in user_update_password() when reading password expiry time. Error code: {$a->errno}; Error string: {$a->errstring}';
-$string['updatepasserrorexpiregrace'] = 'Error in user_update_password() when modifying expirationtime and/or gracelogins. Error code: {$a->errno}; Error string: {$a->errstring}';
+$string['updatepasserrorexpiregrace'] = 'Error in user_update_password() when modifying expiry time and/or grace logins. Error code: {$a->errno}; Error string: {$a->errstring}';
 $string['updateusernotfound'] = 'Could not find user while updating externally. Details follow: search base: \'{$a->userdn}\'; search filter: \'(objectClass=*)\'; search attributes: {$a->attribs}';
 $string['user_activatenotsupportusertype'] = 'auth: ldap user_activate() does not support selected usertype: {$a}';
 $string['user_disablenotsupportusertype'] = 'auth: ldap user_disable() does not support selected usertype: {$a}';
index d691552..9209e9d 100644 (file)
@@ -32,9 +32,6 @@ defined('MOODLE_INTERNAL') || die();
 function xmldb_auth_manual_upgrade($oldversion) {
     global $CFG;
 
-    // Automatically generated Moodle v3.2.0 release upgrade line.
-    // Put any upgrade step following this.
-
     if ($oldversion < 2017020700) {
         // Convert info in config plugins from auth/manual to auth_manual.
         upgrade_fix_config_auth_plugin_names('manual');
index f2f536d..ccb32cf 100644 (file)
@@ -161,18 +161,10 @@ class provider implements
             return;
         }
 
-        $params = [
-            'contextuser' => CONTEXT_USER,
-            'contextid' => $context->id
-        ];
-
-        $sql = "SELECT ctx.instanceid as userid
-                  FROM {mnet_log} ml
-                  JOIN {context} ctx
-                       ON ctx.instanceid = ml.userid
-                       AND ctx.contextlevel = :contextuser
-                 WHERE ctx.id = :contextid";
-
+        $sql = "SELECT userid
+                  FROM {mnet_log}
+                 WHERE userid = ?";
+        $params = [$context->instanceid];
         $userlist->add_from_sql('userid', $sql, $params);
     }
 
index 7d4d88d..be29bcd 100644 (file)
@@ -32,8 +32,6 @@ defined('MOODLE_INTERNAL') || die();
 function xmldb_auth_mnet_upgrade($oldversion) {
     global $CFG;
 
-    // Automatically generated Moodle v3.2.0 release upgrade line.
-    // Put any upgrade step following this.
     if ($oldversion < 2017020700) {
         // Convert info in config plugins from auth/mnet to auth_mnet.
         upgrade_fix_config_auth_plugin_names('mnet');
index 06f36e7..a8c7ed3 100644 (file)
@@ -91,11 +91,11 @@ $string['privacy:metadata:mnet_log:remoteid'] = 'Remote ID of the user who carri
 $string['privacy:metadata:mnet_log:time'] = 'Time when the action occurred.';
 $string['privacy:metadata:mnet_log:url'] = 'Remote system URL where the action occurred.';
 $string['privacy:metadata:mnet_log:userid'] = 'Local ID of the user who carried out the action in the remote system.';
-$string['privacy:metadata:mnet_session'] = 'The details of each MNet user session in a remote system is stored temporarily.';
+$string['privacy:metadata:mnet_session'] = 'The details of each MNet user session in a remote system. The data is stored temporarily.';
 $string['privacy:metadata:mnet_session:expires'] = 'Time when the session expires.';
 $string['privacy:metadata:mnet_session:mnethostid'] = 'Remote system MNet ID.';
 $string['privacy:metadata:mnet_session:token'] = 'Unique session identifier';
-$string['privacy:metadata:mnet_session:useragent'] = 'String denoting the user agent being which is accessing the page.';
+$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
index 596740b..2d5fc97 100644 (file)
@@ -32,9 +32,6 @@ defined('MOODLE_INTERNAL') || die();
 function xmldb_auth_none_upgrade($oldversion) {
     global $CFG, $DB;
 
-    // Automatically generated Moodle v3.2.0 release upgrade line.
-    // Put any upgrade step following this.
-
     if ($oldversion < 2017020700) {
         // Convert info in config plugins from auth/none to auth_none.
         upgrade_fix_config_auth_plugin_names('none');
index 67f1679..1ecfca8 100644 (file)
@@ -35,6 +35,7 @@ use core\oauth2\issuer;
 use core\oauth2\client;
 
 require_once($CFG->libdir.'/authlib.php');
+require_once($CFG->dirroot.'/user/lib.php');
 
 /**
  * Plugin for oauth2 authentication.
@@ -110,7 +111,7 @@ class auth extends \auth_plugin_base {
      * @return bool true means automatically copy data from ext to user table
      */
     public function is_synchronised_with_external() {
-        return false;
+        return true;
     }
 
     /**
@@ -300,6 +301,62 @@ class auth extends \auth_plugin_base {
         return true;
     }
 
+    /**
+     * Update user data according to data sent by authorization server.
+     *
+     * @param array $externaldata data from authorization server
+     * @param stdClass $userdata Current data of the user to be updated
+     * @return stdClass The updated user record, or the existing one if there's nothing to be updated.
+     */
+    private function update_user(array $externaldata, $userdata) {
+        $user = (object) [
+            'id' => $userdata->id,
+        ];
+
+        // We can only update if the default authentication type of the user is set to OAuth2 as well. Otherwise, we might mess
+        // up the user data of other users that use different authentication mechanisms (e.g. linked logins).
+        if ($userdata->auth !== $this->authtype) {
+            return $userdata;
+        }
+
+        // Go through each field from the external data.
+        foreach ($externaldata as $fieldname => $value) {
+            if (!in_array($fieldname, $this->userfields)) {
+                // Skip if this field doesn't belong to the list of fields that can be synced with the OAuth2 issuer.
+                continue;
+            }
+
+            if (!property_exists($userdata, $fieldname)) {
+                // Just in case this field is on the list, but not part of the user data. This shouldn't happen though.
+                continue;
+            }
+
+            // Get the old value.
+            $oldvalue = (string)$userdata->$fieldname;
+
+            // Get the lock configuration of the field.
+            $lockvalue = $this->config->{'field_lock_' . $fieldname};
+
+            // We should update fields that meet the following criteria:
+            // - Lock value set to 'unlocked'; or 'unlockedifempty', given the current value is empty.
+            // - The value has changed.
+            if ($lockvalue === 'unlocked' || ($lockvalue === 'unlockedifempty' && empty($oldvalue))) {
+                $value = (string)$value;
+                if ($oldvalue !== $value) {
+                    $user->$fieldname = $value;
+                }
+            }
+        }
+        // Update the user data.
+        user_update_user($user, false);
+
+        // Save user profile data.
+        profile_save_data($user);
+
+        // Refresh user for $USER variable.
+        return get_complete_user_data('id', $user->id);
+    }
+
     /**
      * Confirm the new user as registered.
      *
@@ -417,7 +474,8 @@ class auth extends \auth_plugin_base {
                 $client->log_out();
                 redirect(new moodle_url('/login/index.php'));
             } else if ($mappeduser && $mappeduser->confirmed) {
-                $userinfo = (array) $mappeduser;
+                // Update user fields.
+                $userinfo = $this->update_user($userinfo, $mappeduser);
                 $userwasmapped = true;
             } else {
                 // Trigger login failed event.
@@ -475,7 +533,7 @@ class auth extends \auth_plugin_base {
                     exit();
                 } else {
                     \auth_oauth2\api::link_login($userinfo, $issuer, $moodleuser->id, true);
-                    $userinfo = get_complete_user_data('id', $moodleuser->id);
+                    $userinfo = $this->update_user($userinfo, $moodleuser);
                     // No redirect, we will complete this login.
                 }
 
@@ -541,7 +599,6 @@ class auth extends \auth_plugin_base {
                     // Create a new confirmed account.
                     $newuser = \auth_oauth2\api::create_new_confirmed_account($userinfo, $issuer);
                     $userinfo = get_complete_user_data('id', $newuser->id);
-
                     // No redirect, we will complete this login.
                 }
             }
index d0dfa65..cd1750c 100644 (file)
@@ -99,18 +99,10 @@ class provider implements
             return;
         }
 
-        $params = [
-            'contextuser' => CONTEXT_USER,
-            'contextid' => $context->id
-        ];
-
-        $sql = "SELECT ctx.instanceid as userid
-                  FROM {auth_oauth2_linked_login} ao
-                  JOIN {context} ctx
-                       ON ctx.instanceid = ao.userid
-                       AND ctx.contextlevel = :contextuser
-                 WHERE ctx.id = :contextid";
-
+        $sql = "SELECT userid
+                  FROM {auth_oauth2_linked_login}
+                 WHERE userid = ?";
+        $params = [$context->instanceid];
         $userlist->add_from_sql('userid', $sql, $params);
     }
 
index 8d0a6a6..7582cc9 100644 (file)
@@ -35,9 +35,6 @@ function xmldb_auth_oauth2_upgrade($oldversion) {
 
     $dbman = $DB->get_manager();
 
-    // Automatically generated Moodle v3.2.0 release upgrade line.
-    // Put any upgrade step following this.
-
     // Automatically generated Moodle v3.3.0 release upgrade line.
     // Put any upgrade step following this.
 
index 39ca032..c52ad12 100644 (file)
@@ -308,9 +308,9 @@ class auth_plugin_shibboleth extends auth_plugin_base {
 
     /**
      * Sets the standard SAML domain cookie that is also used to preselect
-     * the right entry on the local wayf
+     * the right entry on the local way
      *
-     * @param IdP identifiere
+     * @param string $selectedIDP IDP identifier
      */
     function set_saml_cookie($selectedIDP) {
         if (isset($_COOKIE['_saml_idp']))
@@ -325,41 +325,12 @@ class auth_plugin_shibboleth extends auth_plugin_base {
         setcookie ('_saml_idp', generate_cookie_value($IDPArray), time() + (100*24*3600));
     }
 
-     /**
-     * Prints the option elements for the select element of the drop down list
-     *
-     */
-    function print_idp_list(){
-        $config = get_config('auth_shibboleth');
-
-        $IdPs = get_idp_list($config->organization_selection);
-        if (isset($_COOKIE['_saml_idp'])){
-            $idp_cookie = generate_cookie_array($_COOKIE['_saml_idp']);
-            do {
-                $selectedIdP = array_pop($idp_cookie);
-            } while (!isset($IdPs[$selectedIdP]) && count($idp_cookie) > 0);
-
-        } else {
-            $selectedIdP = '-';
-        }
-
-        foreach($IdPs as $IdP => $data){
-            if ($IdP == $selectedIdP){
-                echo '<option value="'.$IdP.'" selected="selected">'.$data[0].'</option>';
-            } else {
-                echo '<option value="'.$IdP.'">'.$data[0].'</option>';
-            }
-        }
-    }
-
-
-     /**
+    /**
      * Generate array of IdPs from Moodle Shibboleth settings
      *
      * @param string Text containing tuble/triple of IdP entityId, name and (optionally) session initiator
      * @return array Identifier of IdPs and their name/session initiator
      */
-
     function get_idp_list($organization_selection) {
         $idp_list = array();
 
index 416ac20..640a023 100644 (file)
@@ -32,9 +32,6 @@ defined('MOODLE_INTERNAL') || die();
 function xmldb_auth_shibboleth_upgrade($oldversion) {
     global $CFG, $DB;
 
-    // Automatically generated Moodle v3.2.0 release upgrade line.
-    // Put any upgrade step following this.
-
     if ($oldversion < 2017020700) {
         // Convert info in config plugins from auth/shibboleth to auth_shibboleth.
         upgrade_fix_config_auth_plugin_names('shibboleth');
diff --git a/auth/shibboleth/index_form.html b/auth/shibboleth/index_form.html
deleted file mode 100644 (file)
index 9f1e23e..0000000
+++ /dev/null
@@ -1,98 +0,0 @@
-<?php
-$config = get_config('auth_shibboleth');
-
-if ($show_instructions) {
-    $columns = 'twocolumns';
-} else {
-    $columns = 'onecolumn';
-}
-?>
-<div class="loginbox clearfix <?php echo $columns ?>">
-  <div class="loginpanel">
-    <!--<h2><?php print_string("returningtosite") ?></h2>-->
-
-    <h2><?php
-        if (isset($config->login_name) && !empty($config->login_name)){
-            echo $config->login_name;
-        } else {
-            print_string("auth_shibboleth_login_long", "auth_shibboleth");
-        }
-    ?></h2>
-      <div class="subcontent loginsub">
-        <div class="desc">
-        <?php
-          if (!empty($errormsg)) {
-              echo '<div class="loginerrors">';
-              echo $OUTPUT->error_text($errormsg);
-              echo '</div>';
-          }
-
-        ?>
-          <div class="guestsub">
-          <p><label for="idp"><?php print_string("auth_shibboleth_select_organization", "auth_shibboleth"); ?></label></p>
-            <form action="login.php" method="post" id="guestlogin">
-            <select id="idp" name="idp">
-                <option value="-" ><?php print_string("auth_shibboleth_select_member", "auth_shibboleth"); ?></option>
-                <?php
-                    print_idp_list();
-                ?>
-            </select><p><input type="submit" value="<?php print_string("select"); ?>" accesskey="s" /></p>
-            </form>
-            <p>
-            <?php
-                print_string("auth_shib_contact_administrator", "auth_shibboleth", get_admin()->email);
-            ?>
-            </p>
-          </div>
-         </div>
-      </div>
-
-<?php if ($CFG->guestloginbutton) {  ?>
-      <div class="subcontent guestsub">
-        <div class="desc">
-          <?php print_string("someallowguest") ?>
-        </div>
-        <form action="../../login/index.php" method="post" id="guestlogin">
-          <div class="guestform">
-            <input type="hidden" name="logintoken" value="<?php echo s(\core\session\manager::get_login_token()); ?>" />
-            <input type="hidden" name="username" value="guest" />
-            <input type="hidden" name="password" value="guest" />
-            <input type="submit" value="<?php print_string("loginguest") ?>" />
-          </div>
-        </form>
-      </div>
-<?php } ?>
-     </div>
-
-
-<?php if ($show_instructions) { ?>
-    <div class="signuppanel">
-      <h2><?php print_string("firsttime") ?></h2>
-      <div class="subcontent">
-<?php     if (is_enabled_auth('none')) { // instructions override the rest for security reasons
-              print_string("loginstepsnone");
-          } else if ($CFG->registerauth == 'email') {
-              if (!empty($config->auth_instructions)) {
-                  echo format_text($config->auth_instructions);
-              } else {
-                  print_string("loginsteps", "", "signup.php");
-              } ?>
-                 <div class="signupform">
-                   <form action="../../login/signup.php" method="get" id="signup">
-                   <div><input type="submit" value="<?php print_string("startsignup") ?>" /></div>
-                   </form>
-                 </div>
-<?php     } else if (!empty($CFG->registerauth)) {
-              echo format_text($config->auth_instructions); ?>
-              <div class="signupform">
-                <form action="../../login/signup.php" method="get" id="signup">
-                <div><input type="submit" value="<?php print_string("startsignup") ?>" /></div>
-                </form>
-              </div>
-<?php     } else {
-              echo format_text($config->auth_instructions);
-          } ?>
-      </div>
-    </div>
-<?php } ?>
-</div>
index 8ef9ec1..fd8b747 100644 (file)
@@ -41,7 +41,7 @@ $string['auth_shib_convert_data_warning'] = 'The file does not exist or is not r
 $string['auth_shib_changepasswordurl'] = 'Password-change URL';
 $string['auth_shib_idp_list'] = 'Identity providers';
 $string['auth_shib_idp_list_description'] = 'Provide a list of Identity Provider entityIDs to let the user choose from on the login page.<br />On each line there must be a comma-separated tuple for entityID of the IdP (see the Shibboleth metadata file) and Name of IdP as it shall be displayed in the drop-down list.<br />As an optional third parameter you can add the location of a Shibboleth session initiator that shall be used in case your Moodle installation is part of a multi federation setup.';
-$string['auth_shib_instructions'] = 'Use the <a href="{$a}">Shibboleth login</a> to get access via Shibboleth, if your institution supports it.<br />Otherwise, use the normal login form shown here.';
+$string['auth_shib_instructions'] = 'Use the <a href="{$a}">Shibboleth login</a> to get access via Shibboleth, if your institution supports it. Otherwise, use the normal login form shown here.';
 $string['auth_shib_instructions_help'] = 'Here you should provide custom instructions for your users to explain Shibboleth.  It will be shown on the login page in the instructions section. The instructions must include a link to "<b>{$a}</b>" that users click when they want to log in.';
 $string['auth_shib_instructions_key'] = 'Login instructions';
 $string['auth_shib_integrated_wayf'] = 'Moodle WAYF service';
index 6877fcb..d4fc639 100644 (file)
@@ -3,10 +3,9 @@
     require_once("../../config.php");
     require_once($CFG->dirroot."/auth/shibboleth/auth.php");
 
-    //initialize variables
-    $errormsg = '';
+    $idp = optional_param('idp', null, PARAM_RAW);
 
-/// Check for timed out sessions
+    // Check for timed out sessions.
     if (!empty($SESSION->has_timed_out)) {
         $session_has_timed_out = true;
         $SESSION->has_timed_out = false;
@@ -14,8 +13,8 @@
         $session_has_timed_out = false;
     }
 
-
-/// Define variables used in page
+    // Define variables used in page.
+    $isvalid = true;
     $site = get_site();
 
     $loginsite = get_string("loginsite");
 
     $config = get_config('auth_shibboleth');
     if (!empty($CFG->registerauth) or is_enabled_auth('none') or !empty($config->auth_instructions)) {
-        $show_instructions = true;
+        $showinstructions = true;
     } else {
-        $show_instructions = false;
+        $showinstructions = false;
     }
 
-    $IdPs = get_idp_list($config->organization_selection);
-    if (isset($_POST['idp']) && isset($IdPs[$_POST['idp']])){
-        $selectedIdP = $_POST['idp'];
-        set_saml_cookie($selectedIdP);
+    $idplist = get_idp_list($config->organization_selection);
+    if (isset($idp)) {
+        if (isset($idplist[$idp])) {
+            set_saml_cookie($idp);
 
-        // Redirect to SessionInitiator with entityID as argument
-        if (isset($IdPs[$selectedIdP][1]) && !empty($IdPs[$selectedIdP][1])) {
-            // For Shibbolet 1.x Service Providers
-            header('Location: '.$IdPs[$selectedIdP][1].'?providerId='. urlencode($selectedIdP) .'&target='. urlencode($CFG->wwwroot.'/auth/shibboleth/index.php'));
+            $targeturl = new moodle_url('/auth/shibboleth/index.php');
+            $idpinfo = $idplist[$idp];
 
-            // For Shibbolet 2.x Service Providers
-            // header('Location: '.$IdPs[$selectedIdP][1].'?entityID='. urlencode($selectedIdP) .'&target='. urlencode($CFG->wwwroot.'/auth/shibboleth/index.php'));
+            // Redirect to SessionInitiator with entityID as argument.
+            if (isset($idpinfo[1]) && !empty($idpinfo[1])) {
+                $sso = $idpinfo[1];
+            } else {
+                $sso = '/Shibboleth.sso';
+            }
+            // For Shibboleth 1.x Service Providers.
+            header('Location: ' . $sso . '?providerId=' . urlencode($idp) . '&target=' . urlencode($targeturl->out()));
 
         } else {
-            // For Shibbolet 1.x Service Providers
-            header('Location: /Shibboleth.sso?providerId='. urlencode($selectedIdP) .'&target='. urlencode($CFG->wwwroot.'/auth/shibboleth/index.php'));
-
-            // For Shibboleth 2.x Service Providers
-            // header('Location: /Shibboleth.sso/DS?entityID='. urlencode($selectedIdP) .'&target='. urlencode($CFG->wwwroot.'/auth/shibboleth/index.php'));
+            $isvalid = false;
         }
-    } elseif (isset($_POST['idp']) && !isset($IdPs[$_POST['idp']]))  {
-        $errormsg = get_string('auth_shibboleth_errormsg', 'auth_shibboleth');
     }
 
     $loginsite = get_string("loginsite");
@@ -60,6 +57,7 @@
     $PAGE->navbar->add($loginsite);
     $PAGE->set_title("$site->fullname: $loginsite");
     $PAGE->set_heading($site->fullname);
+    $PAGE->set_pagelayout('login');
 
     echo $OUTPUT->header();
 
         echo $OUTPUT->confirm(get_string('alreadyloggedin', 'error', fullname($USER)), $logout, $continue);
         echo $OUTPUT->box_end();
     } else {
-        include("index_form.html");
-    }
+        // Print login page.
+        $selectedidp = '-';
+        if (isset($_COOKIE['_saml_idp'])) {
+            $idpcookie = generate_cookie_array($_COOKIE['_saml_idp']);
+            do {
+                $selectedidp = array_pop($idpcookie);
+            } while (!isset($idplist[$selectedidp]) && count($idpcookie) > 0);
+        }
 
-    echo $OUTPUT->footer();
+        $idps = [];
+        foreach ($idplist as $value => $data) {
+            $name = reset($data);
+            $selected = $value === $selectedidp;
+            $idps[] = (object)[
+                'name' => $name,
+                'value' => $value,
+                'selected' => $selected
+            ];
+        }
 
+        // Whether the user can sign up.
+        $cansignup = !empty($CFG->registerauth);
+        // Default instructions.
+        $instructions = format_text($config->auth_instructions);
+        if (is_enabled_auth('none')) {
+            $instructions = get_string('loginstepsnone');
+        } else if ($cansignup) {
+            if ($CFG->registerauth === 'email' && empty($instructions)) {
+                $instructions = get_string('loginsteps');
+            }
+        }
 
+        // Build the template context data.
+        $templatedata = (object)[
+            'adminemail' => get_admin()->email,
+            'cansignup' => $cansignup,
+            'guestlogin' => $CFG->guestloginbutton,
+            'guestloginurl' => new moodle_url('/login/index.php'),
+            'idps' => $idps,
+            'instructions' => $instructions,
+            'loginname' => $config->login_name ?? null,
+            'logintoken' => \core\session\manager::get_login_token(),
+            'loginurl' => new moodle_url('/auth/shibboleth/login.php'),
+            'showinstructions' => $showinstructions,
+            'signupurl' => new moodle_url('/login/signup.php'),
+            'isvalid' => $isvalid
+        ];
+
+        // Render the login form.
+        echo $OUTPUT->render_from_template('auth_shibboleth/login_form', $templatedata);
+    }
+
+    echo $OUTPUT->footer();
diff --git a/auth/shibboleth/templates/login_form.mustache b/auth/shibboleth/templates/login_form.mustache
new file mode 100644 (file)
index 0000000..230f615
--- /dev/null
@@ -0,0 +1,129 @@
+{{!
+    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 auth_shibboleth/login_form
+
+    Template for the Shibboleth authentication plugin's login form.
+
+    Classes required for JS:
+    * none
+
+    Data attributes required for JS:
+    * none
+
+    Context variables required for this template:
+    * adminemail String The Administrator's email address.
+    * cansignup Boolean Whether a new user can sign up for an account.
+    * guestlogin Boolean Whether to show the guest login section.
+    * guestloginurl String The URL for guest login.
+    * idps Array The list of identity providers for the Shibboleth authentication plugin in value-name pairs per IDP.
+    * instructions String Signup instructions.
+    * isvalid Boolean Whether form validation passes.
+    * loginname String The custom login name.
+    * logintoken String The login token.
+    * loginurl String The login URL.
+    * showinstructions Boolean Whether to show additional login instructions.
+    * signupurl String The signup URL.
+
+    Example context (json):
+    {
+        "loginurl": "#",
+        "guestloginurl": "#",
+        "guestlogin": true,
+        "idps": [
+            { "value": 1, "name": "IDP 1" },
+            { "value": 2, "name": "IDP 2", "selected": true },
+            { "value": 3, "name": "IDP 3" }
+        ],
+        "showinstructions": true,
+        "logintoken": "abcde",
+        "adminemail": "admin@example.com",
+        "loginname": "Shib auth",
+        "cansignup": true,
+        "signupurl": "#",
+        "instructions": "Sign up here",
+        "isvalid": false
+    }
+}}
+
+<div class="my-1 my-sm-5"></div>
+<div class="container">
+    <div class="card">
+        <h2 class="card-header">
+            {{#loginname}}{{.}}{{/loginname}}
+            {{^loginname}}{{#str}}auth_shibboleth_login_long, auth_shibboleth{{/str}}{{/loginname}}
+        </h2>
+        <div class="card-body">
+            <div class="row justify-content-center m-l-1 m-r-1 m-b-1">
+                <div class="col-md-5">
+                    <form action="{{loginurl}}" method="post" id="login">
+                        <div class="form-group">
+                            <label for="idp">{{#str}}auth_shibboleth_select_organization, auth_shibboleth{{/str}}</label>
+                            <select id="idp" name="idp" class="form-control input-block-level {{^isvalid}}is-invalid{{/isvalid}}">
+                                <option value="-">{{#str}}auth_shibboleth_select_member, auth_shibboleth{{/str}}</option>
+                                {{#idps}}
+                                    <option value="{{value}}" {{#selected}}selected{{/selected}}>{{name}}</option>
+                                {{/idps}}
+                            </select>
+                            <div class="invalid-feedback text-danger m-b-1" {{#isvalid}}hidden{{/isvalid}}>
+                                {{#str}}auth_shibboleth_errormsg, auth_shibboleth{{/str}}
+                            </div>
+                        </div>
+                        <button type="submit" class="btn btn-primary btn-block m-b-1" accesskey="s">
+                            {{#str}}select, moodle{{/str}}
+                        </button>
+                        <p class="form-text text-muted m-t-1 m-b-1">
+                            {{#str}}auth_shib_contact_administrator, auth_shibboleth, {{adminemail}}{{/str}}
+                        </p>
+                    </form>
+                </div>
+                {{#guestlogin}}
+                <div class="col-md-5">
+                    <p>
+                        {{#str}}someallowguest, moodle{{/str}}
+                    </p>
+                    <form action="{{guestloginurl}}" method="post" id="guestlogin">
+                        <div class="guestform">
+                            <input type="hidden" name="logintoken" value="{{logintoken}}">
+                            <input type="hidden" name="username" value="guest">
+                            <input type="hidden" name="password" value="guest">
+                            <button type="submit" class="btn btn-secondary btn-block">
+                                {{#str}}loginguest, moodle{{/str}}
+                            </button>
+                        </div>
+                    </form>
+                </div>
+                {{/guestlogin}}
+            </div>
+        </div>
+    </div>
+    {{#showinstructions}}
+    <div class="card m-t-1">
+        <div class="card-body m-l-1 m-r-1 m-b-1">
+            <h2 class="card-title">{{#str}}firsttime, moodle{{/str}}</h2>
+            <p>
+                {{{instructions}}}