Merge branch 'install_master' of https://git.in.moodle.com/amosbot/moodle-install
authorDan Poltawski <dan@moodle.com>
Tue, 18 Apr 2017 11:34:49 +0000 (12:34 +0100)
committerDan Poltawski <dan@moodle.com>
Tue, 18 Apr 2017 11:34:49 +0000 (12:34 +0100)
565 files changed:
admin/tool/messageinbound/classes/manager.php
admin/tool/xmldb/lang/en/tool_xmldb.php
auth/oauth2/classes/auth.php
auth/oauth2/config.html [deleted file]
auth/oauth2/settings.php [new file with mode: 0644]
auth/shibboleth/logout.php
blocks/community/renderer.php
blocks/myoverview/classes/output/course_summary.php [deleted file]
blocks/myoverview/classes/output/courses_view.php
blocks/myoverview/classes/output/main.php
blocks/myoverview/lang/en/block_myoverview.php
blocks/myoverview/templates/courses-view-course-item.mustache
blocks/myoverview/templates/timeline-view-courses.mustache
calendar/renderer.php
config-dist.php
course/externallib.php
course/tests/externallib_test.php
enrol/yui/rolemanager/rolemanager.js
lang/en/mimetypes.php
lang/en/moodle.php
lib/amd/build/ajax.min.js
lib/amd/src/ajax.js
lib/classes/filetypes.php
lib/classes/output/icon_system_fontawesome.php
lib/ddl/sql_generator.php
lib/ddl/tests/ddl_test.php
lib/deprecatedlib.php
lib/filterlib.php
lib/moodlelib.php
lib/outputlib.php
lib/phpmailer/README_MOODLE.txt
lib/phpmailer/VERSION
lib/phpmailer/class.phpmailer.php
lib/phpmailer/class.smtp.php
lib/phpmailer/language/phpmailer.lang-de.php
lib/phpmailer/language/phpmailer.lang-es.php
lib/phpmailer/language/phpmailer.lang-ro.php
lib/searchlib.php
lib/templates/permissionmanager_role.mustache
lib/tests/moodlelib_test.php
lib/thirdpartylibs.xml
lib/weblib.php
mod/assign/feedback/editpdf/locallib.php
mod/assign/lib.php
mod/assign/submission/file/lang/en/assignsubmission_file.php
mod/assign/submission/file/locallib.php
mod/assign/submission/file/tests/behat/file_type_restriction.feature [new file with mode: 0644]
mod/assign/submission/file/tests/locallib_test.php
mod/assign/submission/file/version.php
mod/book/lib.php
mod/chat/lib.php
mod/choice/lib.php
mod/data/lib.php
mod/feedback/lib.php
mod/folder/lib.php
mod/forum/backup/moodle2/backup_forum_stepslib.php
mod/forum/backup/moodle2/restore_forum_stepslib.php
mod/forum/classes/output/big_search_form.php
mod/forum/classes/post_form.php
mod/forum/db/tag.php [new file with mode: 0644]
mod/forum/lang/en/forum.php
mod/forum/lib.php
mod/forum/locallib.php
mod/forum/post.php
mod/forum/search.php
mod/forum/templates/big_search_form.mustache
mod/forum/tests/behat/advanced_search.feature
mod/forum/tests/behat/edit_tags.feature [new file with mode: 0644]
mod/forum/tests/generator/lib.php
mod/forum/tests/generator_test.php
mod/forum/tests/lib_test.php
mod/forum/version.php
mod/glossary/lib.php
mod/imscp/lib.php
mod/label/lib.php
mod/lesson/lib.php
mod/lti/lib.php
mod/page/lib.php
mod/quiz/lib.php
mod/resource/lib.php
mod/resource/tests/lib_test.php
mod/scorm/lib.php
mod/survey/lib.php
mod/upgrade.txt
mod/url/lib.php
mod/wiki/lib.php
mod/workshop/lib.php
pix/f/FileTypesIcons-LICENSE.txt [new file with mode: 0644]
pix/f/Oxygen-LICENSE.txt [new file with mode: 0644]
pix/f/archive-128.png [new file with mode: 0644]
pix/f/archive-24.png [new file with mode: 0644]
pix/f/archive-256.png [new file with mode: 0644]
pix/f/archive-32.png [new file with mode: 0644]
pix/f/archive-48.png [new file with mode: 0644]
pix/f/archive-64.png [new file with mode: 0644]
pix/f/archive-72.png [new file with mode: 0644]
pix/f/archive-80.png [new file with mode: 0644]
pix/f/archive-96.png [new file with mode: 0644]
pix/f/archive.png [new file with mode: 0644]
pix/f/archive.svg [deleted file]
pix/f/audio-128.png [new file with mode: 0644]
pix/f/audio-24.png [new file with mode: 0644]
pix/f/audio-256.png [new file with mode: 0644]
pix/f/audio-32.png [new file with mode: 0644]
pix/f/audio-48.png [new file with mode: 0644]
pix/f/audio-64.png [new file with mode: 0644]
pix/f/audio-72.png [new file with mode: 0644]
pix/f/audio-80.png [new file with mode: 0644]
pix/f/audio-96.png [new file with mode: 0644]
pix/f/audio.png [new file with mode: 0644]
pix/f/audio.svg [deleted file]
pix/f/avi-128.png [new file with mode: 0644]
pix/f/avi-24.png [new file with mode: 0644]
pix/f/avi-256.png [new file with mode: 0644]
pix/f/avi-32.png [new file with mode: 0644]
pix/f/avi-48.png [new file with mode: 0644]
pix/f/avi-64.png [new file with mode: 0644]
pix/f/avi-72.png [new file with mode: 0644]
pix/f/avi-80.png [new file with mode: 0644]
pix/f/avi-96.png [new file with mode: 0644]
pix/f/avi.png [new file with mode: 0644]
pix/f/avi.svg [deleted file]
pix/f/base-128.png [new file with mode: 0644]
pix/f/base-24.png [new file with mode: 0644]
pix/f/base-32.png [new file with mode: 0644]
pix/f/base-48.png [new file with mode: 0644]
pix/f/base-64.png [new file with mode: 0644]
pix/f/base-72.png [new file with mode: 0644]
pix/f/base-80.png [new file with mode: 0644]
pix/f/base-96.png [new file with mode: 0644]
pix/f/base.png [new file with mode: 0644]
pix/f/base.svg [deleted file]
pix/f/bmp-128.png [new file with mode: 0644]
pix/f/bmp-24.png [new file with mode: 0644]
pix/f/bmp-256.png [new file with mode: 0644]
pix/f/bmp-32.png [new file with mode: 0644]
pix/f/bmp-48.png [new file with mode: 0644]
pix/f/bmp-64.png [new file with mode: 0644]
pix/f/bmp-72.png [new file with mode: 0644]
pix/f/bmp-80.png [new file with mode: 0644]
pix/f/bmp-96.png [new file with mode: 0644]
pix/f/bmp.png [new file with mode: 0644]
pix/f/bmp.svg [deleted file]
pix/f/calc-128.png [new file with mode: 0644]
pix/f/calc-24.png [new file with mode: 0644]
pix/f/calc-32.png [new file with mode: 0644]
pix/f/calc-48.png [new file with mode: 0644]
pix/f/calc-64.png [new file with mode: 0644]
pix/f/calc-72.png [new file with mode: 0644]
pix/f/calc-80.png [new file with mode: 0644]
pix/f/calc-96.png [new file with mode: 0644]
pix/f/calc.png [new file with mode: 0644]
pix/f/calc.svg [deleted file]
pix/f/chart-128.png [new file with mode: 0644]
pix/f/chart-24.png [new file with mode: 0644]
pix/f/chart-32.png [new file with mode: 0644]
pix/f/chart-48.png [new file with mode: 0644]
pix/f/chart-64.png [new file with mode: 0644]
pix/f/chart-72.png [new file with mode: 0644]
pix/f/chart-80.png [new file with mode: 0644]
pix/f/chart-96.png [new file with mode: 0644]
pix/f/chart.png [new file with mode: 0644]
pix/f/chart.svg [deleted file]
pix/f/database-128.png [new file with mode: 0644]
pix/f/database-24.png [new file with mode: 0644]
pix/f/database-256.png [new file with mode: 0644]
pix/f/database-32.png [new file with mode: 0644]
pix/f/database-48.png [new file with mode: 0644]
pix/f/database-64.png [new file with mode: 0644]
pix/f/database-72.png [new file with mode: 0644]
pix/f/database-80.png [new file with mode: 0644]
pix/f/database-96.png [new file with mode: 0644]
pix/f/database.png [new file with mode: 0644]
pix/f/database.svg [deleted file]
pix/f/dmg-32.png [new file with mode: 0644]
pix/f/dmg.gif [new file with mode: 0644]
pix/f/dmg.svg [deleted file]
pix/f/document-128.png [new file with mode: 0644]
pix/f/document-24.png [new file with mode: 0644]
pix/f/document-256.png [new file with mode: 0644]
pix/f/document-32.png [new file with mode: 0644]
pix/f/document-48.png [new file with mode: 0644]
pix/f/document-64.png [new file with mode: 0644]
pix/f/document-72.png [new file with mode: 0644]
pix/f/document-80.png [new file with mode: 0644]
pix/f/document-96.png [new file with mode: 0644]
pix/f/document.png [new file with mode: 0644]
pix/f/document.svg [deleted file]
pix/f/draw-128.png [new file with mode: 0644]
pix/f/draw-24.png [new file with mode: 0644]
pix/f/draw-32.png [new file with mode: 0644]
pix/f/draw-48.png [new file with mode: 0644]
pix/f/draw-64.png [new file with mode: 0644]
pix/f/draw-72.png [new file with mode: 0644]
pix/f/draw-80.png [new file with mode: 0644]
pix/f/draw-96.png [new file with mode: 0644]
pix/f/draw.png [new file with mode: 0644]
pix/f/draw.svg [deleted file]
pix/f/edit-32.png [new file with mode: 0644]
pix/f/edit.gif [new file with mode: 0644]
pix/f/env.gif [new file with mode: 0644]
pix/f/eps-128.png [new file with mode: 0644]
pix/f/eps-24.png [new file with mode: 0644]
pix/f/eps-256.png [new file with mode: 0644]
pix/f/eps-32.png [new file with mode: 0644]
pix/f/eps-48.png [new file with mode: 0644]
pix/f/eps-64.png [new file with mode: 0644]
pix/f/eps-72.png [new file with mode: 0644]
pix/f/eps-80.png [new file with mode: 0644]
pix/f/eps-96.png [new file with mode: 0644]
pix/f/eps.png [new file with mode: 0644]
pix/f/eps.svg [deleted file]
pix/f/epub-128.png [new file with mode: 0644]
pix/f/epub-24.png [new file with mode: 0644]
pix/f/epub-256.png [new file with mode: 0644]
pix/f/epub-32.png [new file with mode: 0644]
pix/f/epub-48.png [new file with mode: 0644]
pix/f/epub-64.png [new file with mode: 0644]
pix/f/epub-72.png [new file with mode: 0644]
pix/f/epub-80.png [new file with mode: 0644]
pix/f/epub-96.png [new file with mode: 0644]
pix/f/epub.png [new file with mode: 0644]
pix/f/epub.svg [deleted file]
pix/f/explore-32.png [new file with mode: 0644]
pix/f/explore.gif [new file with mode: 0644]
pix/f/flash-128.png [new file with mode: 0644]
pix/f/flash-24.png [new file with mode: 0644]
pix/f/flash-256.png [new file with mode: 0644]
pix/f/flash-32.png [new file with mode: 0644]
pix/f/flash-48.png [new file with mode: 0644]
pix/f/flash-64.png [new file with mode: 0644]
pix/f/flash-72.png [new file with mode: 0644]
pix/f/flash-80.png [new file with mode: 0644]
pix/f/flash-96.png [new file with mode: 0644]
pix/f/flash.png [new file with mode: 0644]
pix/f/flash.svg [deleted file]
pix/f/folder-128.png [new file with mode: 0644]
pix/f/folder-24.png [new file with mode: 0644]
pix/f/folder-32.png [new file with mode: 0644]
pix/f/folder-48.png [new file with mode: 0644]
pix/f/folder-64.png [new file with mode: 0644]
pix/f/folder-open-128.png [new file with mode: 0644]
pix/f/folder-open-24.png [new file with mode: 0644]
pix/f/folder-open-32.png [new file with mode: 0644]
pix/f/folder-open-48.png [new file with mode: 0644]
pix/f/folder-open-64.png [new file with mode: 0644]
pix/f/folder-open.png [new file with mode: 0644]
pix/f/folder-open.svg [deleted file]
pix/f/folder.png [new file with mode: 0644]
pix/f/folder.svg [deleted file]
pix/f/gif-128.png [new file with mode: 0644]
pix/f/gif-24.png [new file with mode: 0644]
pix/f/gif-256.png [new file with mode: 0644]
pix/f/gif-32.png [new file with mode: 0644]
pix/f/gif-48.png [new file with mode: 0644]
pix/f/gif-64.png [new file with mode: 0644]
pix/f/gif-72.png [new file with mode: 0644]
pix/f/gif-80.png [new file with mode: 0644]
pix/f/gif-96.png [new file with mode: 0644]
pix/f/gif.png [new file with mode: 0644]
pix/f/gif.svg [deleted file]
pix/f/help-32.png [new file with mode: 0644]
pix/f/help.gif [new file with mode: 0644]
pix/f/html-128.png [new file with mode: 0644]
pix/f/html-24.png [new file with mode: 0644]
pix/f/html-256.png [new file with mode: 0644]
pix/f/html-32.png [new file with mode: 0644]
pix/f/html-48.png [new file with mode: 0644]
pix/f/html-64.png [new file with mode: 0644]
pix/f/html-72.png [new file with mode: 0644]
pix/f/html-80.png [new file with mode: 0644]
pix/f/html-96.png [new file with mode: 0644]
pix/f/html.gif [new file with mode: 0644]
pix/f/html.svg [deleted file]
pix/f/image-128.png [new file with mode: 0644]
pix/f/image-24.png [new file with mode: 0644]
pix/f/image-256.png [new file with mode: 0644]
pix/f/image-32.png [new file with mode: 0644]
pix/f/image-48.png [new file with mode: 0644]
pix/f/image-64.png [new file with mode: 0644]
pix/f/image-72.png [new file with mode: 0644]
pix/f/image-80.png [new file with mode: 0644]
pix/f/image-96.png [new file with mode: 0644]
pix/f/image.png [new file with mode: 0644]
pix/f/image.svg [deleted file]
pix/f/impress-128.png [new file with mode: 0644]
pix/f/impress-24.png [new file with mode: 0644]
pix/f/impress-32.png [new file with mode: 0644]
pix/f/impress-48.png [new file with mode: 0644]
pix/f/impress-64.png [new file with mode: 0644]
pix/f/impress-72.png [new file with mode: 0644]
pix/f/impress-80.png [new file with mode: 0644]
pix/f/impress-96.png [new file with mode: 0644]
pix/f/impress.png [new file with mode: 0644]
pix/f/impress.svg [deleted file]
pix/f/isf-128.png [new file with mode: 0644]
pix/f/isf-24.png [new file with mode: 0644]
pix/f/isf-256.png [new file with mode: 0644]
pix/f/isf-32.png [new file with mode: 0644]
pix/f/isf-48.png [new file with mode: 0644]
pix/f/isf-64.png [new file with mode: 0644]
pix/f/isf-72.png [new file with mode: 0644]
pix/f/isf-80.png [new file with mode: 0644]
pix/f/isf-96.png [new file with mode: 0644]
pix/f/isf.png [new file with mode: 0644]
pix/f/isf.svg [deleted file]
pix/f/jpeg-128.png [new file with mode: 0644]
pix/f/jpeg-24.png [new file with mode: 0644]
pix/f/jpeg-256.png [new file with mode: 0644]
pix/f/jpeg-32.png [new file with mode: 0644]
pix/f/jpeg-48.png [new file with mode: 0644]
pix/f/jpeg-64.png [new file with mode: 0644]
pix/f/jpeg-72.png [new file with mode: 0644]
pix/f/jpeg-80.png [new file with mode: 0644]
pix/f/jpeg-96.png [new file with mode: 0644]
pix/f/jpeg.png [new file with mode: 0644]
pix/f/jpeg.svg [deleted file]
pix/f/markup-128.png [new file with mode: 0644]
pix/f/markup-24.png [new file with mode: 0644]
pix/f/markup-256.png [new file with mode: 0644]
pix/f/markup-32.png [new file with mode: 0644]
pix/f/markup-48.png [new file with mode: 0644]
pix/f/markup-64.png [new file with mode: 0644]
pix/f/markup-72.png [new file with mode: 0644]
pix/f/markup-80.png [new file with mode: 0644]
pix/f/markup-96.png [new file with mode: 0644]
pix/f/markup.png [new file with mode: 0644]
pix/f/markup.svg [deleted file]
pix/f/math-128.png [new file with mode: 0644]
pix/f/math-24.png [new file with mode: 0644]
pix/f/math-32.png [new file with mode: 0644]
pix/f/math-48.png [new file with mode: 0644]
pix/f/math-64.png [new file with mode: 0644]
pix/f/math-72.png [new file with mode: 0644]
pix/f/math-80.png [new file with mode: 0644]
pix/f/math-96.png [new file with mode: 0644]
pix/f/math.png [new file with mode: 0644]
pix/f/math.svg [deleted file]
pix/f/moodle-128.png [new file with mode: 0644]
pix/f/moodle-24.png [new file with mode: 0644]
pix/f/moodle-256.png [new file with mode: 0644]
pix/f/moodle-32.png [new file with mode: 0644]
pix/f/moodle-48.png [new file with mode: 0644]
pix/f/moodle-64.png [new file with mode: 0644]
pix/f/moodle-72.png [new file with mode: 0644]
pix/f/moodle-80.png [new file with mode: 0644]
pix/f/moodle-96.png [new file with mode: 0644]
pix/f/moodle.png [new file with mode: 0644]
pix/f/moodle.svg [deleted file]
pix/f/mov.png [new file with mode: 0644]
pix/f/mov.svg [deleted file]
pix/f/move.gif [new file with mode: 0644]
pix/f/mp3-128.png [new file with mode: 0644]
pix/f/mp3-24.png [new file with mode: 0644]
pix/f/mp3-256.png [new file with mode: 0644]
pix/f/mp3-32.png [new file with mode: 0644]
pix/f/mp3-48.png [new file with mode: 0644]
pix/f/mp3-64.png [new file with mode: 0644]
pix/f/mp3-72.png [new file with mode: 0644]
pix/f/mp3-80.png [new file with mode: 0644]
pix/f/mp3-96.png [new file with mode: 0644]
pix/f/mp3.png [new file with mode: 0644]
pix/f/mp3.svg [deleted file]
pix/f/mpeg-128.png [new file with mode: 0644]
pix/f/mpeg-24.png [new file with mode: 0644]
pix/f/mpeg-256.png [new file with mode: 0644]
pix/f/mpeg-32.png [new file with mode: 0644]
pix/f/mpeg-48.png [new file with mode: 0644]
pix/f/mpeg-64.png [new file with mode: 0644]
pix/f/mpeg-72.png [new file with mode: 0644]
pix/f/mpeg-80.png [new file with mode: 0644]
pix/f/mpeg-96.png [new file with mode: 0644]
pix/f/mpeg.png [new file with mode: 0644]
pix/f/mpeg.svg [deleted file]
pix/f/oth-128.png [new file with mode: 0644]
pix/f/oth-24.png [new file with mode: 0644]
pix/f/oth-32.png [new file with mode: 0644]
pix/f/oth-48.png [new file with mode: 0644]
pix/f/oth-64.png [new file with mode: 0644]
pix/f/oth-72.png [new file with mode: 0644]
pix/f/oth-80.png [new file with mode: 0644]
pix/f/oth-96.png [new file with mode: 0644]
pix/f/oth.png [new file with mode: 0644]
pix/f/oth.svg [deleted file]
pix/f/parent-32.png [new file with mode: 0644]
pix/f/parent.gif [new file with mode: 0644]
pix/f/pdf-128.png [new file with mode: 0644]
pix/f/pdf-24.png [new file with mode: 0644]
pix/f/pdf-256.png [new file with mode: 0644]
pix/f/pdf-32.png [new file with mode: 0644]
pix/f/pdf-48.png [new file with mode: 0644]
pix/f/pdf-64.png [new file with mode: 0644]
pix/f/pdf-72.png [new file with mode: 0644]
pix/f/pdf-80.png [new file with mode: 0644]
pix/f/pdf-96.png [new file with mode: 0644]
pix/f/pdf.png [new file with mode: 0644]
pix/f/pdf.svg [deleted file]
pix/f/png-128.png [new file with mode: 0644]
pix/f/png-24.png [new file with mode: 0644]
pix/f/png-256.png [new file with mode: 0644]
pix/f/png-32.png [new file with mode: 0644]
pix/f/png-48.png [new file with mode: 0644]
pix/f/png-64.png [new file with mode: 0644]
pix/f/png-72.png [new file with mode: 0644]
pix/f/png-80.png [new file with mode: 0644]
pix/f/png-96.png [new file with mode: 0644]
pix/f/png.png [new file with mode: 0644]
pix/f/png.svg [deleted file]
pix/f/powerpoint-128.png [new file with mode: 0644]
pix/f/powerpoint-24.png [new file with mode: 0644]
pix/f/powerpoint-256.png [new file with mode: 0644]
pix/f/powerpoint-32.png [new file with mode: 0644]
pix/f/powerpoint-48.png [new file with mode: 0644]
pix/f/powerpoint-64.png [new file with mode: 0644]
pix/f/powerpoint-72.png [new file with mode: 0644]
pix/f/powerpoint-80.png [new file with mode: 0644]
pix/f/powerpoint-96.png [new file with mode: 0644]
pix/f/powerpoint.png [new file with mode: 0644]
pix/f/powerpoint.svg [deleted file]
pix/f/psd-128.png [new file with mode: 0644]
pix/f/psd-24.png [new file with mode: 0644]
pix/f/psd-256.png [new file with mode: 0644]
pix/f/psd-32.png [new file with mode: 0644]
pix/f/psd-48.png [new file with mode: 0644]
pix/f/psd-64.png [new file with mode: 0644]
pix/f/psd-72.png [new file with mode: 0644]
pix/f/psd-80.png [new file with mode: 0644]
pix/f/psd-96.png [new file with mode: 0644]
pix/f/psd.png [new file with mode: 0644]
pix/f/psd.svg [deleted file]
pix/f/publisher-128.png [new file with mode: 0644]
pix/f/publisher-24.png [new file with mode: 0644]
pix/f/publisher-256.png [new file with mode: 0644]
pix/f/publisher-32.png [new file with mode: 0644]
pix/f/publisher-48.png [new file with mode: 0644]
pix/f/publisher-64.png [new file with mode: 0644]
pix/f/publisher-72.png [new file with mode: 0644]
pix/f/publisher-80.png [new file with mode: 0644]
pix/f/publisher-96.png [new file with mode: 0644]
pix/f/publisher.png [new file with mode: 0644]
pix/f/publisher.svg [deleted file]
pix/f/quicktime-128.png [new file with mode: 0644]
pix/f/quicktime-24.png [new file with mode: 0644]
pix/f/quicktime-256.png [new file with mode: 0644]
pix/f/quicktime-32.png [new file with mode: 0644]
pix/f/quicktime-48.png [new file with mode: 0644]
pix/f/quicktime-64.png [new file with mode: 0644]
pix/f/quicktime-72.png [new file with mode: 0644]
pix/f/quicktime-80.png [new file with mode: 0644]
pix/f/quicktime-96.png [new file with mode: 0644]
pix/f/quicktime.png [new file with mode: 0644]
pix/f/quicktime.svg [deleted file]
pix/f/sourcecode-128.png [new file with mode: 0644]
pix/f/sourcecode-24.png [new file with mode: 0644]
pix/f/sourcecode-256.png [new file with mode: 0644]
pix/f/sourcecode-32.png [new file with mode: 0644]
pix/f/sourcecode-48.png [new file with mode: 0644]
pix/f/sourcecode-64.png [new file with mode: 0644]
pix/f/sourcecode-72.png [new file with mode: 0644]
pix/f/sourcecode-80.png [new file with mode: 0644]
pix/f/sourcecode-96.png [new file with mode: 0644]
pix/f/sourcecode.png [new file with mode: 0644]
pix/f/sourcecode.svg [deleted file]
pix/f/spreadsheet-128.png [new file with mode: 0644]
pix/f/spreadsheet-24.png [new file with mode: 0644]
pix/f/spreadsheet-256.png [new file with mode: 0644]
pix/f/spreadsheet-32.png [new file with mode: 0644]
pix/f/spreadsheet-48.png [new file with mode: 0644]
pix/f/spreadsheet-64.png [new file with mode: 0644]
pix/f/spreadsheet-72.png [new file with mode: 0644]
pix/f/spreadsheet-80.png [new file with mode: 0644]
pix/f/spreadsheet-96.png [new file with mode: 0644]
pix/f/spreadsheet.png [new file with mode: 0644]
pix/f/spreadsheet.svg [deleted file]
pix/f/text-128.png [new file with mode: 0644]
pix/f/text-24.png [new file with mode: 0644]
pix/f/text-256.png [new file with mode: 0644]
pix/f/text-32.png [new file with mode: 0644]
pix/f/text-48.png [new file with mode: 0644]
pix/f/text-64.png [new file with mode: 0644]
pix/f/text-72.png [new file with mode: 0644]
pix/f/text-80.png [new file with mode: 0644]
pix/f/text-96.png [new file with mode: 0644]
pix/f/text.png [new file with mode: 0644]
pix/f/text.svg [deleted file]
pix/f/tiff-128.png [new file with mode: 0644]
pix/f/tiff-24.png [new file with mode: 0644]
pix/f/tiff-256.png [new file with mode: 0644]
pix/f/tiff-32.png [new file with mode: 0644]
pix/f/tiff-48.png [new file with mode: 0644]
pix/f/tiff-64.png [new file with mode: 0644]
pix/f/tiff-72.png [new file with mode: 0644]
pix/f/tiff-80.png [new file with mode: 0644]
pix/f/tiff-96.png [new file with mode: 0644]
pix/f/tiff.png [new file with mode: 0644]
pix/f/tiff.svg [deleted file]
pix/f/unknown-128.png [new file with mode: 0644]
pix/f/unknown-24.png [new file with mode: 0644]
pix/f/unknown-256.png [new file with mode: 0644]
pix/f/unknown-32.png [new file with mode: 0644]
pix/f/unknown-48.png [new file with mode: 0644]
pix/f/unknown-64.png [new file with mode: 0644]
pix/f/unknown-72.png [new file with mode: 0644]
pix/f/unknown-80.png [new file with mode: 0644]
pix/f/unknown-96.png [new file with mode: 0644]
pix/f/unknown.png [new file with mode: 0644]
pix/f/unknown.svg [deleted file]
pix/f/video-128.png [new file with mode: 0644]
pix/f/video-24.png [new file with mode: 0644]
pix/f/video-256.png [new file with mode: 0644]
pix/f/video-32.png [new file with mode: 0644]
pix/f/video-48.png [new file with mode: 0644]
pix/f/video-64.png [new file with mode: 0644]
pix/f/video-72.png [new file with mode: 0644]
pix/f/video-80.png [new file with mode: 0644]
pix/f/video-96.png [new file with mode: 0644]
pix/f/video.png [new file with mode: 0644]
pix/f/video.svg [deleted file]
pix/f/wav-128.png [new file with mode: 0644]
pix/f/wav-24.png [new file with mode: 0644]
pix/f/wav-256.png [new file with mode: 0644]
pix/f/wav-32.png [new file with mode: 0644]
pix/f/wav-48.png [new file with mode: 0644]
pix/f/wav-64.png [new file with mode: 0644]
pix/f/wav-72.png [new file with mode: 0644]
pix/f/wav-80.png [new file with mode: 0644]
pix/f/wav-96.png [new file with mode: 0644]
pix/f/wav.png [new file with mode: 0644]
pix/f/wav.svg [deleted file]
pix/f/wmv-128.png [new file with mode: 0644]
pix/f/wmv-24.png [new file with mode: 0644]
pix/f/wmv-256.png [new file with mode: 0644]
pix/f/wmv-32.png [new file with mode: 0644]
pix/f/wmv-48.png [new file with mode: 0644]
pix/f/wmv-64.png [new file with mode: 0644]
pix/f/wmv-72.png [new file with mode: 0644]
pix/f/wmv-80.png [new file with mode: 0644]
pix/f/wmv-96.png [new file with mode: 0644]
pix/f/wmv.png [new file with mode: 0644]
pix/f/wmv.svg [deleted file]
pix/f/writer-128.png [new file with mode: 0644]
pix/f/writer-24.png [new file with mode: 0644]
pix/f/writer-32.png [new file with mode: 0644]
pix/f/writer-48.png [new file with mode: 0644]
pix/f/writer-64.png [new file with mode: 0644]
pix/f/writer-72.png [new file with mode: 0644]
pix/f/writer-80.png [new file with mode: 0644]
pix/f/writer-96.png [new file with mode: 0644]
pix/f/writer.png [new file with mode: 0644]
pix/f/writer.svg [deleted file]
report/participation/index.php
theme/boost/templates/mod_forum/big_search_form.mustache
theme/bootstrapbase/less/moodle/blocks.less
theme/bootstrapbase/less/moodle/forms.less
theme/bootstrapbase/style/moodle.css
theme/bootstrapbase/templates/block_myoverview/course-event-list-item.mustache
theme/bootstrapbase/templates/block_myoverview/course-event-list.mustache [new file with mode: 0644]
theme/bootstrapbase/templates/block_myoverview/course-item.mustache
theme/bootstrapbase/templates/block_myoverview/course-paging-content-item.mustache [new file with mode: 0644]
theme/bootstrapbase/templates/block_myoverview/course-summary.mustache [new file with mode: 0644]
theme/bootstrapbase/templates/block_myoverview/courses-view-course-item.mustache [new file with mode: 0644]
theme/bootstrapbase/templates/block_myoverview/courses-view.mustache [new file with mode: 0644]
theme/bootstrapbase/templates/block_myoverview/event-list-group.mustache [new file with mode: 0644]
theme/bootstrapbase/templates/block_myoverview/event-list-item.mustache
theme/bootstrapbase/templates/block_myoverview/timeline-view.mustache

index 775f006..74de88a 100644 (file)
@@ -1012,7 +1012,7 @@ class manager {
         $messageparams = new \stdClass();
         $messageparams->html    = $message->html;
         $messageparams->plain   = $message->plain;
-        $messagepreferencesurl = new \moodle_url("/message/edit.php", array('id' => $USER->id));
+        $messagepreferencesurl = new \moodle_url("/message/notificationpreferences.php", array('id' => $USER->id));
         $messageparams->messagepreferencesurl = $messagepreferencesurl->out();
         $htmlmessage = get_string('messageprocessingsuccesshtml', 'tool_messageinbound', $messageparams);
         $plainmessage = get_string('messageprocessingsuccess', 'tool_messageinbound', $messageparams);
index 638941f..80a7dda 100644 (file)
@@ -49,7 +49,7 @@ It\'s highly recommended to be running the latest (+ version) available of your
 This functionality doesn\'t perform any action against the DB (just reads from it), so can be safely executed at any moment.';
 $string['confirmcheckforeignkeys'] = 'This functionality will search for potential violations of the foreign keys defined in the install.xml definitions. (Moodle does not currently generate actual foreign key constraints in the database, which is why invalid data may be present.)
 
-It\'s highly recommended to be running the latest (+ version) available of your Moodle release before executing the search of missing indexes.
+It\'s highly recommended to be running the latest (+ version) available of your Moodle release before executing the search for potential violations of the foreign keys.
 
 This functionality doesn\'t perform any action against the DB (just reads from it), so can be safely executed at any moment.';
 $string['confirmcheckindexes'] = 'This functionality will search for potential missing indexes in your Moodle server, generating (but not executing!) automatically the needed SQL statements to keep everything updated.
index 51a3043..0106627 100644 (file)
@@ -151,22 +151,6 @@ class auth extends \auth_plugin_base {
         return true;
     }
 
-    /**
-     * Prints a form for configuring this authentication plugin.
-     *
-     * This function is called from admin/auth.php, and outputs a full page with
-     * a form for configuring this plugin.
-     *
-     * @param stdClass $config
-     * @param string $err
-     * @param array $userfields
-     */
-    public function config_form($config, $err, $userfields) {
-        include(__DIR__ . "/../config.html");
-
-        return;
-    }
-
     /**
      * Return the userinfo from the oauth handshake. Will only be valid
      * for the logged in user.
diff --git a/auth/oauth2/config.html b/auth/oauth2/config.html
deleted file mode 100644 (file)
index e7ce606..0000000
+++ /dev/null
@@ -1,12 +0,0 @@
-<!-- No config needed -->
-<div style="text-align: center"><?php print_string('plugindescription', 'auth_oauth2'); ?></div>
-
-<div class="alert alert-warning m-y-2" style="text-align: center"><?php print_string('createaccountswarning', 'auth_oauth2'); ?></div>
-
-<table cellspacing="0" cellpadding="5" border="0">
-<?php
-
-print_auth_lock_options($this->authtype, $userfields, get_string('auth_fieldlocks_help', 'auth'), false, false);
-
-?>
-</table>
diff --git a/auth/oauth2/settings.php b/auth/oauth2/settings.php
new file mode 100644 (file)
index 0000000..51e15b7
--- /dev/null
@@ -0,0 +1,35 @@
+<?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/>.
+
+/**
+ * Admin settings and defaults.
+ *
+ * @package auth_oauth2
+ * @copyright  2017 Damyon Wiese
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die;
+
+if ($ADMIN->fulltree) {
+
+    $warning = $OUTPUT->notification(get_string('createaccountswarning', 'auth_oauth2'), 'warning');
+    $settings->add(new admin_setting_heading('auth_oauth2/pluginname', '', $warning));
+
+    $authplugin = get_auth_plugin($this->name);
+    display_auth_lock_options($settings, $authplugin->authtype, $authplugin->userfields,
+            get_string('auth_fieldlocks_help', 'auth'), false, false);
+}
index 997f757..7bb0bec 100644 (file)
@@ -26,13 +26,14 @@ if (!is_enabled_auth('shibboleth')) {
 $inputstream = file_get_contents("php://input");
 if ($action == 'logout' && !empty($redirect)) {
 
-    if ($USER->auth == 'shibboleth') {
-        // Logout out user from application.
+    if (isloggedin($USER) && $USER->auth == 'shibboleth') {
+        // Logout user from application.
         require_logout();
-         // Finally, send user to the return URL.
-        redirect($redirect);
     }
 
+    // Finally, send user to the return URL.
+    redirect($redirect);
+
 } else if (!empty($inputstream)) {
 
     // Back channel logout.
index c945b00..f29d301 100644 (file)
@@ -153,7 +153,8 @@ class block_community_renderer extends plugin_renderer_base {
                                 array('class' => 'hubcourseuserinfo'));
 
                 //create course content related information html
-                $course->subject = get_string($course->subject, 'edufields');
+                $course->subject = (get_string_manager()->string_exists($course->subject, 'edufields')) ?
+                        get_string($course->subject, 'edufields') : get_string('none');
                 $course->audience = get_string('audience' . $course->audience, 'hub');
                 $course->educationallevel = get_string('edulevel' . $course->educationallevel, 'hub');
                 $coursecontentinfo = '';
diff --git a/blocks/myoverview/classes/output/course_summary.php b/blocks/myoverview/classes/output/course_summary.php
deleted file mode 100644 (file)
index 5155930..0000000
+++ /dev/null
@@ -1,82 +0,0 @@
-<?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/>.
-
-/**
- * Class containing data for my overview block.
- *
- * @package    block_myoverview
- * @copyright  2017 Simey Lameze <simey@moodle.com>
- * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
- */
-namespace block_myoverview\output;
-defined('MOODLE_INTERNAL') || die();
-
-use core_course\external\course_summary_exporter;
-use renderable;
-use renderer_base;
-use templatable;
-/**
- * Class containing data for my overview block.
- *
- * @copyright  2017 Simey Lameze <simey@moodle.com>
- * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
- */
-class course_summary implements renderable, templatable {
-
-    /** @var array $courses List of courses the user is enrolled in. */
-    protected $courses = [];
-
-    /** @var array $coursesprogress List of progress percentage for each course. */
-    protected $coursesprogress = [];
-
-    /**
-     * The course_summary constructor.
-     *
-     * @param array $courses list of courses.
-     * @param array $coursesprogress list of courses progress.
-     */
-    public function __construct($courses, $coursesprogress) {
-        $this->courses = $courses;
-        $this->coursesprogress = $coursesprogress;
-    }
-
-    /**
-     * Export this data so it can be used as the context for a mustache template.
-     *
-     * @param \renderer_base $output
-     * @return array
-     */
-    public function export_for_template(renderer_base $output) {
-
-        $data = [];
-        foreach ($this->courses as $courseid => $course) {
-            $context = \context_course::instance($courseid);
-            // Convert summary to plain text.
-            $course->summary = content_to_text($course->summary, false);
-            $exporter = new course_summary_exporter($course, array('context' => $context));
-            $exportedcourse = $exporter->export($output);
-
-            if (isset($this->coursesprogress[$courseid])) {
-                $courseprogress = $this->coursesprogress[$courseid];
-                $exportedcourse->hasprogress = !is_null($courseprogress);
-                $exportedcourse->progress = $courseprogress;
-            }
-
-            $data[] = $exportedcourse;
-        }
-        return $data;
-    }
-}
index 86b0e49..c92346a 100644 (file)
@@ -97,6 +97,7 @@ class courses_view implements renderable, templatable {
                 $coursesview['future']['pages'][$futurepages]['courses'][] = $exportedcourse;
                 $coursesview['future']['pages'][$futurepages]['active'] = ($futurepages == 0 ? true : false);
                 $coursesview['future']['pages'][$futurepages]['page'] = $futurepages + 1;
+                $coursesview['future']['haspages'] = true;
                 $coursesbystatus['future']++;
 
             } else if (!empty($enddate) && $enddate < $today) {
@@ -106,6 +107,7 @@ class courses_view implements renderable, templatable {
                 $coursesview['past']['pages'][$pastpages]['courses'][] = $exportedcourse;
                 $coursesview['past']['pages'][$pastpages]['active'] = ($pastpages == 0 ? true : false);
                 $coursesview['past']['pages'][$pastpages]['page'] = $pastpages + 1;
+                $coursesview['past']['haspages'] = true;
                 $coursesbystatus['past']++;
 
             } else {
@@ -115,6 +117,7 @@ class courses_view implements renderable, templatable {
                 $coursesview['inprogress']['pages'][$inprogresspages]['courses'][] = $exportedcourse;
                 $coursesview['inprogress']['pages'][$inprogresspages]['active'] = ($inprogresspages == 0 ? true : false);
                 $coursesview['inprogress']['pages'][$inprogresspages]['page'] = $inprogresspages + 1;
+                $coursesview['inprogress']['haspages'] = true;
                 $coursesbystatus['inprogress']++;
             }
         }
index bdc92d8..377697f 100644 (file)
@@ -57,13 +57,11 @@ class main implements renderable, templatable {
             $coursesprogress[$course->id] = $percentage;
         }
 
-        $coursesummary = new course_summary($courses, $coursesprogress);
         $coursesview = new courses_view($courses, $coursesprogress);
         $nocoursesurl = $output->image_url('courses', 'block_myoverview')->out();
         $noeventsurl = $output->image_url('activities', 'block_myoverview')->out();
 
         return [
-            'courses' => $coursesummary->export_for_template($output),
             'coursesview' => $coursesview->export_for_template($output),
             'urls' => [
                 'nocourses' => $nocoursesurl,
index 02b0832..87ce648 100644 (file)
@@ -24,6 +24,7 @@
 
 $string['future'] = 'Future';
 $string['inprogress'] = 'In progress';
+$string['morecourses'] = 'More courses';
 $string['myoverview:addinstance'] = 'Add a new my overview block';
 $string['myoverview:myaddinstance'] = 'Add a new my overview block to Dashboard';
 $string['nocourses'] = 'No courses';
index aa77887..529e33b 100644 (file)
@@ -28,7 +28,7 @@
         "summary": "It is a long established fact that a reader will be distracted by the readable content of a page when looking at its layout."
     }
 }}
-<div class="col-lg-6 col-xl-4">
+<div class="col-lg-6">
     <div class="card m-b-1 courses-view-course-item">
         <div class="card-block course-info-container" id="course-info-container-{{id}}">
             <div class="hidden-sm-up hidden-phone">
index 39b41e1..2f0d72a 100644 (file)
     Example context (json):
     {}
 }}
-<ul class="list-group unstyled" id="timeline-view-courses-{{uniqid}}">
-    {{#courses}} {{> block_myoverview/course-item }} {{/courses}}
-    {{^courses}}
-        <div class="text-xs-center text-center m-t-3">
-            <img class="empty-placeholder-image-lg"
-                 src="{{urls.noevents}}"
-                 alt="{{#str}} nocoursesinprogress, block_myoverview {{/str}}">
-            <p class="text-muted m-t-1">{{#str}} nocoursesinprogress, block_myoverview {{/str}}</p>
-        </div>
-    {{/courses}}
-</ul>
+<div id="sort-by-courses-view-{{uniqid}}">
+    {{#coursesview}}
+        {{#inprogress}}
+            {{#haspages}}
+                {{#pages}}
+                    <ul class="list-group unstyled hidden" data-region="course-block">
+                        {{#courses}} {{> block_myoverview/course-item }} {{/courses}}
+                    </ul>
+                {{/pages}}
+                <div class="text-xs-center text-center m-t-1">
+                    <button type="button" class="btn btn-secondary" data-action="more-courses">
+                        {{#str}} morecourses, block_myoverview {{/str}}
+                        <span class="hidden" data-region="loading-icon-container">
+                            {{> core/loading }}
+                        </span>
+                    </button>
+                </div>
+            {{/haspages}}
+            {{^haspages}}
+                <div class="text-xs-center text-center m-t-3">
+                    <img class="empty-placeholder-image-lg"
+                         src="{{urls.noevents}}"
+                         alt="{{#str}} nocoursesinprogress, block_myoverview {{/str}}">
+                    <p class="text-muted m-t-1">{{#str}} nocoursesinprogress, block_myoverview {{/str}}</p>
+                </div>
+            {{/haspages}}
+        {{/inprogress}}
+        {{^inprogress}}
+            <div class="text-xs-center text-center m-t-3">
+                <img class="empty-placeholder-image-lg"
+                     src="{{urls.noevents}}"
+                     alt="{{#str}} nocoursesinprogress, block_myoverview {{/str}}">
+                <p class="text-muted m-t-1">{{#str}} nocoursesinprogress, block_myoverview {{/str}}</p>
+            </div>
+        {{/inprogress}}
+    {{/coursesview}}
+</div>
 {{#js}}
-    require(['jquery', 'block_myoverview/event_list_by_course'], function($, EventListByCourse) {
-    var root = $("#timeline-view-courses-{{uniqid}}");
-    EventListByCourse.init(root);
+    require(['jquery', 'core/custom_interaction_events', 'block_myoverview/event_list_by_course'],
+        function($, CustomEvents, EventListByCourse) {
+
+        var root = $("#sort-by-courses-view-{{uniqid}}");
+        // This flag is used so that we can delay the loading of the events until the tab
+        // is toggled by the user.
+        var seen = false;
+
+        CustomEvents.define(root, [CustomEvents.events.activate]);
+        // Show more courses and load their events when the user clicks the "more courses"
+        // button.
+        root.on(CustomEvents.events.activate, '[data-action="more-courses"]', function(e, data) {
+            var button = $(e.target);
+            var blocks = root.find('[data-region="course-block"].hidden');
+
+            if (blocks && blocks.length) {
+                var block = blocks.first();
+                EventListByCourse.init(block);
+                block.removeClass('hidden');
+            }
+
+            // If there was only one hidden block then we have no more to show now
+            // so we can disable the button.
+            if (blocks && blocks.length == 1) {
+                button.prop('disabled', true);
+            }
+
+            if (data) {
+                data.originalEvent.preventDefault();
+                data.originalEvent.stopPropagation();
+            }
+            e.stopPropagation();
+        });
+
+        // Listen for when the user changes tab so that we can show the first set of courses
+        // and load their events when they request the sort by courses view for the first time.
+        root.closest('[data-region="timeline-view"]').on('shown shown.bs.tab', function(e) {
+            if (seen) {
+                return;
+            }
+
+            var tab = $(e.target);
+            var tabTarget = $(tab.attr('href'));
+
+            if (!tabTarget || !tabTarget.length) {
+                return;
+            }
+
+            var viewCourses = tabTarget.find('#sort-by-courses-view-{{uniqid}}');
+
+            if (viewCourses && viewCourses.length && !seen) {
+                seen = true;
+                viewCourses.find('[data-action="more-courses"]').trigger(CustomEvents.events.activate);
+            }
+        });
     });
 {{/js}}
index 014262c..378dc98 100644 (file)
@@ -285,14 +285,18 @@ class core_calendar_renderer extends plugin_renderer_base {
             }
             $output .= html_writer::tag('div', $source, array('class' => 'subscription'));
         }
+        if (!empty($event->courselink)) {
+            $output .= html_writer::tag('div', $event->courselink, array('class' => 'course'));
+        }
         if (!empty($event->time)) {
             $output .= html_writer::tag('span', $event->time, array('class' => 'date pull-xs-right m-r-1'));
         } else {
             $attrs = array('class' => 'date pull-xs-right m-r-1');
             $output .= html_writer::tag('span', calendar_time_representation($event->timestart), $attrs);
         }
-        if (!empty($event->courselink)) {
-            $output .= html_writer::tag('div', $event->courselink, array('class' => 'course'));
+
+        if (!empty($event->actionurl)) {
+            $output .= html_writer::tag('div', html_writer::link(new moodle_url($event->actionurl), $event->actionname));
         }
 
         $output .= $this->output->box_end();
index a6882a0..48d89d7 100644 (file)
@@ -853,7 +853,19 @@ $CFG->admin = 'admin';
 // Unoconv is used convert between file formats supported by LibreOffice.
 // Use a recent version of unoconv ( >= 0.7 ), older versions have trouble running from a webserver.
 //      $CFG->pathtounoconv = '';
-
+//
+//=========================================================================
+// 14. ALTERNATIVE FILE SYSTEM SETTINGS
+//=========================================================================
+//
+// Alternative file system.
+// Since 3.3 it is possible to override file_storage and file_system API and use alternative storage systems (e.g. S3,
+// Rackspace Cloud Files, Google Cloud Storage, Azure Storage, etc.).
+// To set the alternative file storage system in config.php you can use the following setting, providing the
+// alternative system class name that will be auto-loaded by file_storage API.
+//
+//      $CFG->alternative_file_system_class = '\\local_myfilestorage\\file_system';
+//
 //=========================================================================
 // ALL DONE!  To continue installation, visit your main page with a browser
 //=========================================================================
index 6efb9f4..27733fc 100644 (file)
@@ -2664,7 +2664,7 @@ class core_course_external extends external_api {
      * Returns description of method parameters
      *
      * @deprecated since 3.3
-     *
+     * @todo The final deprecation of this function will take place in Moodle 3.7 - see MDL-57487.
      * @return external_function_parameters
      * @since Moodle 3.2
      */
@@ -2680,7 +2680,7 @@ class core_course_external extends external_api {
      * Return activities overview for the given courses.
      *
      * @deprecated since 3.3
-     *
+     * @todo The final deprecation of this function will take place in Moodle 3.7 - see MDL-57487.
      * @param array $courseids a list of course ids
      * @return array of warnings and the activities overview
      * @since Moodle 3.2
@@ -2739,7 +2739,7 @@ class core_course_external extends external_api {
      * Returns description of method result value
      *
      * @deprecated since 3.3
-     *
+     * @todo The final deprecation of this function will take place in Moodle 3.7 - see MDL-57487.
      * @return external_description
      * @since Moodle 3.2
      */
index b30e442..9d0cb22 100644 (file)
@@ -484,7 +484,7 @@ class core_course_externallib_testcase extends externallib_advanced_testcase {
                     $course3options['numsections']);
                 $this->assertEquals($courseinfo->coursedisplay, $course3options['coursedisplay']);
             } else {
-                throw moodle_exception('Unexpected shortname');
+                throw new moodle_exception('Unexpected shortname');
             }
         }
 
@@ -1161,7 +1161,7 @@ class core_course_externallib_testcase extends externallib_advanced_testcase {
                 $this->assertEquals(0, $courseinfo->newsitems);
                 $this->assertEquals(FORMAT_MOODLE, $courseinfo->summaryformat);
             } else {
-                throw moodle_exception('Unexpected shortname');
+                throw new moodle_exception('Unexpected shortname');
             }
         }
 
index 750ecdb..5542d12 100644 (file)
@@ -301,12 +301,18 @@ YUI.add('moodle-enrol-rolemanager', function(Y) {
         addRoleToDisplay : function(roleId, roleTitle) {
             var m = this.get(MANIPULATOR);
             var container = this.get(CONTAINER);
-            var role = Y.Node.create('<div class="role role_'+roleId+'">'+roleTitle+'<a class="unassignrolelink"><img src="'+M.util.image_url('t/delete', 'moodle')+'" alt="" /></a></div>');
-            var link = role.one('.unassignrolelink');
-            link.roleId = roleId;
-            link.on('click', m.removeRole, m, this, link.roleId);
-            container.one('.col_role .roles').append(role);
-            this._toggleCurrentRole(link.roleId, true);
+            window.require(['core/templates'], function(Templates) {
+                Templates.renderPix('t/delete', 'core').then(function(pix) {
+                    var role = Y.Node.create('<div class="role role_' + roleId + '">' +
+                                             roleTitle +
+                                             '<a class="unassignrolelink">' + pix + '</a></div>');
+                    var link = role.one('.unassignrolelink');
+                    link.roleId = roleId;
+                    link.on('click', m.removeRole, m, this, link.roleId);
+                    container.one('.col_role .roles').append(role);
+                    this._toggleCurrentRole(link.roleId, true);
+                }.bind(this));
+            }.bind(this));
         },
         removeRoleFromDisplay : function(roleId) {
             var container = this.get(CONTAINER);
index c2b48d7..3889226 100644 (file)
@@ -40,17 +40,48 @@ $string['application/msword'] = 'Word document';
 $string['application/pdf'] = 'PDF document';
 $string['application/vnd.moodle.backup'] = 'Moodle backup';
 $string['application/vnd.ms-excel'] = 'Excel spreadsheet';
+$string['application/vnd.ms-excel.sheet.macroEnabled.12'] = 'Excel 2007 macro-enabled workbook';
 $string['application/vnd.ms-powerpoint'] = 'Powerpoint presentation';
-$string['application/vnd.openxmlformats-officedocument.presentationml.presentation'] = 'Powerpoint presentation';
-$string['application/vnd.openxmlformats-officedocument.presentationml.slideshow'] = 'Powerpoint slideshow';
-$string['application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'] = 'Excel spreadsheet';
-$string['application/vnd.openxmlformats-officedocument.spreadsheetml.template'] = 'Excel template';
-$string['application/vnd.openxmlformats-officedocument.wordprocessingml.document'] = 'Word document';
+$string['application/vnd.oasis.opendocument.spreadsheet'] = 'OpenDocument Spreadsheet';
+$string['application/vnd.oasis.opendocument.spreadsheet-template'] = 'OpenDocument Spreadsheet template';
+$string['application/vnd.oasis.opendocument.text'] = 'OpenDocument Text document';
+$string['application/vnd.oasis.opendocument.text-template'] = 'OpenDocument Text template';
+$string['application/vnd.oasis.opendocument.text-web'] = 'OpenDocument Web page template';
+$string['application/vnd.openxmlformats-officedocument.presentationml.presentation'] = 'Powerpoint 2007 presentation';
+$string['application/vnd.openxmlformats-officedocument.presentationml.slideshow'] = 'Powerpoint 2007 slideshow';
+$string['application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'] = 'Excel 2007 spreadsheet';
+$string['application/vnd.openxmlformats-officedocument.spreadsheetml.template'] = 'Excel 2007 template';
+$string['application/vnd.openxmlformats-officedocument.wordprocessingml.document'] = 'Word 2007 document';
+$string['application/x-iwork-keynote-sffkey'] = 'iWork Keynote presentation';
+$string['application/x-iwork-numbers-sffnumbers'] = 'iWork Numbers spreadsheet';
+$string['application/x-iwork-pages-sffpages'] = 'iWork Pages document';
+$string['application/x-javascript'] = 'JavaScript source';
+$string['application/x-mspublisher'] = 'Publisher document';
+$string['application/x-shockwave-flash'] = 'Flash animation';
+$string['application/xhtml_xml'] = 'XHTML document';
 $string['archive'] = 'Archive ({$a->EXT})';
 $string['audio'] = 'Audio file ({$a->EXT})';
 $string['default'] = '{$a->mimetype}';
 $string['document/unknown'] = 'File';
+$string['group:archive'] = 'Archive files';
+$string['group:audio'] = 'Audio files';
+$string['group:document'] = 'Document files';
+$string['group:html_audio'] = 'Audio files natively supported by browsers';
+$string['group:html_track'] = 'HTML track files';
+$string['group:html_video'] = 'Video files natively supported by browsers';
+$string['group:image'] = 'Image files';
+$string['group:presentation'] = 'Presentation files';
+$string['group:sourcecode'] = 'Source code';
+$string['group:spreadsheet'] = 'Spreadsheet files';
+$string['group:video'] = 'Video files';
+$string['group:web_audio'] = 'Audio files used on the web';
+$string['group:web_file'] = 'Web files';
+$string['group:web_image'] = 'Image files used on the web';
+$string['group:web_video'] = 'Video files used on the web';
 $string['image'] = 'Image ({$a->MIMETYPE2})';
+$string['image/vnd.microsoft.icon'] = 'Windows icon';
+$string['text/css'] = 'Cascading Style-Sheet';
+$string['text/csv'] = 'Comma-separated values';
 $string['text/html'] = 'HTML document';
 $string['text/plain'] = 'Text file';
 $string['text/rtf'] = 'RTF document';
index 2b0ab86..c377d40 100644 (file)
@@ -1890,6 +1890,7 @@ $string['today'] = 'Today';
 $string['todaylogs'] = 'Today\'s logs';
 $string['toeveryone'] = 'to everyone';
 $string['toomanybounces'] = 'That email address has had too many bounces. You <b>must</b> change it to continue.';
+$string['toomanytags'] = 'This search included too many tags, some will have been ignored.';
 $string['toomanytoshow'] = 'There are too many users to show.';
 $string['toomanyusersmatchsearch'] = 'Too many users ({$a->count}) match \'{$a->search}\'';
 $string['toomanyuserstoshow'] = 'Too many users ({$a}) to show';
index 79cd16a..936d338 100644 (file)
Binary files a/lib/amd/build/ajax.min.js and b/lib/amd/build/ajax.min.js differ
index b2afb6b..663ff25 100644 (file)
@@ -111,7 +111,9 @@ define(['jquery', 'core/config'], function($, config) {
         call: function(requests, async, loginrequired) {
             var ajaxRequestData = [],
                 i,
-                promises = [];
+                promises = [],
+                methodInfo = [],
+                requestInfo = '';
 
             if (typeof loginrequired === "undefined") {
                 loginrequired = true;
@@ -137,6 +139,13 @@ define(['jquery', 'core/config'], function($, config) {
                     request.deferred.fail(request.fail);
                 }
                 request.index = i;
+                methodInfo.push(request.methodname);
+            }
+
+            if (methodInfo.length <= 5) {
+                requestInfo = methodInfo.sort().join();
+            } else {
+                requestInfo = methodInfo.length + '-method-calls';
             }
 
             ajaxRequestData = JSON.stringify(ajaxRequestData);
@@ -150,20 +159,22 @@ define(['jquery', 'core/config'], function($, config) {
                 contentType: "application/json"
             };
 
-            var script = config.wwwroot + '/lib/ajax/service.php?sesskey=' + config.sesskey;
+            var script = 'service.php';
             if (!loginrequired) {
-                script = config.wwwroot + '/lib/ajax/service-nologin.php?sesskey=' + config.sesskey;
+                script = 'service-nologin.php';
             }
+            var url = config.wwwroot + '/lib/ajax/' + script +
+                    '?sesskey=' + config.sesskey + '&info=' + requestInfo;
 
             // Jquery deprecated done and fail with async=false so we need to do this 2 ways.
             if (async) {
-                $.ajax(script, settings)
+                $.ajax(url, settings)
                     .done(requestSuccess)
                     .fail(requestFail);
             } else {
                 settings.success = requestSuccess;
                 settings.error = requestFail;
-                $.ajax(script, settings);
+                $.ajax(url, settings);
             }
 
             return promises;
index 97ff456..23c0191 100644 (file)
@@ -90,7 +90,7 @@ abstract class core_filetypes {
             'dxr' => array('type' => 'application/x-director', 'icon' => 'flash'),
             'eps' => array('type' => 'application/postscript', 'icon' => 'eps'),
             'epub' => array('type' => 'application/epub+zip', 'icon' => 'epub', 'groups' => array('document')),
-            'fdf' => array('type' => 'application/pdf', 'icon' => 'pdf'),
+            'fdf' => array('type' => 'application/vnd.fdf', 'icon' => 'pdf'),
             'flac' => array('type' => 'audio/flac', 'icon' => 'audio', 'groups' => array('audio', 'html_audio', 'web_audio'),
                     'string' => 'audio'),
             'flv' => array('type' => 'video/x-flv', 'icon' => 'flash',
@@ -197,7 +197,7 @@ abstract class core_filetypes {
                     'string' => 'video'),
 
             'pct' => array('type' => 'image/pict', 'icon' => 'image', 'groups' => array('image'), 'string' => 'image'),
-            'pdf' => array('type' => 'application/pdf', 'icon' => 'pdf'),
+            'pdf' => array('type' => 'application/pdf', 'icon' => 'pdf', 'groups' => array('document')),
             'php' => array('type' => 'text/plain', 'icon' => 'sourcecode'),
             'pic' => array('type' => 'image/pict', 'icon' => 'image', 'groups' => array('image'), 'string' => 'image'),
             'pict' => array('type' => 'image/pict', 'icon' => 'image', 'groups' => array('image'), 'string' => 'image'),
@@ -283,12 +283,13 @@ abstract class core_filetypes {
             'wma' => array('type' => 'audio/x-ms-wma', 'icon' => 'audio', 'groups' => array('audio'), 'string' => 'audio'),
 
             'xbk' => array('type' => 'application/x-smarttech-notebook', 'icon' => 'archive'),
-            'xdp' => array('type' => 'application/pdf', 'icon' => 'pdf'),
-            'xfd' => array('type' => 'application/pdf', 'icon' => 'pdf'),
-            'xfdf' => array('type' => 'application/pdf', 'icon' => 'pdf'),
+            'xdp' => array('type' => 'application/vnd.adobe.xdp+xml', 'icon' => 'pdf'),
+            'xfd' => array('type' => 'application/vnd.xfdl', 'icon' => 'pdf'),
+            'xfdf' => array('type' => 'application/vnd.adobe.xfdf', 'icon' => 'pdf'),
 
             'xls' => array('type' => 'application/vnd.ms-excel', 'icon' => 'spreadsheet', 'groups' => array('spreadsheet')),
-            'xlsx' => array('type' => 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', 'icon' => 'spreadsheet'),
+            'xlsx' => array('type' => 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', 'icon' => 'spreadsheet',
+                'groups' => array('spreadsheet')),
             'xlsm' => array('type' => 'application/vnd.ms-excel.sheet.macroEnabled.12',
                     'icon' => 'spreadsheet', 'groups' => array('spreadsheet')),
             'xltx' => array('type' => 'application/vnd.openxmlformats-officedocument.spreadsheetml.template',
index 993275f..15ea1d2 100644 (file)
@@ -170,48 +170,6 @@ class icon_system_fontawesome extends icon_system_font {
             'core:e/undo' => 'fa-undo',
             'core:e/visual_aid' => 'fa-universal-access',
             'core:e/visual_blocks' => 'fa-audio-description',
-            'core:f/archive' => 'fa-file-zip-o',
-            'core:f/audio' => 'fa-file-audio-o',
-            'core:f/avi' => 'fa-file-movie-o',
-            'core:f/base' => 'fa-file-o',
-            'core:f/bmp' => 'fa-file-image-o',
-            'core:f/calc' => 'fa-file-excel-o',
-            'core:f/chart' => 'fa-bar-chart',
-            'core:f/database' => 'fa-database',
-            'core:f/document' => 'fa-file',
-            'core:f/draw' => 'fa-file-image-o',
-            'core:f/eps' => 'fa-file-pdf-o',
-            'core:f/epub' => 'fa-book',
-            'core:f/flash' => 'fa-flash',
-            'core:f/folder' => 'fa-folder-o',
-            'core:f/folder-open' => 'fa-folder-open-o',
-            'core:f/gif' => 'fa-file-image-o',
-            'core:f/html' => 'fa-file-code-o',
-            'core:f/image' => 'fa-file-image-o',
-            'core:f/impress' => 'fa-file-powerpoint-o',
-            'core:f/isf' => 'fa-file-image-o',
-            'core:f/jpeg' => 'fa-file-image-o',
-            'core:f/markup' => 'fa-file-code-o',
-            'core:f/math' => 'fa-calculator',
-            'core:f/moodle' => 'fa-graduation-cap',
-            'core:f/mp3' => 'fa-file-audio-o',
-            'core:f/mpeg' => 'fa-file-video-o',
-            'core:f/oth' => 'fa-file-o',
-            'core:f/pdf' => 'fa-file-pdf-o',
-            'core:f/png' => 'fa-file-image-o',
-            'core:f/powerpoint' => 'fa-file-powerpoint-o',
-            'core:f/psd' => 'fa-file-image-o',
-            'core:f/publisher' => 'fa-file-image-o',
-            'core:f/quicktime' => 'fa-file-video-o',
-            'core:f/sourcecode' => 'fa-file-code-o',
-            'core:f/spreadsheet' => 'fa-file-excel-o',
-            'core:f/text' => 'fa-file-text-o',
-            'core:f/tiff' => 'fa-file-image-o',
-            'core:f/unknown' => 'fa-file-o',
-            'core:f/video' => 'fa-file-video-o',
-            'core:f/wav' => 'fa-file-audio-o',
-            'core:f/wmv' => 'fa-file-video-o',
-            'core:f/writer' => 'fa-file-word-o',
             'theme:fp/add_file' => 'fa-file-o',
             'theme:fp/alias' => 'fa-link',
             'theme:fp/check' => 'fa-check',
@@ -388,7 +346,7 @@ class icon_system_fontawesome extends icon_system_font {
             'core:t/messages' => 'fa-comments',
             'core:t/message' => 'fa-comment',
             'core:t/more' => 'fa-caret-down',
-            'core:t/move' => 'fa-arrows',
+            'core:t/move' => 'fa-arrows-v',
             'core:t/passwordunmask-edit' => 'fa-pencil',
             'core:t/passwordunmask-reveal' => 'fa-eye',
             'core:t/portfolioadd' => 'fa-plus',
index 356fb71..721fce7 100644 (file)
@@ -1074,8 +1074,11 @@ abstract class sql_generator {
         // along all the request life, but never to return cached results
         // We need this because sql statements are created before executing
         // them, hence names doesn't exist "physically" yet in DB, so we need
-        // to known which ones have been used
-        static $used_names = array();
+        // to known which ones have been used.
+        // We track all the keys used, and the previous counters to make subsequent creates faster.
+        // This may happen a lot with things like bulk backups or restores.
+        static $usednames = array();
+        static $previouscounters = array();
 
         // Use standard naming. See http://docs.moodle.org/en/XMLDB_key_and_index_naming
         $tablearr = explode ('_', $tablename);
@@ -1094,17 +1097,26 @@ abstract class sql_generator {
         $maxlengthwithoutsuffix = $this->names_max_length - strlen($suffix) - ($suffix ? 1 : 0);
         $namewithsuffix = substr($name, 0, $maxlengthwithoutsuffix) . ($suffix ? ('_' . $suffix) : '');
 
-        // If the calculated name is in the cache, or if we detect it by introspecting the DB let's modify if
-        $counter = 1;
-        while (in_array($namewithsuffix, $used_names) || $this->isNameInUse($namewithsuffix, $suffix, $tablename)) {
+        if (isset($previouscounters[$name])) {
+            // If we have a counter stored, we will need to modify the key to the next counter location.
+            $counter = $previouscounters[$name] + 1;
+            $namewithsuffix = substr($name, 0, $maxlengthwithoutsuffix - strlen($counter)) .
+                    $counter . ($suffix ? ('_' . $suffix) : '');
+        } else {
+            $counter = 1;
+        }
+
+        // If the calculated name is in the cache, or if we detect it by introspecting the DB let's modify it.
+        while (isset($usednames[$namewithsuffix]) || $this->isNameInUse($namewithsuffix, $suffix, $tablename)) {
             // Now iterate until not used name is found, incrementing the counter
             $counter++;
             $namewithsuffix = substr($name, 0, $maxlengthwithoutsuffix - strlen($counter)) .
                     $counter . ($suffix ? ('_' . $suffix) : '');
         }
 
-        // Add the name to the cache
-        $used_names[] = $namewithsuffix;
+        // Add the name to the cache. Using key look with isset because it is much faster than in_array.
+        $usednames[$namewithsuffix] = true;
+        $previouscounters[$name] = $counter;
 
         // Quote it if necessary (reserved words)
         $namewithsuffix = $this->getEncQuoted($namewithsuffix);
index a0d78fd..edf6258 100644 (file)
@@ -2080,6 +2080,38 @@ class core_ddl_testcase extends database_driver_testcase {
                         strlen($gen->getNameForObject($table, $fields, $suffix)),
                         'Generated object name is too long. $i = '.$i);
             }
+
+            // Now test to confirm that a duplicate name isn't issued, even if they come from different root names.
+            // Move to a new field.
+            $fields = "fl";
+
+            // Insert twice, moving is to a key with fl2.
+            $this->assertEquals($gen->names_max_length - 1, strlen($gen->getNameForObject($table, $fields, $suffix)));
+            $result1 = $gen->getNameForObject($table, $fields, $suffix);
+
+            // Make sure we end up with _fl2_ in the result.
+            $this->assertRegExp('/_fl2_/', $result1);
+
+            // Now, use a field that would result in the same key if it wasn't already taken.
+            $fields = "fl2";
+            // Because we are now at the max key length, it will try:
+            // - _fl2_ (the natural name)
+            // - _fl2_ (removing the original 2, and adding a counter 2)
+            // - then settle on _fl3_.
+            $result2 = $gen->getNameForObject($table, $fields, $suffix);
+            $this->assertRegExp('/_fl3_/', $result2);
+
+            // Make sure they don't match.
+            $this->assertNotEquals($result1, $result2);
+            // But are only different in the way we expect. This confirms the test is working properly.
+            $this->assertEquals(str_replace('_fl2_', '', $result1), str_replace('_fl3_', '', $result2));
+
+            // Now go back. We would expect the next result to be fl3 again, but it is taken, so it should move to fl4.
+            $fields = "fl";
+            $result3 = $gen->getNameForObject($table, $fields, $suffix);
+
+            $this->assertNotEquals($result2, $result3);
+            $this->assertRegExp('/_fl4_/', $result3);
         }
     }
 
index 09b22a6..6be8c82 100644 (file)
@@ -6551,6 +6551,7 @@ function calendar_preferences_button(stdClass $course) {
  * Return the name of the weekday
  *
  * @deprecated since 3.3
+ * @todo The final deprecation of this function will take place in Moodle 3.7 - see MDL-57617.
  * @param string $englishname
  * @return string of the weekeday
  */
@@ -6563,6 +6564,7 @@ function calendar_wday_name($englishname) {
  * Get the upcoming event block.
  *
  * @deprecated since 3.3
+ * @todo The final deprecation of this function will take place in Moodle 3.7 - see MDL-57617.
  * @param array $events list of events
  * @param moodle_url|string $linkhref link to event referer
  * @param boolean $showcourselink whether links to courses should be shown
@@ -6583,6 +6585,7 @@ function calendar_get_block_upcoming($events, $linkhref = null, $showcourselink
  * Display month selector options.
  *
  * @deprecated since 3.3
+ * @todo The final deprecation of this function will take place in Moodle 3.7 - see MDL-57617.
  * @param string $name for the select element
  * @param string|array $selected options for select elements
  */
@@ -6600,6 +6603,7 @@ function calendar_print_month_selector($name, $selected) {
  * Update calendar subscriptions.
  *
  * @deprecated since 3.3
+ * @todo The final deprecation of this function will take place in Moodle 3.7 - see MDL-57617.
  * @return bool
  */
 function calendar_cron() {
index 688ec00..38eaf31 100644 (file)
@@ -1027,7 +1027,7 @@ function filter_preload_activities(course_modinfo $modinfo) {
 
     // Get all filter_active rows relating to all these contexts
     list ($sql, $params) = $DB->get_in_or_equal($allcontextids);
-    $filteractives = $DB->get_records_select('filter_active', "contextid $sql", $params);
+    $filteractives = $DB->get_records_select('filter_active', "contextid $sql", $params, 'sortorder');
 
     // Get all filter_config only for the cm contexts
     list ($sql, $params) = $DB->get_in_or_equal($cmcontextids);
index cc2a1e2..2331474 100644 (file)
@@ -1932,6 +1932,10 @@ function set_user_preference($name, $value, $user = null) {
 
     // Update value in cache.
     $user->preference[$name] = $value;
+    // Update the $USER in case where we've not a direct reference to $USER.
+    if ($user !== $USER && $user->id == $USER->id) {
+        $USER->preference[$name] = $value;
+    }
 
     // Set reload flag for other sessions.
     mark_user_preferences_changed($user->id);
@@ -2001,6 +2005,10 @@ function unset_user_preference($name, $user = null) {
 
     // Delete the preference from cache.
     unset($user->preference[$name]);
+    // Update the $USER in case where we've not a direct reference to $USER.
+    if ($user !== $USER && $user->id == $USER->id) {
+        unset($USER->preference[$name]);
+    }
 
     // Set reload flag for other sessions.
     mark_user_preferences_changed($user->id);
index 86ee87c..77ee70e 100644 (file)
@@ -1825,7 +1825,8 @@ class theme_config {
      *
      * @param string $image name of image, may contain relative path
      * @param string $component
-     * @param bool $svg If set to true SVG images will also be looked for.
+     * @param bool $svg|null Should SVG images also be looked for? If null, resorts to $CFG->svgicons if that is set; falls back to
+     * auto-detection of browser support otherwise
      * @return string full file path
      */
     public function resolve_image_location($image, $component, $svg = false) {
index 22169bb..b9ddfac 100644 (file)
@@ -1,4 +1,4 @@
-Description of PHPMailer 5.2.16 library import into Moodle
+Description of PHPMailer 5.2.23 library import into Moodle
 
 We now use a vanilla version of phpmailer and do our customisations in a
 subclass.
index 56f1219..3ace8b4 100644 (file)
@@ -1 +1 @@
-5.2.16
\ No newline at end of file
+5.2.23
index f9013eb..1b31ec1 100644 (file)
@@ -31,7 +31,7 @@ class PHPMailer
      * The PHPMailer Version number.
      * @var string
      */
-    public $Version = '5.2.16';
+    public $Version = '5.2.23';
 
     /**
      * Email priority.
@@ -201,6 +201,9 @@ class PHPMailer
     /**
      * An ID to be used in the Message-ID header.
      * If empty, a unique id will be generated.
+     * You can set your own, but it must be in the format "<id@domain>",
+     * as defined in RFC5322 section 3.6.4 or it will be ignored.
+     * @see https://tools.ietf.org/html/rfc5322#section-3.6.4
      * @var string
      */
     public $MessageID = '';
@@ -420,6 +423,13 @@ class PHPMailer
      */
     public $DKIM_private = '';
 
+    /**
+     * DKIM private key string.
+     * If set, takes precedence over `$DKIM_private`.
+     * @var string
+     */
+    public $DKIM_private_string = '';
+
     /**
      * Callback Action function name.
      *
@@ -681,16 +691,16 @@ class PHPMailer
         } else {
             $subject = $this->encodeHeader($this->secureHeader($subject));
         }
-        //Can't use additional_parameters in safe_mode
+
+        //Can't use additional_parameters in safe_mode, calling mail() with null params breaks
         //@link http://php.net/manual/en/function.mail.php
-        if (ini_get('safe_mode') or !$this->UseSendmailOptions) {
+        if (ini_get('safe_mode') or !$this->UseSendmailOptions or is_null($params)) {
             $result = @mail($to, $subject, $body, $header);
         } else {
             $result = @mail($to, $subject, $body, $header, $params);
         }
         return $result;
     }
-
     /**
      * Output debugging info via user-defined method.
      * Only generates output if SMTP debug output is enabled (@see SMTP::$do_debug).
@@ -1284,9 +1294,11 @@ class PHPMailer
 
             // Sign with DKIM if enabled
             if (!empty($this->DKIM_domain)
-                && !empty($this->DKIM_private)
                 && !empty($this->DKIM_selector)
-                && file_exists($this->DKIM_private)) {
+                && (!empty($this->DKIM_private_string)
+                   || (!empty($this->DKIM_private) && file_exists($this->DKIM_private))
+                )
+            ) {
                 $header_dkim = $this->DKIM_Add(
                     $this->MIMEHeader . $this->mailHeader,
                     $this->encodeHeader($this->secureHeader($this->Subject)),
@@ -1352,19 +1364,24 @@ class PHPMailer
      */
     protected function sendmailSend($header, $body)
     {
-        if ($this->Sender != '') {
+        // CVE-2016-10033, CVE-2016-10045: Don't pass -f if characters will be escaped.
+        if (!empty($this->Sender) and self::isShellSafe($this->Sender)) {
             if ($this->Mailer == 'qmail') {
-                $sendmail = sprintf('%s -f%s', escapeshellcmd($this->Sendmail), escapeshellarg($this->Sender));
+                $sendmailFmt = '%s -f%s';
             } else {
-                $sendmail = sprintf('%s -oi -f%s -t', escapeshellcmd($this->Sendmail), escapeshellarg($this->Sender));
+                $sendmailFmt = '%s -oi -f%s -t';
             }
         } else {
             if ($this->Mailer == 'qmail') {
-                $sendmail = sprintf('%s', escapeshellcmd($this->Sendmail));
+                $sendmailFmt = '%s';
             } else {
-                $sendmail = sprintf('%s -oi -t', escapeshellcmd($this->Sendmail));
+                $sendmailFmt = '%s -oi -t';
             }
         }
+
+        // TODO: If possible, this should be changed to escapeshellarg.  Needs thorough testing.
+        $sendmail = sprintf($sendmailFmt, escapeshellcmd($this->Sendmail), $this->Sender);
+
         if ($this->SingleTo) {
             foreach ($this->SingleToArray as $toAddr) {
                 if (!@$mail = popen($sendmail, 'w')) {
@@ -1410,6 +1427,40 @@ class PHPMailer
         return true;
     }
 
+    /**
+     * Fix CVE-2016-10033 and CVE-2016-10045 by disallowing potentially unsafe shell characters.
+     *
+     * Note that escapeshellarg and escapeshellcmd are inadequate for our purposes, especially on Windows.
+     * @param string $string The string to be validated
+     * @see https://github.com/PHPMailer/PHPMailer/issues/924 CVE-2016-10045 bug report
+     * @access protected
+     * @return boolean
+     */
+    protected static function isShellSafe($string)
+    {
+        // Future-proof
+        if (escapeshellcmd($string) !== $string
+            or !in_array(escapeshellarg($string), array("'$string'", "\"$string\""))
+        ) {
+            return false;
+        }
+
+        $length = strlen($string);
+
+        for ($i = 0; $i < $length; $i++) {
+            $c = $string[$i];
+
+            // All other characters have a special meaning in at least one common shell, including = and +.
+            // Full stop (.) has a special meaning in cmd.exe, but its impact should be negligible here.
+            // Note that this does permit non-Latin alphanumeric characters based on the current locale.
+            if (!ctype_alnum($c) && strpos('@_-.', $c) === false) {
+                return false;
+            }
+        }
+
+        return true;
+    }
+
     /**
      * Send mail using the PHP mail() function.
      * @param string $header The message headers
@@ -1429,10 +1480,13 @@ class PHPMailer
 
         $params = null;
         //This sets the SMTP envelope sender which gets turned into a return-path header by the receiver
-        if (!empty($this->Sender)) {
-            $params = sprintf('-f%s', $this->Sender);
+        if (!empty($this->Sender) and $this->validateAddress($this->Sender)) {
+            // CVE-2016-10033, CVE-2016-10045: Don't pass -f if characters will be escaped.
+            if (self::isShellSafe($this->Sender)) {
+                $params = sprintf('-f%s', $this->Sender);
+            }
         }
-        if ($this->Sender != '' and !ini_get('safe_mode')) {
+        if (!empty($this->Sender) and !ini_get('safe_mode') and $this->validateAddress($this->Sender)) {
             $old_from = ini_get('sendmail_from');
             ini_set('sendmail_from', $this->Sender);
         }
@@ -1486,10 +1540,10 @@ class PHPMailer
         if (!$this->smtpConnect($this->SMTPOptions)) {
             throw new phpmailerException($this->lang('smtp_connect_failed'), self::STOP_CRITICAL);
         }
-        if ('' == $this->Sender) {
-            $smtp_from = $this->From;
-        } else {
+        if (!empty($this->Sender) and $this->validateAddress($this->Sender)) {
             $smtp_from = $this->Sender;
+        } else {
+            $smtp_from = $this->From;
         }
         if (!$this->smtp->mail($smtp_from)) {
             $this->setError($this->lang('from_failed') . $smtp_from . ' : ' . implode(',', $this->smtp->getError()));
@@ -1681,6 +1735,19 @@ class PHPMailer
      */
     public function setLanguage($langcode = 'en', $lang_path = '')
     {
+        // Backwards compatibility for renamed language codes
+        $renamed_langcodes = array(
+            'br' => 'pt_br',
+            'cz' => 'cs',
+            'dk' => 'da',
+            'no' => 'nb',
+            'se' => 'sv',
+        );
+
+        if (isset($renamed_langcodes[$langcode])) {
+            $langcode = $renamed_langcodes[$langcode];
+        }
+
         // Define full set of translatable strings in English
         $PHPMAILER_LANG = array(
             'authenticate' => 'SMTP Error: Could not authenticate.',
@@ -1707,6 +1774,10 @@ class PHPMailer
             // Calculate an absolute path so it can work if CWD is not here
             $lang_path = dirname(__FILE__). DIRECTORY_SEPARATOR . 'language'. DIRECTORY_SEPARATOR;
         }
+        //Validate $langcode
+        if (!preg_match('/^[a-z]{2}(?:_[a-zA-Z]{2})?$/', $langcode)) {
+            $langcode = 'en';
+        }
         $foundlang = true;
         $lang_file = $lang_path . 'phpmailer.lang-' . $langcode . '.php';
         // There is no English translation file
@@ -2000,6 +2071,8 @@ class PHPMailer
             $result .= $this->headerLine('Subject', $this->encodeHeader($this->secureHeader($this->Subject)));
         }
 
+        // Only allow a custom message ID if it conforms to RFC 5322 section 3.6.4
+        // https://tools.ietf.org/html/rfc5322#section-3.6.4
         if ('' != $this->MessageID and preg_match('/^<.*@.*>$/', $this->MessageID)) {
             $this->lastMessageID = $this->MessageID;
         } else {
@@ -2105,6 +2178,14 @@ class PHPMailer
         return rtrim($this->MIMEHeader . $this->mailHeader, "\n\r") . self::CRLF . self::CRLF . $this->MIMEBody;
     }
 
+    /**
+     * Create unique ID
+     * @return string
+     */
+    protected function generateId() {
+        return md5(uniqid(time()));
+    }
+
     /**
      * Assemble the message body.
      * Returns an empty string on failure.
@@ -2116,7 +2197,7 @@ class PHPMailer
     {
         $body = '';
         //Create unique IDs and preset boundaries
-        $this->uniqueid = md5(uniqid(time()));
+        $this->uniqueid = $this->generateId();
         $this->boundary[1] = 'b1_' . $this->uniqueid;
         $this->boundary[2] = 'b2_' . $this->uniqueid;
         $this->boundary[3] = 'b3_' . $this->uniqueid;
@@ -2411,6 +2492,7 @@ class PHPMailer
 
     /**
      * Add an attachment from a path on the filesystem.
+     * Never use a user-supplied path to a file!
      * Returns false if the file could not be found or read.
      * @param string $path Path to the attachment.
      * @param string $name Overrides the attachment name.
@@ -2936,6 +3018,7 @@ class PHPMailer
      * displayed inline with the message, not just attached for download.
      * This is used in HTML messages that embed the images
      * the HTML refers to using the $cid value.
+     * Never use a user-supplied path to a file!
      * @param string $path Path to the attachment.
      * @param string $cid Content ID of the attachment; Use this to reference
      *        the content when using an embedded image in HTML.
@@ -3296,21 +3379,29 @@ class PHPMailer
     }
 
     /**
-     * Create a message from an HTML string.
-     * Automatically makes modifications for inline images and backgrounds
-     * and creates a plain-text version by converting the HTML.
-     * Overwrites any existing values in $this->Body and $this->AltBody
+     * Create a message body from an HTML string.
+     * Automatically inlines images and creates a plain-text version by converting the HTML,
+     * overwriting any existing values in Body and AltBody.
+     * Do not source $message content from user input!
+     * $basedir is prepended when handling relative URLs, e.g. <img src="/images/a.png"> and must not be empty
+     * will look for an image file in $basedir/images/a.png and convert it to inline.
+     * If you don't provide a $basedir, relative paths will be left untouched (and thus probably break in email)
+     * If you don't want to apply these transformations to your HTML, just set Body and AltBody directly.
      * @access public
      * @param string $message HTML message string
-     * @param string $basedir baseline directory for path
+     * @param string $basedir Absolute path to a base directory to prepend to relative paths to images
      * @param boolean|callable $advanced Whether to use the internal HTML to text converter
      *    or your own custom converter @see PHPMailer::html2text()
-     * @return string $message
+     * @return string $message The transformed message Body
      */
     public function msgHTML($message, $basedir = '', $advanced = false)
     {
         preg_match_all('/(src|background)=["\'](.*)["\']/Ui', $message, $images);
         if (array_key_exists(2, $images)) {
+            if (strlen($basedir) > 1 && substr($basedir, -1) != '/') {
+                // Ensure $basedir has a trailing /
+                $basedir .= '/';
+            }
             foreach ($images[2] as $imgindex => $url) {
                 // Convert data URIs into embedded images
                 if (preg_match('#^data:(image[^;,]*)(;base64)?,#', $url, $match)) {
@@ -3328,18 +3419,24 @@ class PHPMailer
                             $message
                         );
                     }
-                } elseif (substr($url, 0, 4) !== 'cid:' && !preg_match('#^[a-z][a-z0-9+.-]*://#i', $url)) {
-                    // Do not change urls for absolute images (thanks to corvuscorax)
+                    continue;
+                }
+                if (
+                    // Only process relative URLs if a basedir is provided (i.e. no absolute local paths)
+                    !empty($basedir)
+                    // Ignore URLs containing parent dir traversal (..)
+                    && (strpos($url, '..') === false)
                     // Do not change urls that are already inline images
+                    && substr($url, 0, 4) !== 'cid:'
+                    // Do not change absolute URLs, including anonymous protocol
+                    && !preg_match('#^[a-z][a-z0-9+.-]*:?//#i', $url)
+                ) {
                     $filename = basename($url);
                     $directory = dirname($url);
                     if ($directory == '.') {
                         $directory = '';
                     }
                     $cid = md5($url) . '@phpmailer.0'; // RFC2392 S 2
-                    if (strlen($basedir) > 1 && substr($basedir, -1) != '/') {
-                        $basedir .= '/';
-                    }
                     if (strlen($directory) > 1 && substr($directory, -1) != '/') {
                         $directory .= '/';
                     }
@@ -3375,7 +3472,7 @@ class PHPMailer
      * Convert an HTML string into plain text.
      * This is used by msgHTML().
      * Note - older versions of this function used a bundled advanced converter
-     * which was been removed for license reasons in #232
+     * which was been removed for license reasons in #232.
      * Example usage:
      * <code>
      * // Use default conversion
@@ -3675,7 +3772,7 @@ class PHPMailer
      * @access public
      * @param string $signHeader
      * @throws phpmailerException
-     * @return string
+     * @return string The DKIM signature value
      */
     public function DKIM_Sign($signHeader)
     {
@@ -3685,15 +3782,33 @@ class PHPMailer
             }
             return '';
         }
-        $privKeyStr = file_get_contents($this->DKIM_private);
-        if ($this->DKIM_passphrase != '') {
+        $privKeyStr = !empty($this->DKIM_private_string) ? $this->DKIM_private_string : file_get_contents($this->DKIM_private);
+        if ('' != $this->DKIM_passphrase) {
             $privKey = openssl_pkey_get_private($privKeyStr, $this->DKIM_passphrase);
         } else {
             $privKey = openssl_pkey_get_private($privKeyStr);
         }
-        if (openssl_sign($signHeader, $signature, $privKey, 'sha256WithRSAEncryption')) { //sha1WithRSAEncryption
-            openssl_pkey_free($privKey);
-            return base64_encode($signature);
+        //Workaround for missing digest algorithms in old PHP & OpenSSL versions
+        //@link http://stackoverflow.com/a/11117338/333340
+        if (version_compare(PHP_VERSION, '5.3.0') >= 0 and
+            in_array('sha256WithRSAEncryption', openssl_get_md_methods(true))) {
+            if (openssl_sign($signHeader, $signature, $privKey, 'sha256WithRSAEncryption')) {
+                openssl_pkey_free($privKey);
+                return base64_encode($signature);
+            }
+        } else {
+            $pinfo = openssl_pkey_get_details($privKey);
+            $hash = hash('sha256', $signHeader);
+            //'Magic' constant for SHA256 from RFC3447
+            //@link https://tools.ietf.org/html/rfc3447#page-43
+            $t = '3031300d060960864801650304020105000420' . $hash;
+            $pslen = $pinfo['bits'] / 8 - (strlen($t) / 2 + 3);
+            $eb = pack('H*', '0001' . str_repeat('FF', $pslen) . '00' . $t);
+
+            if (openssl_private_encrypt($eb, $signature, $privKey, OPENSSL_NO_PADDING)) {
+                openssl_pkey_free($privKey);
+                return base64_encode($signature);
+            }
         }
         openssl_pkey_free($privKey);
         return '';
index 0c016f1..01cee82 100644 (file)
@@ -30,7 +30,7 @@ class SMTP
      * The PHPMailer SMTP version number.
      * @var string
      */
-    const VERSION = '5.2.16';
+    const VERSION = '5.2.23';
 
     /**
      * SMTP line break constant.
@@ -81,7 +81,7 @@ class SMTP
      * @deprecated Use the `VERSION` constant instead
      * @see SMTP::VERSION
      */
-    public $Version = '5.2.16';
+    public $Version = '5.2.23';
 
     /**
      * SMTP server port number.
@@ -150,6 +150,17 @@ class SMTP
      */
     public $Timelimit = 300;
 
+    /**
+     * @var array patterns to extract smtp transaction id from smtp reply
+     * Only first capture group will be use, use non-capturing group to deal with it
+     * Extend this class to override this property to fulfil your needs.
+     */
+    protected $smtp_transaction_id_patterns = array(
+        'exim' => '/[0-9]{3} OK id=(.*)/',
+        'sendmail' => '/[0-9]{3} 2.0.0 (.*) Message/',
+        'postfix' => '/[0-9]{3} 2.0.0 Ok: queued as (.*)/'
+    );
+
     /**
      * The socket for the server connection.
      * @var resource
@@ -206,7 +217,7 @@ class SMTP
         }
         //Avoid clash with built-in function names
         if (!in_array($this->Debugoutput, array('error_log', 'html', 'echo')) and is_callable($this->Debugoutput)) {
-            call_user_func($this->Debugoutput, $str, $this->do_debug);
+            call_user_func($this->Debugoutput, $str, $level);
             return;
         }
         switch ($this->Debugoutput) {
@@ -220,8 +231,7 @@ class SMTP
                     preg_replace('/[\r\n]+/', '', $str),
                     ENT_QUOTES,
                     'UTF-8'
-                )
-                . "<br>\n";
+                ) . "<br>\n";
                 break;
             case 'echo':
             default:
@@ -231,7 +241,7 @@ class SMTP
                     "\n",
                     "\n                   \t                  ",
                     trim($str)
-                )."\n";
+                ) . "\n";
         }
     }
 
@@ -265,15 +275,16 @@ class SMTP
         }
         // Connect to the SMTP server
         $this->edebug(
-            "Connection: opening to $host:$port, timeout=$timeout, options=".var_export($options, true),
+            "Connection: opening to $host:$port, timeout=$timeout, options=" .
+            var_export($options, true),
             self::DEBUG_CONNECTION
         );
         $errno = 0;
         $errstr = '';
         if ($streamok) {
             $socket_context = stream_context_create($options);
-            //Suppress errors; connection failures are handled at a higher level
-            $this->smtp_conn = @stream_socket_client(
+            set_error_handler(array($this, 'errorHandler'));
+            $this->smtp_conn = stream_socket_client(
                 $host . ":" . $port,
                 $errno,
                 $errstr,
@@ -281,12 +292,14 @@ class SMTP
                 STREAM_CLIENT_CONNECT,
                 $socket_context
             );
+            restore_error_handler();
         } else {
             //Fall back to fsockopen which should work in more places, but is missing some features
             $this->edebug(
                 "Connection: stream_socket_client not available, falling back to fsockopen",
                 self::DEBUG_CONNECTION
             );
+            set_error_handler(array($this, 'errorHandler'));
             $this->smtp_conn = fsockopen(
                 $host,
                 $port,
@@ -294,6 +307,7 @@ class SMTP
                 $errstr,
                 $timeout
             );
+            restore_error_handler();
         }
         // Verify we connected properly
         if (!is_resource($this->smtp_conn)) {
@@ -348,14 +362,14 @@ class SMTP
         }
 
         // Begin encrypted connection
-        if (!stream_socket_enable_crypto(
+        set_error_handler(array($this, 'errorHandler'));
+        $crypto_ok = stream_socket_enable_crypto(
             $this->smtp_conn,
             true,
             $crypto_method
-        )) {
-            return false;
-        }
-        return true;
+        );
+        restore_error_handler();
+        return $crypto_ok;
     }
 
     /**
@@ -384,8 +398,7 @@ class SMTP
         }
 
         if (array_key_exists('EHLO', $this->server_caps)) {
-        // SMTP extensions are available. Let's try to find a proper authentication method
-
+            // SMTP extensions are available; try to find a proper authentication method
             if (!array_key_exists('AUTH', $this->server_caps)) {
                 $this->setError('Authentication is not allowed at this stage');
                 // 'at this stage' means that auth may be allowed after the stage changes
@@ -410,7 +423,7 @@ class SMTP
                     $this->setError('No supported authentication methods found');
                     return false;
                 }
-                self::edebug('Auth method selected: '.$authtype, self::DEBUG_LOWLEVEL);
+                self::edebug('Auth method selected: ' . $authtype, self::DEBUG_LOWLEVEL);
             }
 
             if (!in_array($authtype, $this->server_caps['AUTH'])) {
@@ -474,7 +487,7 @@ class SMTP
                 $temp = new stdClass;
                 $ntlm_client = new ntlm_sasl_client_class;
                 //Check that functions are available
-                if (!$ntlm_client->Initialize($temp)) {
+                if (!$ntlm_client->initialize($temp)) {
                     $this->setError($temp->error);
                     $this->edebug(
                         'You need to enable some modules in your php.ini file: '
@@ -484,7 +497,7 @@ class SMTP
                     return false;
                 }
                 //msg1
-                $msg1 = $ntlm_client->TypeMsg1($realm, $workstation); //msg1
+                $msg1 = $ntlm_client->typeMsg1($realm, $workstation); //msg1
 
                 if (!$this->sendCommand(
                     'AUTH NTLM',
@@ -503,7 +516,7 @@ class SMTP
                     $password
                 );
                 //msg3
-                $msg3 = $ntlm_client->TypeMsg3(
+                $msg3 = $ntlm_client->typeMsg3(
                     $ntlm_res,
                     $username,
                     $realm,
@@ -536,7 +549,7 @@ class SMTP
      * Works like hash_hmac('md5', $data, $key)
      * in case that function is not available
      * @param string $data The data to hash
-     * @param string $key  The key to hash with
+     * @param string $key The key to hash with
      * @access protected
      * @return string
      */
@@ -879,7 +892,8 @@ class SMTP
             $code_ex = (count($matches) > 2 ? $matches[2] : null);
             // Cut off error code from each response line
             $detail = preg_replace(
-                "/{$code}[ -]".($code_ex ? str_replace('.', '\\.', $code_ex).' ' : '')."/m",
+                "/{$code}[ -]" .
+                ($code_ex ? str_replace('.', '\\.', $code_ex) . ' ' : '') . "/m",
                 '',
                 $this->last_reply
             );
@@ -1091,7 +1105,7 @@ class SMTP
             // Now check if reads took too long
             if ($endtime and time() > $endtime) {
                 $this->edebug(
-                    'SMTP -> get_lines(): timelimit reached ('.
+                    'SMTP -> get_lines(): timelimit reached (' .
                     $this->Timelimit . ' sec)',
                     self::DEBUG_LOWLEVEL
                 );
@@ -1189,4 +1203,49 @@ class SMTP
     {
         return $this->Timeout;
     }
+
+    /**
+     * Reports an error number and string.
+     * @param integer $errno The error number returned by PHP.
+     * @param string $errmsg The error message returned by PHP.
+     * @param string $errfile The file the error occurred in
+     * @param integer $errline The line number the error occurred on
+     */
+    protected function errorHandler($errno, $errmsg, $errfile = '', $errline = 0)
+    {
+        $notice = 'Connection failed.';
+        $this->setError(
+            $notice,
+            $errno,
+            $errmsg
+        );
+        $this->edebug(
+            $notice . ' Error #' . $errno . ': ' . $errmsg . " [$errfile line $errline]",
+            self::DEBUG_CONNECTION
+        );
+    }
+
+    /**
+     * Will return the ID of the last smtp transaction based on a list of patterns provided
+     * in SMTP::$smtp_transaction_id_patterns.
+     * If no reply has been received yet, it will return null.
+     * If no pattern has been matched, it will return false.
+     * @return bool|null|string
+     */
+    public function getLastTransactionID()
+    {
+        $reply = $this->getLastReply();
+
+        if (empty($reply)) {
+            return null;
+        }
+
+        foreach ($this->smtp_transaction_id_patterns as $smtp_transaction_id_pattern) {
+            if (preg_match($smtp_transaction_id_pattern, $reply, $matches)) {
+                return $matches[1];
+            }
+        }
+
+        return false;
+    }
 }
index 43057ef..aa987a9 100644 (file)
@@ -4,22 +4,22 @@
  * @package PHPMailer
  */
 
-$PHPMAILER_LANG['authenticate']         = 'SMTP Fehler: Authentifizierung fehlgeschlagen.';
-$PHPMAILER_LANG['connect_host']         = 'SMTP Fehler: Konnte keine Verbindung zum SMTP-Host herstellen.';
-$PHPMAILER_LANG['data_not_accepted']    = 'SMTP Fehler: Daten werden nicht akzeptiert.';
-$PHPMAILER_LANG['empty_message']        = 'E-Mail Inhalt ist leer.';
-$PHPMAILER_LANG['encoding']             = 'Unbekanntes Encoding-Format: ';
+$PHPMAILER_LANG['authenticate']         = 'SMTP-Fehler: Authentifizierung fehlgeschlagen.';
+$PHPMAILER_LANG['connect_host']         = 'SMTP-Fehler: Konnte keine Verbindung zum SMTP-Host herstellen.';
+$PHPMAILER_LANG['data_not_accepted']    = 'SMTP-Fehler: Daten werden nicht akzeptiert.';
+$PHPMAILER_LANG['empty_message']        = 'E-Mail-Inhalt ist leer.';
+$PHPMAILER_LANG['encoding']             = 'Unbekannte Kodierung: ';
 $PHPMAILER_LANG['execute']              = 'Konnte folgenden Befehl nicht ausführen: ';
 $PHPMAILER_LANG['file_access']          = 'Zugriff auf folgende Datei fehlgeschlagen: ';
-$PHPMAILER_LANG['file_open']            = 'Datei Fehler: konnte folgende Datei nicht öffnen: ';
+$PHPMAILER_LANG['file_open']            = 'Dateifehler: Konnte folgende Datei nicht öffnen: ';
 $PHPMAILER_LANG['from_failed']          = 'Die folgende Absenderadresse ist nicht korrekt: ';
-$PHPMAILER_LANG['instantiate']          = 'Mail Funktion konnte nicht initialisiert werden.';
-$PHPMAILER_LANG['invalid_address']      = 'E-Mail wird nicht gesendet, die Adresse ist ungültig: ';
+$PHPMAILER_LANG['instantiate']          = 'Mail-Funktion konnte nicht initialisiert werden.';
+$PHPMAILER_LANG['invalid_address']      = 'Die Adresse ist ungültig: ';
 $PHPMAILER_LANG['mailer_not_supported'] = ' mailer wird nicht unterstützt.';
-$PHPMAILER_LANG['provide_address']      = 'Bitte geben Sie mindestens eine Empfänger E-Mailadresse an.';
-$PHPMAILER_LANG['recipients_failed']    = 'SMTP Fehler: Die folgenden Empfänger sind nicht korrekt: ';
+$PHPMAILER_LANG['provide_address']      = 'Bitte geben Sie mindestens eine Empfängeradresse an.';
+$PHPMAILER_LANG['recipients_failed']    = 'SMTP-Fehler: Die folgenden Empfänger sind nicht korrekt: ';
 $PHPMAILER_LANG['signing']              = 'Fehler beim Signieren: ';
-$PHPMAILER_LANG['smtp_connect_failed']  = 'Verbindung zu SMTP Server fehlgeschlagen.';
-$PHPMAILER_LANG['smtp_error']           = 'Fehler vom SMTP Server: ';
+$PHPMAILER_LANG['smtp_connect_failed']  = 'Verbindung zum SMTP-Server fehlgeschlagen.';
+$PHPMAILER_LANG['smtp_error']           = 'Fehler vom SMTP-Server: ';
 $PHPMAILER_LANG['variable_set']         = 'Kann Variable nicht setzen oder zurücksetzen: ';
-//$PHPMAILER_LANG['extension_missing']    = 'Extension missing: ';
+$PHPMAILER_LANG['extension_missing']    = 'Fehlende Erweiterung: ';
index 5ef716e..d2eac8d 100644 (file)
@@ -23,4 +23,4 @@ $PHPMAILER_LANG['signing']              = 'Error al firmar: ';
 $PHPMAILER_LANG['smtp_connect_failed']  = 'SMTP Connect() falló.';
 $PHPMAILER_LANG['smtp_error']           = 'Error del servidor SMTP: ';
 $PHPMAILER_LANG['variable_set']         = 'No se pudo configurar la variable: ';
-//$PHPMAILER_LANG['extension_missing']    = 'Extension missing: ';
+$PHPMAILER_LANG['extension_missing']    = 'Extensión faltante: ';
index cf37cc1..fa100ea 100644 (file)
@@ -2,25 +2,25 @@
 /**
  * Romanian PHPMailer language file: refer to English translation for definitive list
  * @package PHPMailer
- * @author Catalin Constantin <catalin@dazoot.ro>
+ * @author Alex Florea <alecz.fia@gmail.com>
  */
 
-$PHPMAILER_LANG['authenticate']         = 'Eroare SMTP: Nu a functionat autentificarea.';
-$PHPMAILER_LANG['connect_host']         = 'Eroare SMTP: Nu m-am putut conecta la adresa SMTP.';
-$PHPMAILER_LANG['data_not_accepted']    = 'Eroare SMTP: Continutul mailului nu a fost acceptat.';
+$PHPMAILER_LANG['authenticate']         = 'Eroare SMTP: Autentificarea a eșuat.';
+$PHPMAILER_LANG['connect_host']         = 'Eroare SMTP: Conectarea la serverul SMTP a eșuat.';
+$PHPMAILER_LANG['data_not_accepted']    = 'Eroare SMTP: Datele nu au fost acceptate.';
 $PHPMAILER_LANG['empty_message']        = 'Mesajul este gol.';
-$PHPMAILER_LANG['encoding']             = 'Encodare necunoscuta: ';
-$PHPMAILER_LANG['execute']              = 'Nu pot executa:  ';
-$PHPMAILER_LANG['file_access']          = 'Nu pot accesa fisierul: ';
-$PHPMAILER_LANG['file_open']            = 'Eroare de fisier: Nu pot deschide fisierul: ';
-$PHPMAILER_LANG['from_failed']          = 'Urmatoarele adrese From au dat eroare: ';
-$PHPMAILER_LANG['instantiate']          = 'Nu am putut instantia functia mail.';
-$PHPMAILER_LANG['invalid_address']      = 'Adresa de email nu este valida: ';
+$PHPMAILER_LANG['encoding']             = 'Encodare necunoscută: ';
+$PHPMAILER_LANG['execute']              = 'Nu se poate executa următoarea comandă:  ';
+$PHPMAILER_LANG['file_access']          = 'Nu se poate accesa următorul fișier: ';
+$PHPMAILER_LANG['file_open']            = 'Eroare fișier: Nu se poate deschide următorul fișier: ';
+$PHPMAILER_LANG['from_failed']          = 'Următoarele adrese From au dat eroare: ';
+$PHPMAILER_LANG['instantiate']          = 'Funcția mail nu a putut fi inițializată.';
+$PHPMAILER_LANG['invalid_address']      = 'Adresa de email nu este validă: ';
 $PHPMAILER_LANG['mailer_not_supported'] = ' mailer nu este suportat.';
-$PHPMAILER_LANG['provide_address']      = 'Trebuie sa adaugati cel putin un recipient (adresa de mail).';
-$PHPMAILER_LANG['recipients_failed']    = 'Eroare SMTP: Urmatoarele adrese de mail au dat eroare: ';
-$PHPMAILER_LANG['signing']              = 'A aparut o problema la semnarea emailului. ';
-$PHPMAILER_LANG['smtp_connect_failed']  = 'Conectarea la serverul SMTP a esuat.';
-$PHPMAILER_LANG['smtp_error']           = 'A aparut o eroare la serverul SMTP. ';
+$PHPMAILER_LANG['provide_address']      = 'Trebuie să adăugați cel puțin o adresă de email.';
+$PHPMAILER_LANG['recipients_failed']    = 'Eroare SMTP: Următoarele adrese de email au eșuat: ';
+$PHPMAILER_LANG['signing']              = 'A aparut o problemă la semnarea emailului. ';
+$PHPMAILER_LANG['smtp_connect_failed']  = 'Conectarea la serverul SMTP a eșuat.';
+$PHPMAILER_LANG['smtp_error']           = 'Eroare server SMTP: ';
 $PHPMAILER_LANG['variable_set']         = 'Nu se poate seta/reseta variabila. ';
-//$PHPMAILER_LANG['extension_missing']    = 'Extension missing: ';
+$PHPMAILER_LANG['extension_missing']    = 'Lipsește extensia: ';
index 1393cd4..440eb4b 100644 (file)
@@ -38,6 +38,7 @@ define("TOKEN_USERID","5");
 define("TOKEN_DATEFROM","6");
 define("TOKEN_DATETO","7");
 define("TOKEN_INSTANCE","8");
+define("TOKEN_TAGS","9");
 
 /**
  * Class to hold token/value pairs after they're parsed.
@@ -110,6 +111,14 @@ class search_lexer extends Lexer{
     $this->addExitPattern("\s","indatefrom");
 
 
+    // If we see the string tags: while in the base accept state, start
+    // parsing tags and go to the intags state.
+    $this->addEntryPattern("tags:\S+","accept","intags");
+
+    // Snarf everything into the tags until we see whitespace, then exit
+    // back to the base accept state.
+    $this->addExitPattern("\s","intags");
+
     // Patterns to handle strings  of the form dateto:foo
 
     // If we see the string dateto: while in the base accept state, start
@@ -268,6 +277,17 @@ class search_parser {
         return true;
     }
 
+    // State for handling tags:tagname,tagname constructs. Potentially emits a token.
+    function intags($content){
+        if (strlen($content) < 5) { // State exit or missing parameter.
+            return true;
+        }
+        // Strip off the tags: part and add the reminder to the parsed token array
+        $param = trim(substr($content,5));
+        $this->tokens[] = new search_token(TOKEN_TAGS,$param);
+        return true;
+    }
+
     // State for handling instance:foo constructs. Potentially emits a token.
     function ininstance($content){
         if (strlen($content) < 10) { // State exit or missing parameter.
@@ -390,7 +410,8 @@ function search_generate_text_SQL($parsetree, $datafield, $metafield, $mainidfie
  * @global object
  */
 function search_generate_SQL($parsetree, $datafield, $metafield, $mainidfield, $useridfield,
-                             $userfirstnamefield, $userlastnamefield, $timefield, $instancefield) {
+                             $userfirstnamefield, $userlastnamefield, $timefield, $instancefield,
+                             $tagfields = []) {
     global $CFG, $DB;
     static $p = 0;
 
@@ -407,7 +428,7 @@ function search_generate_SQL($parsetree, $datafield, $metafield, $mainidfield, $
     }
 
     $SQLString = '';
-
+    $nexttagfield = 0;
     for ($i=0; $i<$ntokens; $i++){
         if ($i > 0) {// We have more than one clause, need to tack on AND
             $SQLString .= ' AND ';
@@ -465,6 +486,22 @@ function search_generate_SQL($parsetree, $datafield, $metafield, $mainidfield, $
                 $SQLString .= "($timefield >= :$name1)";
                 $params[$name1] =  $value;
                 break;
+            case TOKEN_TAGS:
+                $sqlstrings = [];
+                foreach (explode(',', $value) as $tag) {
+                    $paramname = $name1 . '_' . $nexttagfield;
+                    if (isset($tagfields[$nexttagfield])) {
+                        $sqlstrings[]       = "($tagfields[$nexttagfield] = :$paramname)";
+                        $params[$paramname] = $tag;
+                    } else if (!isset($tagfields[$nexttagfield]) && !isset($stoppedprocessingtags)) {
+                        // Show a debugging message the first time we hit this.
+                        $stoppedprocessingtags = true;
+                        \core\notification::add(get_string('toomanytags'), \core\notification::WARNING);
+                    }
+                    $nexttagfield++;
+                }
+                $SQLString .= implode(' AND ', $sqlstrings);
+                break;
             case TOKEN_NEGATE:
                 $SQLString .= "(NOT ((".$DB->sql_like($datafield, ":$name1", false).") OR (".$DB->sql_like($metafield, ":$name2", false).")))";
                 $params[$name1] =  "%$value%";
index d6314df..d2a1652 100644 (file)
      "action": "prevent",
      "spanclass": "allowed",
      "linkclass": "preventlink",
-     "adminurl" : "http://localhost/moodle/admin/",
-     "imageurl": "http://localhost/moodle/theme/image.php?theme=base&component=core&image=t%2Fdelete"}
+     "adminurl" : "http://localhost/moodle/admin/"}
 }}
 <span style="display:inline-block;" class="{{spanclass}}">&nbsp;{{rolename}}&nbsp;
-    {{#imageurl}}
-        <a href="{{adminurl}}roles/permissions.php" class="{{linkclass}}" data-role-id="{{roleid}}" data-action="{{action}}">
-            {{#icon}}
-                {{#pix}}{{icon}}, 'core', {{iconalt}}{{/pix}}
-            {{/icon}}
-        </a>
-    {{/imageurl}}
+    <a href="{{adminurl}}roles/permissions.php" class="{{linkclass}}" data-role-id="{{roleid}}" data-action="{{action}}">
+        {{#icon}}
+            {{#pix}}{{icon}}, core, {{iconalt}}{{/pix}}
+        {{/icon}}
+    </a>
 </span>
index 5086082..4e1324c 100644 (file)
@@ -1194,6 +1194,26 @@ class core_moodlelib_testcase extends advanced_testcase {
         }
     }
 
+    public function test_set_user_preference_for_current_user() {
+        global $USER;
+        $this->resetAfterTest();
+        $this->setAdminUser();
+
+        set_user_preference('test_pref', 2);
+        set_user_preference('test_pref', 1, $USER->id);
+        $this->assertEquals(1, get_user_preferences('test_pref'));
+    }
+
+    public function test_unset_user_preference_for_current_user() {
+        global $USER;
+        $this->resetAfterTest();
+        $this->setAdminUser();
+
+        set_user_preference('test_pref', 1);
+        unset_user_preference('test_pref', $USER->id);
+        $this->assertNull(get_user_preferences('test_pref'));
+    }
+
     public function test_get_extra_user_fields() {
         global $CFG, $USER, $DB;
         $this->resetAfterTest();
index 4185b18..07a0923 100644 (file)
@@ -53,7 +53,7 @@
     <location>jabber</location>
     <name>XMPPHP</name>
     <license>GPL</license>
-    <version></version>
+    <version>0.1rc2-r77</version>
     <licenseversion>2.0+</licenseversion>
   </library>
   <library>
     <location>phpmailer</location>
     <name>PHPMailer</name>
     <license>LGPL</license>
-    <version>5.2.16</version>
+    <version>5.2.23</version>
     <licenseversion>2.1</licenseversion>
   </library>
   <library>
index 147816f..10eaa29 100644 (file)
@@ -2650,7 +2650,7 @@ function mdie($msg='', $errorcode=1) {
  * Print a message and exit.
  *
  * @param string $message The message to print in the notice
- * @param string $link The link to use for the continue button
+ * @param moodle_url|string $link The link to use for the continue button
  * @param object $course A course object. Unused.
  * @return void This function simply exits
  */
index a3c77c9..c8bc5cf 100644 (file)
@@ -58,7 +58,7 @@ class assign_feedback_editpdf extends assign_feedback_plugin {
      */
     public function get_widget($userid, $grade, $readonly) {
         $attempt = -1;
-        if ($grade) {
+        if ($grade && isset($grade->attemptnumber)) {
             $attempt = $grade->attemptnumber;
         } else {
             $grade = $this->assignment->get_user_grade($userid, true);
index 048f291..8016f7a 100644 (file)
@@ -491,7 +491,7 @@ function assign_page_type_list($pagetype, $parentcontext, $currentcontext) {
  * for the courses.
  *
  * @deprecated since 3.3
- *
+ * @todo The final deprecation of this function will take place in Moodle 3.7 - see MDL-57487.
  * @param mixed $courses The list of courses to print the overview for
  * @param array $htmlarray The array of html to return
  * @return true
@@ -634,7 +634,7 @@ function assign_print_overview($courses, &$htmlarray) {
  * assignment.
  *
  * @deprecated since 3.3
- *
+ * @todo The final deprecation of this function will take place in Moodle 3.7 - see MDL-57487.
  * @param array $mysubmissions list of submissions of current user indexed by assignment id.
  * @param string $sqlassignmentids sql clause used to filter open assignments.
  * @param array $assignmentidparams sql params used to filter open assignments.
@@ -724,7 +724,7 @@ function assign_get_mysubmission_details_for_print_overview(&$mysubmissions, $sq
  * assignment's submissions.
  *
  * @deprecated since 3.3
- *
+ * @todo The final deprecation of this function will take place in Moodle 3.7 - see MDL-57487.
  * @param array $unmarkedsubmissions list of submissions of that are currently unmarked indexed by assignment id.
  * @param string $sqlassignmentids sql clause used to filter open assignments.
  * @param array $assignmentidparams sql params used to filter open assignments.
@@ -1775,7 +1775,10 @@ function mod_assign_core_calendar_is_event_visible(calendar_event $event) {
 }
 
 /**
- * Handles creating actions for events.
+ * This function receives a calendar event and returns the action associated with it, or null if there is none.
+ *
+ * This is used by block_myoverview in order to display the event appropriately. If null is returned then the event
+ * is not displayed on the block.
  *
  * @param calendar_event $event
  * @param \core_calendar\action_factory $factory
index 5f1bf0d..381adb2 100644 (file)
@@ -23,6 +23,8 @@
  */
 
 
+$string['acceptedfiletypes'] = 'Accepted file types';
+$string['acceptedfiletypes_help'] = 'Accepted file types can be restricted by entering a semicolon-separated list of mimetypes, for example \'video/mp4; audio/mp3; image/png; image/jpeg\'. You may also limit to extensions by including the dot, for example \'.png; .jpg\' If the field is left empty, then all file types are allowed.';
 $string['configmaxbytes'] = 'Maximum file size';
 $string['countfiles'] = '{$a} files';
 $string['default'] = 'Enabled by default';
@@ -31,6 +33,8 @@ $string['enabled'] = 'File submissions';
 $string['enabled_help'] = 'If enabled, students are able to upload one or more files as their submission.';
 $string['eventassessableuploaded'] = 'A file has been uploaded.';
 $string['file'] = 'File submissions';
+$string['filesofthesetypes'] = 'Files of these types may be added to the submission:';
+$string['filetypewithexts'] = '{$a->name} &mdash; {$a->extlist}';
 $string['maxbytes'] = 'Maximum file size';
 $string['maxfiles'] = 'Maximum files per submission';
 $string['maxfiles_help'] = 'If file submissions are enabled, each assignment can be set to accept up to this number of files for their submission.';
@@ -38,6 +42,7 @@ $string['maxfilessubmission'] = 'Maximum number of uploaded files';
 $string['maxfilessubmission_help'] = 'If file submissions are enabled, each student will be able to upload up to this number of files for their submission.';
 $string['maximumsubmissionsize'] = 'Maximum submission size';
 $string['maximumsubmissionsize_help'] = 'Files uploaded by students may be up to this size.';
+$string['nonexistentfiletypes'] = 'The following file types were not recognised: {$a}';
 $string['numfilesforlog'] = 'The number of file(s) : {$a} file(s).';
 $string['pluginname'] = 'File submissions';
 $string['siteuploadlimit'] = 'Site upload limit';
index cd54142..b0816c3 100644 (file)
@@ -71,6 +71,7 @@ class assign_submission_file extends assign_submission_plugin {
 
         $defaultmaxfilesubmissions = $this->get_config('maxfilesubmissions');
         $defaultmaxsubmissionsizebytes = $this->get_config('maxsubmissionsizebytes');
+        $defaultfiletypes = (string)$this->get_config('filetypeslist');
 
         $settings = array();
         $options = array();
@@ -105,6 +106,25 @@ class assign_submission_file extends assign_submission_plugin {
         $mform->disabledIf('assignsubmission_file_maxsizebytes',
                            'assignsubmission_file_enabled',
                            'notchecked');
+
+        $name = get_string('acceptedfiletypes', 'assignsubmission_file');
+        $mform->addElement('text', 'assignsubmission_file_filetypes', $name);
+        $mform->addHelpButton('assignsubmission_file_filetypes', 'acceptedfiletypes', 'assignsubmission_file');
+        $mform->setType('assignsubmission_file_filetypes', PARAM_RAW);
+        $mform->setDefault('assignsubmission_file_filetypes', $defaultfiletypes);
+        $mform->disabledIf('assignsubmission_file_filetypes', 'assignsubmission_file_enabled', 'notchecked');
+        $mform->addFormRule(function ($values, $files) {
+            if (empty($values['assignsubmission_file_filetypes'])) {
+                return true;
+            }
+            $nonexistent = $this->get_nonexistent_file_types($values['assignsubmission_file_filetypes']);
+            if (empty($nonexistent)) {
+                return true;
+            } else {
+                $a = join(' ', $nonexistent);
+                return ["assignsubmission_file_filetypes" => get_string('nonexistentfiletypes', 'assignsubmission_file', $a)];
+            }
+        });
     }
 
     /**
@@ -116,6 +136,13 @@ class assign_submission_file extends assign_submission_plugin {
     public function save_settings(stdClass $data) {
         $this->set_config('maxfilesubmissions', $data->assignsubmission_file_maxfiles);
         $this->set_config('maxsubmissionsizebytes', $data->assignsubmission_file_maxsizebytes);
+
+        if (!empty($data->assignsubmission_file_filetypes)) {
+            $this->set_config('filetypeslist', $data->assignsubmission_file_filetypes);
+        } else {
+            $this->set_config('filetypeslist', '');
+        }
+
         return true;
     }
 
@@ -128,7 +155,7 @@ class assign_submission_file extends assign_submission_plugin {
         $fileoptions = array('subdirs' => 1,
                                 'maxbytes' => $this->get_config('maxsubmissionsizebytes'),
                                 'maxfiles' => $this->get_config('maxfilesubmissions'),
-                                'accepted_types' => '*',
+                                'accepted_types' => $this->get_accepted_types(),
                                 'return_types' => (FILE_INTERNAL | FILE_CONTROLLED_LINK));
         if ($fileoptions['maxbytes'] == 0) {
             // Use module default.
@@ -163,6 +190,34 @@ class assign_submission_file extends assign_submission_plugin {
                                                   $submissionid);
         $mform->addElement('filemanager', 'files_filemanager', $this->get_name(), null, $fileoptions);
 
+        if (!empty($this->get_config('filetypeslist'))) {
+            $text = html_writer::tag('p', get_string('filesofthesetypes', 'assignsubmission_file'));
+            $text .= html_writer::start_tag('ul');
+
+            $typesets = $this->get_configured_typesets();
+            foreach ($typesets as $type) {
+                $a = new stdClass();
+                $extensions = file_get_typegroup('extension', $type);
+                $typetext = html_writer::tag('li', $type);
+                // Only bother checking if it's a mimetype or group if it has extensions in the group.
+                if (!empty($extensions)) {
+                    if (strpos($type, '/') !== false) {
+                        $a->name = get_mimetype_description($type);
+                        $a->extlist = implode(' ', $extensions);
+                        $typetext = html_writer::tag('li', get_string('filetypewithexts', 'assignsubmission_file', $a));
+                    } else if (get_string_manager()->string_exists("group:$type", 'mimetypes')) {
+                        $a->name = get_string("group:$type", 'mimetypes');
+                        $a->extlist = implode(' ', $extensions);
+                        $typetext = html_writer::tag('li', get_string('filetypewithexts', 'assignsubmission_file', $a));
+                    }
+                }
+                $text .= $typetext;
+            }
+
+            $text .= html_writer::end_tag('ul');
+            $mform->addElement('static', '', '', $text);
+        }
+
         return true;
     }
 
@@ -570,4 +625,70 @@ class assign_submission_file extends assign_submission_plugin {
         }
         return (array) $configs;
     }
+
+    /**
+     * Get the type sets configured for this assignment.
+     *
+     * @return array('groupname', 'mime/type', ...)
+     */
+    private function get_configured_typesets() {
+        $typeslist = (string)$this->get_config('filetypeslist');
+
+        $sets = $this->get_typesets($typeslist);
+
+        return $sets;
+    }
+
+    /**
+     * Get the type sets passed.
+     *
+     * @param string $types The space , ; separated list of types
+     * @return array('groupname', 'mime/type', ...)
+     */
+    private function get_typesets($types) {
+        $sets = array();
+        if (!empty($types)) {
+            $sets = preg_split('/[\s,;:"\']+/', $types, null, PREG_SPLIT_NO_EMPTY);
+        }
+        return $sets;
+    }
+
+
+    /**
+     * Return the accepted types list for the file manager component.
+     *
+     * @return array|string
+     */
+    private function get_accepted_types() {
+        $acceptedtypes = $this->get_configured_typesets();
+
+        if (!empty($acceptedtypes)) {
+            return $acceptedtypes;
+        }
+
+        return '*';
+    }
+
+    /**
+     * List the nonexistent file types that need to be removed.
+     *
+     * @param string $types space , or ; separated types
+     * @return array A list of the nonexistent file types.
+     */
+    private function get_nonexistent_file_types($types) {
+        $nonexistent = [];
+        foreach ($this->get_typesets($types) as $type) {
+            $coretypes = core_filetypes::get_types();
+            // We can allow any extension, but validate groups & mimetypes.
+            if (strpos($type, '.') === false) {
+                // If there's no dot, check if it's a group.
+                $extensions = file_get_typegroup('extension', [$type]);
+                if (empty($extensions)) {
+                    // If there's no extensions under that group, it doesn't exist.
+                    $nonexistent[$type] = true;
+                }
+            }
+        }
+        return array_keys($nonexistent);
+    }
 }
diff --git a/mod/assign/submission/file/tests/behat/file_type_restriction.feature b/mod/assign/submission/file/tests/behat/file_type_restriction.feature
new file mode 100644 (file)
index 0000000..4990dae
--- /dev/null
@@ -0,0 +1,74 @@
+@mod @mod_assign @assignsubmission_file
+Feature: In an assignment, limit submittable file types
+  In order to constrain student submissions for marking
+  As a teacher
+  I need to limit the submittable file types
+
+  Background:
+    Given the following "courses" exist:
+      | fullname | shortname | category | groupmode |
+      | Course 1 | C1 | 0 | 1 |
+    And the following "users" exist:
+      | username | firstname | lastname | email |
+      | teacher1 | Teacher | 1 | teacher1@example.com |
+      | student1 | Student | 1 | student1@example.com |
+    And the following "course enrolments" exist:
+      | user | course | role |
+      | teacher1 | C1 | editingteacher |
+      | student1 | C1 | student |
+    And the following config values are set as admin:
+      | filetypes | image/png;spreadsheet | assignsubmission_file |
+
+  @javascript
+  Scenario: File types validation for an assignment
+    Given the following "activities" exist:
+      | activity | course | idnumber | name                 | intro                       | duedate    | assignsubmission_onlinetext_enabled | assignsubmission_file_enabled | assignsubmission_file_maxfiles | assignsubmission_file_maxsizebytes |
+      | assign   | C1     | assign1  | Test assignment name | Test assignment description | 1388534400 | 0                                   | 1                             | 1                              | 0                                  |
+    And I log in as "teacher1"
+    And I am on "Course 1" course homepage
+    And I follow "Test assignment name"
+    And I navigate to "Edit settings" in current page administration
+    When I set the field "Accepted file types" to "image/png;doesntexist;.anything;unreal/mimetype;nodot"
+    And I press "Save and display"
+    And I should see "The following file types were not recognised: doesntexist unreal/mimetype nodot"
+    And I set the field "Accepted file types" to "image/png;spreadsheet"
+    And I press "Save and display"
+    And I navigate to "Edit settings" in current page administration
+    Then the field "Accepted file types" matches value "image/png;spreadsheet"
+
+  @javascript @_file_upload
+  Scenario: Uploading permitted file types for an assignment
+    Given the following "activities" exist:
+      | activity | course | idnumber | name                 | intro                       | duedate    | assignsubmission_onlinetext_enabled | assignsubmission_file_enabled | assignsubmission_file_maxfiles | assignsubmission_file_maxsizebytes | assignsubmission_file_filetypes |
+      | assign   | C1     | assign1  | Test assignment name | Test assignment description | 1388534400 | 0                                   | 1                             | 3                              | 0                                  | image/png;spreadsheet;.xml;.txt  |
+    And I log in as "student1"
+    And I am on "Course 1" course homepage
+    And I follow "Test assignment name"
+    When I press "Add submission"
+    And I should see "Files of these types may be added to the submission"
+    And I should see "Image (PNG) — .png"
+    And I should see "Spreadsheet files — .csv .gsheet .ods .ots .xls .xlsx .xlsm"
+    And I should see ".txt"
+    And I upload "lib/tests/fixtures/gd-logo.png" file to "File submissions" filemanager
+    And I upload "lib/tests/fixtures/tabfile.csv" file to "File submissions" filemanager
+    And I upload "lib/tests/fixtures/empty.txt" file to "File submissions" filemanager
+    And I press "Save changes"
+    Then "gd-logo.png" "link" should exist
+    And "tabfile.csv" "link" should exist
+    And "empty.txt" "link" should exist
+
+  @javascript @_file_upload
+  Scenario: No filetypes allows all
+    Given the following "activities" exist:
+      | activity | course | idnumber | name                 | intro                       | duedate    | assignsubmission_onlinetext_enabled | assignsubmission_file_enabled | assignsubmission_file_maxfiles | assignsubmission_file_maxsizebytes | assignsubmission_file_filetypes |
+      | assign   | C1     | assign1  | Test assignment name | Test assignment description | 1388534400 | 0                                   | 1                             | 2                              | 0                                  |                                 |
+    And I log in as "student1"
+    And I am on "Course 1" course homepage
+    And I follow "Test assignment name"
+    When I press "Add submission"
+    And I should not see "Files of these types may be added to the submission"
+    And I upload "lib/tests/fixtures/gd-logo.png" file to "File submissions" filemanager
+    And I upload "lib/tests/fixtures/tabfile.csv" file to "File submissions" filemanager
+    And I press "Save changes"
+    Then "gd-logo.png" "link" should exist
+    And "tabfile.csv" "link" should exist
index 8df613a..c8b1082 100644 (file)
@@ -137,5 +137,69 @@ class assignsubmission_file_locallib_testcase extends advanced_testcase {
         ];
     }
 
+    /**
+     * Data provider for testing test_get_nonexistent_file_types.
+     *
+     * @return array
+     */
+    public function get_nonexistent_file_types_provider() {
+        return [
+            'Nonexistent extensions are allowed' => [
+                'filetypes' => '.rat',
+                'expected' => []
+            ],
+            'Multiple nonexistent extensions are allowed' => [
+                'filetypes' => '.ricefield .rat',
+                'expected' => []
+            ],
+            'Existent extension is allowed' => [
+                'filetypes' => '.xml',
+                'expected' => []
+            ],
+            'Existent group is allowed' => [
+                'filetypes' => 'web_file',
+                'expected' => []
+            ],
+            'Nonexistent group is not allowed' => [
+                'filetypes' => '©ç√√ß∂å√©åß©√',
+                'expected' => ['©ç√√ß∂å√©åß©√']
+            ],
+            'Existent mimetype is allowed' => [
+                'filetypes' => 'application/xml',
+                'expected' => []
+            ],
+            'Nonexistent mimetype is not allowed' => [
+                'filetypes' => 'ricefield/rat',
+                'expected' => ['ricefield/rat']
+            ],
+            'Multiple nonexistent mimetypes are not allowed' => [
+                'filetypes' => 'ricefield/rat cam/ball',
+                'expected' => ['ricefield/rat', 'cam/ball']
+            ],
+            'Missing dot in extension is not allowed' => [
+                'filetypes' => 'png',
+                'expected' => ['png']
+            ],
+            'Some existent some not' => [
+                'filetypes' => '.txt application/xml web_file ©ç√√ß∂å√©åß©√ .png ricefield/rat document png',
+                'expected' => ['©ç√√ß∂å√©åß©√', 'ricefield/rat', 'png']
+            ]
+        ];
+    }
+
+    /**
+     * Test get_nonexistent_file_types().
+     * @dataProvider get_nonexistent_file_types_provider
+     * @param string $filetypes The filetypes to check
+     * @param array $expected The expected result. The list of non existent file types.
+     */
+    public function test_get_nonexistent_file_types($filetypes, $expected) {
+        $this->resetAfterTest();
+        $method = new ReflectionMethod(assign_submission_file::class, 'get_nonexistent_file_types');
+        $method->setAccessible(true);
+        $plugin = $this->assign->get_submission_plugin_by_type('file');
+        $nonexistentfiletypes = $method->invokeArgs($plugin, [$filetypes]);
+        $this->assertSame($expected, $nonexistentfiletypes);
+    }
 
 }
index f7ee636..fe6bfce 100644 (file)
@@ -24,6 +24,6 @@
 
 defined('MOODLE_INTERNAL') || die();
 
-$plugin->version   = 2016120500;
+$plugin->version   = 2017032000;
 $plugin->requires  = 2016112900;
 $plugin->component = 'assignsubmission_file';
index 43b6cd6..2672a7f 100644 (file)
@@ -711,7 +711,10 @@ function mod_book_get_fontawesome_icon_map() {
 }
 
 /**
- * Handles creating actions for events.
+ * This function receives a calendar event and returns the action associated with it, or null if there is none.
+ *
+ * This is used by block_myoverview in order to display the event appropriately. If null is returned then the event
+ * is not displayed on the block.
  *
  * @param calendar_event $event
  * @param \core_calendar\action_factory $factory
index 4e728df..5ab467f 100644 (file)
@@ -1130,7 +1130,7 @@ function chat_get_post_actions() {
 
 /**
  * @deprecated since 3.3
- *
+ * @todo The final deprecation of this function will take place in Moodle 3.7 - see MDL-57487.
  * @global object
  * @global object
  * @param array $courses
@@ -1408,7 +1408,10 @@ function chat_view($chat, $course, $cm, $context) {
 }
 
 /**
- * Handles creating actions for events.
+ * This function receives a calendar event and returns the action associated with it, or null if there is none.
+ *
+ * This is used by block_myoverview in order to display the event appropriately. If null is returned then the event
+ * is not displayed on the block.
  *
  * @param calendar_event $event
  * @param \core_calendar\action_factory $factory
index 7f042ee..dfce5c3 100644 (file)
@@ -923,7 +923,7 @@ function choice_page_type_list($pagetype, $parentcontext, $currentcontext) {
  * and it is available for completing.
  *
  * @deprecated since 3.3
- *
+ * @todo The final deprecation of this function will take place in Moodle 3.7 - see MDL-57487.
  * @uses CONTEXT_MODULE
  * @param array $courses An array of course objects to get choice instances from.
  * @param array $htmlarray Store overview output array( course ID => 'choice' => HTML output )
@@ -1183,7 +1183,10 @@ function choice_check_updates_since(cm_info $cm, $from, $filter = array()) {
 }
 
 /**
- * Handles creating actions for events.
+ * This function receives a calendar event and returns the action associated with it, or null if there is none.
+ *
+ * This is used by block_myoverview in order to display the event appropriately. If null is returned then the event
+ * is not displayed on the block.
  *
  * @param calendar_event $event
  * @param \core_calendar\action_factory $factory
index d9cca40..ce175cf 100644 (file)
@@ -4287,7 +4287,10 @@ function data_check_updates_since(cm_info $cm, $from, $filter = array()) {
 }
 
 /**
- * Handles creating actions for events.
+ * This function receives a calendar event and returns the action associated with it, or null if there is none.
+ *
+ * This is used by block_myoverview in order to display the event appropriately. If null is returned then the event
+ * is not displayed on the block.
  *
  * @param calendar_event $event
  * @param \core_calendar\action_factory $factory
index 4fe20c4..27da47d 100644 (file)
@@ -3419,7 +3419,10 @@ function feedback_check_updates_since(cm_info $cm, $from, $filter = array()) {
 }
 
 /**
- * Handles creating actions for events.
+ * This function receives a calendar event and returns the action associated with it, or null if there is none.
+ *
+ * This is used by block_myoverview in order to display the event appropriately. If null is returned then the event
+ * is not displayed on the block.
  *
  * @param calendar_event $event
  * @param \core_calendar\action_factory $factory
index b549190..4736638 100644 (file)
@@ -777,7 +777,10 @@ function folder_check_updates_since(cm_info $cm, $from, $filter = array()) {
 }
 
 /**
- * Handles creating actions for events.
+ * This function receives a calendar event and returns the action associated with it, or null if there is none.
+ *
+ * This is used by block_myoverview in order to display the event appropriately. If null is returned then the event
+ * is not displayed on the block.
  *
  * @param calendar_event $event
  * @param \core_calendar\action_factory $factory
index c01e33a..63f45d9 100644 (file)
@@ -60,6 +60,9 @@ class backup_forum_activity_structure_step extends backup_activity_structure_ste
             'mailed', 'subject', 'message', 'messageformat',
             'messagetrust', 'attachment', 'totalscore', 'mailnow'));
 
+        $tags = new backup_nested_element('poststags');
+        $tag = new backup_nested_element('tag', array('id'), array('itemid', 'rawname'));
+
         $ratings = new backup_nested_element('ratings');
 
         $rating = new backup_nested_element('rating', array('id'), array(
@@ -110,6 +113,9 @@ class backup_forum_activity_structure_step extends backup_activity_structure_ste
         $forum->add_child($trackedprefs);
         $trackedprefs->add_child($track);
 
+        $forum->add_child($tags);
+        $tags->add_child($tag);
+
         $discussion->add_child($posts);
         $posts->add_child($post);
 
@@ -147,6 +153,19 @@ class backup_forum_activity_structure_step extends backup_activity_structure_ste
                                                       'ratingarea' => backup_helper::is_sqlparam('post'),
                                                       'itemid'     => backup::VAR_PARENTID));
             $rating->set_source_alias('rating', 'value');
+
+            if (core_tag_tag::is_enabled('mod_forum', 'forum_posts')) {
+                // Backup all tags for all forum posts in this forum.
+                $tag->set_source_sql('SELECT t.id, ti.itemid, t.rawname
+                                        FROM {tag} t
+                                        JOIN {tag_instance} ti ON ti.tagid = t.id
+                                       WHERE ti.itemtype = ?
+                                         AND ti.component = ?
+                                         AND ti.contextid = ?', array(
+                    backup_helper::is_sqlparam('forum_posts'),
+                    backup_helper::is_sqlparam('mod_forum'),
+                    backup::VAR_CONTEXTID));
+            }
         }
 
         // Define id annotations
index de8b038..c026123 100644 (file)
@@ -40,6 +40,7 @@ class restore_forum_activity_structure_step extends restore_activity_structure_s
         if ($userinfo) {
             $paths[] = new restore_path_element('forum_discussion', '/activity/forum/discussions/discussion');
             $paths[] = new restore_path_element('forum_post', '/activity/forum/discussions/discussion/posts/post');
+            $paths[] = new restore_path_element('forum_tag', '/activity/forum/poststags/tag');
             $paths[] = new restore_path_element('forum_discussion_sub', '/activity/forum/discussions/discussion/discussion_subs/discussion_sub');
             $paths[] = new restore_path_element('forum_rating', '/activity/forum/discussions/discussion/posts/post/ratings/rating');
             $paths[] = new restore_path_element('forum_subscription', '/activity/forum/subscriptions/subscription');
@@ -117,6 +118,23 @@ class restore_forum_activity_structure_step extends restore_activity_structure_s
         }
     }
 
+    protected function process_forum_tag($data) {
+        $data = (object)$data;
+
+        if (!core_tag_tag::is_enabled('mod_forum', 'forum_posts')) { // Tags disabled in server, nothing to process.
+            return;
+        }
+
+        $tag = $data->rawname;
+        if (!$itemid = $this->get_mappingid('forum_post', $data->itemid)) {
+            // Some orphaned tag, we could not find the restored post for it - ignore.
+            return;
+        }
+
+        $context = context_module::instance($this->task->get_moduleid());
+        core_tag_tag::add_item_tag('mod_forum', 'forum_posts', $itemid, $context, $tag);
+    }
+
     protected function process_forum_rating($data) {
         global $DB;
 
index 8145103..b4ae556 100644 (file)
@@ -52,6 +52,7 @@ class big_search_form implements renderable, templatable {
     public $subject;
     public $user;
     public $words;
+    public $tags;
     /** @var string The URL of the search form. */
     public $actionurl;
 
@@ -64,6 +65,7 @@ class big_search_form implements renderable, templatable {
     public function __construct($course) {
         global $DB;
         $this->course = $course;
+        $this->tags = [];
         $this->showfullwords = $DB->get_dbfamily() == 'mysql' || $DB->get_dbfamily() == 'postgres';
         $this->actionurl = new moodle_url('/mod/forum/search.php');
 
@@ -148,6 +150,15 @@ class big_search_form implements renderable, templatable {
         $this->words = $value;
     }
 
+    /**
+     * Set tags.
+     *
+     * @param mixed $value Tags.
+     */
+    public function set_tags($value) {
+        $this->tags = $value;
+    }
+
     /**
      * Forum ID setter search criteria.
      *
@@ -158,6 +169,7 @@ class big_search_form implements renderable, templatable {
     }
 
     public function export_for_template(renderer_base $output) {
+        global $DB, $CFG, $PAGE;
         $data = new stdClass();
 
         $data->courseid = $this->course->id;
@@ -172,6 +184,26 @@ class big_search_form implements renderable, templatable {
         $data->showfullwords = $this->showfullwords;
         $data->actionurl = $this->actionurl->out(false);
 
+        $tagtypestoshow = \core_tag_area::get_showstandard('mod_forum', 'forum_posts');
+        $showstandard = ($tagtypestoshow != \core_tag_tag::HIDE_STANDARD);
+        $typenewtags = ($tagtypestoshow != \core_tag_tag::STANDARD_ONLY);
+
+        $PAGE->requires->js_call_amd('core/form-autocomplete', 'enhance', $params = array('#tags', $typenewtags, '',
+                              get_string('entertags', 'tag'), false, $showstandard, get_string('noselection', 'form')));
+
+        $data->tagsenabled = \core_tag_tag::is_enabled('mod_forum', 'forum_posts');
+        $namefield = empty($CFG->keeptagnamecase) ? 'name' : 'rawname';
+        $tags = $DB->get_records('tag',
+            array('isstandard' => 1, 'tagcollid' => \core_tag_area::get_collection('mod_forum', 'forum_posts')),
+            $namefield, 'rawname,' . $namefield . ' as fieldname');
+        $data->tags = [];
+        foreach ($tags as $tag) {
+            $data->tagoptions[] = ['value'    => $tag->rawname,
+                                   'text'     => $tag->fieldname,
+                                   'selected' => in_array($tag->rawname, $this->tags)
+            ];
+        }
+
         $datefrom = $this->datefrom;
         if (empty($datefrom)) {
             $datefrom = make_timestamp(2000, 1, 1, 0, 0, 0);
index a9e8e82..0ade6fb 100644 (file)
@@ -238,6 +238,13 @@ class mod_forum_post_form extends moodleform {
             $mform->setConstants(array('timestart' => 0, 'timeend' => 0));
         }
 
+        if (core_tag_tag::is_enabled('mod_forum', 'forum_posts')) {
+            $mform->addElement('header', 'tagshdr', get_string('tags', 'tag'));
+
+            $mform->addElement('tags', 'tags', get_string('tags'),
+                array('itemtype' => 'forum_posts', 'component' => 'mod_forum'));
+        }
+
         //-------------------------------------------------------------------------------
         // buttons
         if (isset($post->edit)) { // hack alert
diff --git a/mod/forum/db/tag.php b/mod/forum/db/tag.php
new file mode 100644 (file)
index 0000000..8496408
--- /dev/null
@@ -0,0 +1,35 @@
+<?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/>.
+
+/**
+ * Tag areas in component mod_forum
+ *
+ * @package   mod_forum
+ * @copyright 2017 Andrew Hancox <andrewdchancox@googlemail.com>
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die();
+
+
+$tagareas = array(
+    array(
+        'itemtype' => 'forum_posts',
+        'component' => 'mod_forum',
+        'callback' => 'mod_forum_get_tagged_posts',
+        'callbackfile' => '/mod/forum/locallib.php',
+    ),
+);
index 415c599..4ee9694 100644 (file)
@@ -430,6 +430,7 @@ $string['qandaforum'] = 'Q and A forum';
 $string['qandanotify'] = 'This is a question and answer forum. In order to see other responses to these questions, you must first post your answer';
 $string['re'] = 'Re:';
 $string['readtherest'] = 'Read the rest of this topic';
+$string['removeallforumtags'] = 'Remove all forum tags';
 $string['replies'] = 'Replies';
 $string['repliesmany'] = '{$a} replies so far';
 $string['repliesone'] = '{$a} reply so far';
@@ -464,6 +465,7 @@ $string['searcholderposts'] = 'Search older posts...';
 $string['searchphrase'] = 'This exact phrase must appear in the post';
 $string['searchresults'] = 'Search results';
 $string['searchsubject'] = 'These words should be in the subject';
+$string['searchtags'] = 'Is tagged with';
 $string['searchuser'] = 'This name should match the author';
 $string['searchuserid'] = 'The Moodle ID of the author';
 $string['searchwhichforums'] = 'Choose which forums to search';
@@ -503,6 +505,9 @@ $string['subscriptionforced'] = 'Forced subscription';
 $string['subscriptionauto'] = 'Auto subscription';
 $string['subscriptiondisabled'] = 'Subscription disabled';
 $string['subscriptions'] = 'Subscriptions';
+$string['tagarea_forum_posts'] = 'Forum posts';
+$string['tagsdeleted'] = 'Forum tags have been deleted';
+$string['tagtitle'] = 'See the "{$a}" tag';
 $string['thisforumisthrottled'] = 'This forum has a limit to the number of forum postings you can make in a given time period - this is currently set at {$a->blockafter} posting(s) in {$a->blockperiod}';
 $string['timedhidden'] = 'Timed status: Hidden from students';
 $string['timedposts'] = 'Timed posts';
index 12d17fd..4ed4e4a 100644 (file)
@@ -1333,7 +1333,7 @@ function forum_user_complete($course, $user, $mod, $forum) {
  * Filters the forum discussions according to groups membership and config.
  *
  * @deprecated since 3.3
- *
+ * @todo The final deprecation of this function will take place in Moodle 3.7 - see MDL-57487.
  * @since  Moodle 2.8, 2.7.1, 2.6.4
  * @param  array $discussions Discussions with new posts array
  * @return array Forums with the number of new posts
@@ -1398,7 +1398,7 @@ function forum_is_user_group_discussion(cm_info $cm, $discussiongroupid) {
 
 /**
  * @deprecated since 3.3
- *
+ * @todo The final deprecation of this function will take place in Moodle 3.7 - see MDL-57487.
  * @global object
  * @global object
  * @global object
@@ -2095,15 +2095,37 @@ function forum_search_posts($searchterms, $courseid=0, $limitfrom=0, $limitnum=5
 
     if ($lexer->parse($searchstring)) {
         $parsearray = $parser->get_parsed_array();
+
+        $tagjoins = '';
+        $tagfields = [];
+        $tagfieldcount = 0;
+        foreach ($parsearray as $token) {
+            if ($token->getType() == TOKEN_TAGS) {
+                for ($i = 0; $i <= substr_count($token->getValue(), ','); $i++) {
+                    // Queries can only have a limited number of joins so set a limit sensible users won't exceed.
+                    if ($tagfieldcount > 10) {
+                        continue;
+                    }
+                    $tagjoins .= " LEFT JOIN {tag_instance} ti_$tagfieldcount
+                                        ON p.id = ti_$tagfieldcount.itemid
+                                            AND ti_$tagfieldcount.component = 'mod_forum'
+                                            AND ti_$tagfieldcount.itemtype = 'forum_posts'";
+                    $tagjoins .= " LEFT JOIN {tag} t_$tagfieldcount ON t_$tagfieldcount.id = ti_$tagfieldcount.tagid";
+                    $tagfields[] = "t_$tagfieldcount.rawname";
+                    $tagfieldcount++;
+                }
+            }
+        }
         list($messagesearch, $msparams) = search_generate_SQL($parsearray, 'p.message', 'p.subject',
                                                               'p.userid', 'u.id', 'u.firstname',
-                                                              'u.lastname', 'p.modified', 'd.forum');
+                                                              'u.lastname', 'p.modified', 'd.forum',
+                                                              $tagfields);
         $params = array_merge($params, $msparams);
     }
 
-    $fromsql = "{forum_posts} p,
-                  {forum_discussions} d,
-                  {user} u";
+    $fromsql = "{forum_posts} p
+                  INNER JOIN {forum_discussions} d ON d.id = p.discussion
+                  INNER JOIN {user} u ON u.id = p.userid $tagjoins";
 
     $selectsql = " $messagesearch
                AND p.discussion = d.id
@@ -3446,6 +3468,10 @@ function forum_print_post($post, $discussion, $forum, &$cm, $course, $ownpost=fa
         $postcontent .= html_writer::tag('div', $attachedimages, array('class'=>'attachedimages'));
     }
 
+    if (\core_tag_tag::is_enabled('mod_forum', 'forum_posts')) {
+        $postcontent .= $OUTPUT->tag_list(core_tag_tag::get_item_tags('mod_forum', 'forum_posts', $post->id), null, 'forum-tags');
+    }
+
     // Output the post content
     $output .= html_writer::tag('div', $postcontent, array('class'=>'posting '.$postclass));
     $output .= html_writer::end_tag('div'); // Content
@@ -4413,6 +4439,10 @@ function forum_add_new_post($post, $mform, $unused = null) {
         forum_tp_mark_post_read($post->userid, $post);
     }
 
+    if (isset($post->tags)) {
+        core_tag_tag::set_item_tags('mod_forum', 'forum_posts', $post->id, $context, $post->tags);
+    }
+
     // Let Moodle know that assessable content is uploaded (eg for plagiarism detection)
     forum_trigger_content_uploaded_event($post, $cm, 'forum_add_new_post');
 
@@ -4474,6 +4504,10 @@ function forum_update_post($newpost, $mform, $unused = null) {
 
     forum_add_attachment($post, $forum, $cm, $mform);
 
+    if (isset($newpost->tags)) {
+        core_tag_tag::set_item_tags('mod_forum', 'forum_posts', $post->id, $context, $newpost->tags);
+    }
+
     if (forum_tp_can_track_forums($forum) && forum_tp_is_tracked($forum)) {
         forum_tp_mark_post_read($USER->id, $post);
     }
@@ -4552,6 +4586,10 @@ function forum_add_discussion($discussion, $mform=null, $unused=null, $userid=nu
         forum_add_attachment($post, $forum, $cm, $mform, $unused);
     }
 
+    if (isset($discussion->tags)) {
+        core_tag_tag::set_item_tags('mod_forum', 'forum_posts', $post->id, context_module::instance($cm->id), $discussion->tags);
+    }
+
     if (forum_tp_can_track_forums($forum) && forum_tp_is_tracked($forum)) {
         forum_tp_mark_post_read($post->userid, $post);
     }
@@ -5262,7 +5300,8 @@ function forum_user_can_see_post($forum, $discussion, $post, $user=NULL, $cm=NUL
         $user = $USER;
     }
 
-    $canviewdiscussion = !empty($cm->cache->caps['mod/forum:viewdiscussion']) || has_capability('mod/forum:viewdiscussion', $modcontext, $user->id);
+    $canviewdiscussion = (isset($cm->cache) && !empty($cm->cache->caps['mod/forum:viewdiscussion']))
+        || has_capability('mod/forum:viewdiscussion', $modcontext, $user->id);
     if (!$canviewdiscussion && !has_all_capabilities(array('moodle/user:viewdetails', 'moodle/user:readuserposts'), context_user::instance($post->userid))) {
         return false;
     }
@@ -5286,12 +5325,16 @@ function forum_user_can_see_post($forum, $discussion, $post, $user=NULL, $cm=NUL
     }
 
     if ($forum->type == 'qanda') {
+        if (has_capability('mod/forum:viewqandawithoutposting', $modcontext, $user->id) || $post->userid == $user->id
+                || (isset($discussion->firstpost) && $discussion->firstpost == $post->id)) {
+            return true;
+        }
         $firstpost = forum_get_firstpost_from_discussion($discussion->id);
+        if ($firstpost->userid == $user->id) {
+            return true;
+        }
         $userfirstpost = forum_get_user_posted_time($discussion->id, $user->id);
-
-        return (($userfirstpost !== false && (time() - $userfirstpost >= $CFG->maxeditingtime)) ||
-                $firstpost->id == $post->id || $post->userid == $user->id || $firstpost->userid == $user->id ||
-                has_capability('mod/forum:viewqandawithoutposting', $modcontext, $user->id));
+        return (($userfirstpost !== false && (time() - $userfirstpost >= $CFG->maxeditingtime)));
     }
     return true;
 }
@@ -8150,7 +8193,10 @@ function mod_forum_core_calendar_event_action_shows_item_count(calendar_event $e
 }
 
 /**
- * Handles creating actions for events.
+ * This function receives a calendar event and returns the action associated with it, or null if there is none.
+ *
+ * This is used by block_myoverview in order to display the event appropriately. If null is returned then the event
+ * is not displayed on the block.
  *
  * @param calendar_event $event
  * @param \core_calendar\action_factory $factory
index 057339a..b446d27 100644 (file)
@@ -566,3 +566,131 @@ class forum_file_info_container extends file_info {
         return $this->browser->get_file_info($this->context);
     }
 }
+
+/**
+ * Returns forum posts tagged with a specified tag.
+ *
+ * This is a callback used by the tag area mod_forum/forum_posts to search for forum posts
+ * tagged with a specific tag.
+ *
+ * @param core_tag_tag $tag
+ * @param bool $exclusivemode if set to true it means that no other entities tagged with this tag
+ *             are displayed on the page and the per-page limit may be bigger
+ * @param int $fromctx context id where the link was displayed, may be used by callbacks
+ *            to display items in the same context first
+ * @param int $ctx context id where to search for records
+ * @param bool $rec search in subcontexts as well
+ * @param int $page 0-based number of page being displayed
+ * @return \core_tag\output\tagindex
+ */
+function mod_forum_get_tagged_posts($tag, $exclusivemode = false, $fromctx = 0, $ctx = 0, $rec = 1, $page = 0) {
+    global $OUTPUT;
+    $perpage = $exclusivemode ? 20 : 5;
+
+    // Build the SQL query.
+    $ctxselect = context_helper::get_preload_record_columns_sql('ctx');
+    $query = "SELECT fp.id, fp.subject, fd.forum, fp.discussion, f.type, fd.timestart, fd.timeend, fd.groupid, fd.firstpost,
+                    fp.parent, fp.userid,
+                    cm.id AS cmid, c.id AS courseid, c.shortname, c.fullname, $ctxselect
+                FROM {forum_posts} fp
+                JOIN {forum_discussions} fd ON fp.discussion = fd.id
+                JOIN {forum} f ON f.id = fd.forum
+                JOIN {modules} m ON m.name='forum'
+                JOIN {course_modules} cm ON cm.module = m.id AND cm.instance = f.id
+                JOIN {tag_instance} tt ON fp.id = tt.itemid
+                JOIN {course} c ON cm.course = c.id
+                JOIN {context} ctx ON ctx.instanceid = cm.id AND ctx.contextlevel = :coursemodulecontextlevel
+               WHERE tt.itemtype = :itemtype AND tt.tagid = :tagid AND tt.component = :component
+                 AND cm.deletioninprogress = 0
+                 AND fp.id %ITEMFILTER% AND c.id %COURSEFILTER%";
+
+    $params = array('itemtype' => 'forum_posts', 'tagid' => $tag->id, 'component' => 'mod_forum',
+                    'coursemodulecontextlevel' => CONTEXT_MODULE);
+
+    if ($ctx) {
+        $context = $ctx ? context::instance_by_id($ctx) : context_system::instance();
+        $query .= $rec ? ' AND (ctx.id = :contextid OR ctx.path LIKE :path)' : ' AND ctx.id = :contextid';
+        $params['contextid'] = $context->id;
+        $params['path'] = $context->path.'/%';
+    }
+
+    $query .= " ORDER BY ";
+    if ($fromctx) {
+        // In order-clause specify that modules from inside "fromctx" context should be returned first.
+        $fromcontext = context::instance_by_id($fromctx);
+        $query .= ' (CASE WHEN ctx.id = :fromcontextid OR ctx.path LIKE :frompath THEN 0 ELSE 1 END),';
+        $params['fromcontextid'] = $fromcontext->id;
+        $params['frompath'] = $fromcontext->path.'/%';
+    }
+    $query .= ' c.sortorder, cm.id, fp.id';
+
+    $totalpages = $page + 1;
+
+    // Use core_tag_index_builder to build and filter the list of items.
+    $builder = new core_tag_index_builder('mod_forum', 'forum_posts', $query, $params, $page * $perpage, $perpage + 1);
+    while ($item = $builder->has_item_that_needs_access_check()) {
+        context_helper::preload_from_record($item);
+        $courseid = $item->courseid;
+        if (!$builder->can_access_course($courseid)) {
+            $builder->set_accessible($item, false);
+            continue;
+        }
+        $modinfo = get_fast_modinfo($builder->get_course($courseid));
+        // Set accessibility of this item and all other items in the same course.
+        $builder->walk(function ($taggeditem) use ($courseid, $modinfo, $builder, $item) {
+            // Checking permission for Q&A forums performs additional DB queries, do not do them in bulk.
+            if ($taggeditem->courseid == $courseid && ($taggeditem->type != 'qanda' || $taggeditem->id == $item->id)) {
+                $cm = $modinfo->get_cm($taggeditem->cmid);
+                $forum = (object)['id'     => $taggeditem->forum,
+                                  'course' => $taggeditem->courseid,
+                                  'type'   => $taggeditem->type
+                ];
+                $discussion = (object)['id'        => $taggeditem->discussion,
+                                       'timestart' => $taggeditem->timestart,
+                                       'timeend'   => $taggeditem->timeend,
+                                       'groupid'   => $taggeditem->groupid,
+                                       'firstpost' => $taggeditem->firstpost
+                ];
+                $post = (object)['id' => $taggeditem->id,
+                                       'parent' => $taggeditem->parent,
+                                       'userid'   => $taggeditem->userid,
+                                       'groupid'   => $taggeditem->groupid
+                ];
+
+                $accessible = forum_user_can_see_post($forum, $discussion, $post, null, $cm);
+                $builder->set_accessible($taggeditem, $accessible);
+            }
+        });
+    }
+
+    $items = $builder->get_items();
+    if (count($items) > $perpage) {
+        $totalpages = $page + 2; // We don't need exact page count, just indicate that the next page exists.
+        array_pop($items);
+    }
+
+    // Build the display contents.
+    if ($items) {
+        $tagfeed = new core_tag\output\tagfeed();
+        foreach ($items as $item) {
+            context_helper::preload_from_record($item);
+            $modinfo = get_fast_modinfo($item->courseid);
+            $cm = $modinfo->get_cm($item->cmid);
+            $pageurl = new moodle_url('/mod/forum/discuss.php', array('d' => $item->discussion), 'p' . $item->id);
+            $pagename = format_string($item->subject, true, array('context' => context_module::instance($item->cmid)));
+            $pagename = html_writer::link($pageurl, $pagename);
+            $courseurl = course_get_url($item->courseid, $cm->sectionnum);
+            $cmname = html_writer::link($cm->url, $cm->get_formatted_name());
+            $coursename = format_string($item->fullname, true, array('context' => context_course::instance($item->courseid)));
+            $coursename = html_writer::link($courseurl, $coursename);
+            $icon = html_writer::link($pageurl, html_writer::empty_tag('img', array('src' => $cm->get_icon_url())));
+            $tagfeed->add($icon, $pagename, $cmname.'<br>'.$coursename);
+        }
+
+        $content = $OUTPUT->render_from_template('core_tag/tagfeed',
+            $tagfeed->export_for_template($OUTPUT));
+
+        return new core_tag\output\tagindex($tag, 'mod_forum', 'forum_posts', $content,
+            $exclusivemode, $fromctx, $ctx, $rec, $page, $totalpages);
+    }
+}
index 1933800..88acf30 100644 (file)
@@ -1052,6 +1052,13 @@ if (!empty($parent)) {
 if (!empty($formheading)) {
     echo $OUTPUT->heading($formheading, 2, array('class' => 'accesshide'));
 }
+
+$data = new StdClass();
+if (isset($postid)) {
+    $data->tags = core_tag_tag::get_item_tags_array('mod_forum', 'forum_posts', $postid);
+    $mform_post->set_data($data);
+}
+
 $mform_post->display();
 
 echo $OUTPUT->footer();
index ec399f9..9cfed3b 100644 (file)
@@ -38,6 +38,7 @@ $phrase  = trim(optional_param('phrase', '', PARAM_NOTAGS));  // Phrase
 $words   = trim(optional_param('words', '', PARAM_NOTAGS));   // Words
 $fullwords = trim(optional_param('fullwords', '', PARAM_NOTAGS)); // Whole words
 $notwords = trim(optional_param('notwords', '', PARAM_NOTAGS));   // Words we don't want
+$tags = optional_param_array('tags', [], PARAM_TEXT);
 
 $timefromrestrict = optional_param('timefromrestrict', 0, PARAM_INT); // Use starting date
 $fromday = optional_param('fromday', 0, PARAM_INT);      // Starting date
@@ -101,6 +102,9 @@ if (empty($search)) {   // Check the other parameters instead
     if (!empty($dateto)) {
         $search .= ' dateto:'.$dateto;
     }
+    if (!empty($tags)) {
+        $search .= ' tags:' . implode(',', $tags);
+    }
     $individualparams = true;
 } else {
     $individualparams = false;
@@ -186,19 +190,27 @@ $PAGE->set_heading($course->fullname);
 $PAGE->set_button($searchform);
 echo $OUTPUT->header();
 echo '<div class="reportlink">';
-echo '<a href="search.php?id='.$course->id.
-                         '&amp;user='.urlencode($user).
-                         '&amp;userid='.$userid.
-                         '&amp;forumid='.$forumid.
-                         '&amp;subject='.urlencode($subject).
-                         '&amp;phrase='.urlencode($phrase).
-                         '&amp;words='.urlencode($words).
-                         '&amp;fullwords='.urlencode($fullwords).
-                         '&amp;notwords='.urlencode($notwords).
-                         '&amp;dateto='.$dateto.
-                         '&amp;datefrom='.$datefrom.
-                         '&amp;showform=1'.
-                         '">'.get_string('advancedsearch','forum').'...</a>';
+
+$params = [
+    'id'        => $course->id,
+    'user'      => $user,
+    'userid'    => $userid,
+    'forumid'   => $forumid,
+    'subject'   => $subject,
+    'phrase'    => $phrase,
+    'words'     => $words,
+    'fullwords' => $fullwords,
+    'notwords'  => $notwords,
+    'dateto'    => $dateto,
+    'datefrom'  => $datefrom,
+    'showform'  => 1
+];
+$url    = new moodle_url("/mod/forum/search.php", $params);
+foreach ($tags as $tag) {
+    $url .= "&tags[]=$tag";
+}
+echo html_writer::link($url, get_string('advancedsearch', 'forum').'...');
+
 echo '</div>';
 
 echo $OUTPUT->heading($strforums, 2);
@@ -318,7 +330,7 @@ echo $OUTPUT->footer();
   * @return void The function prints the form.
   */
 function forum_print_big_search_form($course) {
-    global $PAGE, $words, $subject, $phrase, $user, $fullwords, $notwords, $datefrom, $dateto, $forumid;
+    global $PAGE, $words, $subject, $phrase, $user, $fullwords, $notwords, $datefrom, $dateto, $forumid, $tags;
 
     $renderable = new \mod_forum\output\big_search_form($course, $user);
     $renderable->set_words($words);
@@ -330,6 +342,7 @@ function forum_print_big_search_form($course) {
     $renderable->set_subject($subject);
     $renderable->set_user($user);
     $renderable->set_forumid($forumid);
+    $renderable->set_tags($tags);
 
     $output = $PAGE->get_renderer('mod_forum');
     echo $output->render($renderable);
index f9371f2..4d26e5c 100644 (file)
                     <input type="text" size="35" name="user" id="user" value="{{user}}">
                 </td>
             </tr>
+            {{#tagsenabled}}
+            <tr>
+                <td class="c0">
+                    <label for="tags">{{#str}}searchtags, forum{{/str}}</label>
+                </td>
+                <td class="c1">
+                    <select class="custom-select" name="tags[]"
+                            id="tags" multiple>
+                        {{#tagoptions}}
+                            <option value="{{value}}" {{#selected}}selected{{/selected}}>{{{text}}}</option>
+                        {{/tagoptions}}
+                    </select>
+                </td>
+            </tr>
+            {{/tagsenabled}}
             <tr>
                 <td colspan="2" class="submit">
                     <div class="text-center">
index c9134a8..558ec83 100644 (file)
@@ -18,10 +18,14 @@ Feature: The forum search allows users to perform advanced searches for forum po
       | teacher1 | C1 | editingteacher |
       | teacher2 | C1 | editingteacher |
       | student1 | C1 | student |
+    And the following "tags" exist:
+      | name         | isstandard  |
+      | SearchedTag  | 1           |
     And I log in as "teacher1"
     And I am on "Course 1" course homepage with editing mode on
     And I add the "Latest announcements" block
     And I navigate to "Edit settings" node in "Course administration"
+    And I expand all fieldsets
     And I set the field "id_newsitems" to "1"
     And I press "Save and display"
     And I add a new topic to "Announcements" forum with:
@@ -29,8 +33,8 @@ Feature: The forum search allows users to perform advanced searches for forum po
       | Message | My message |
     And I am on "Course 1" course homepage
     And I add a new topic to "Announcements" forum with:
-      | Subject | My subjective|
-      | Message | My long message |
+      | Subject | Your subjective|
+      | Message | Your long message |
     And I log out
 
   Scenario: Perform an advanced search using any term
@@ -42,7 +46,7 @@ Feature: The forum search allows users to perform advanced searches for forum po
     And I set the field "words" to "subject"
     When I press "Search forums"
     Then I should see "My subject"
-    And I should see "My subjective"
+    And I should see "Your subjective"
 
   Scenario: Perform an advanced search avoiding words
     Given I log in as "student1"
@@ -54,7 +58,7 @@ Feature: The forum search allows users to perform advanced searches for forum po
     And I set the field "notwords" to "subjective"
     When I press "Search forums"
     Then I should see "My subject"
-    And I should not see "My subjective"
+    And I should not see "Your subjective"
 
   Scenario: Perform an advanced search using whole words
     Given database family used is one of the following:
@@ -68,7 +72,7 @@ Feature: The forum search allows users to perform advanced searches for forum po
     And I set the field "fullwords" to "subject"
     When I press "Search forums"
     Then I should see "My subject"
-    And I should not see "My subjective"
+    And I should not see "Your subjective"
 
   Scenario: Perform an advanced search matching the subject
     Given I log in as "student1"
@@ -79,7 +83,7 @@ Feature: The forum search allows users to perform advanced searches for forum po
     And I set the field "subject" to "subjective"
     When I press "Search forums"
     Then I should not see "My message"
-    And I should see "My subjective"
+    And I should see "Your subjective"
 
   Scenario: Perform an advanced search matching the author
     Given I log in as "teacher2"
@@ -104,7 +108,29 @@ Feature: The forum search allows users to perform advanced searches for forum po
     And I follow "Announcements"
     And I press "Search forums"
     And I should see "Advanced search"
-    And I set the field "subject" to "my subjective"
+    And I set the field "subject" to "your subjective"
     When I press "Search forums"
     Then I should not see "My message"
-    And I should see "My subjective"
+    And I should see "Your subjective"
+
+  @javascript
+  Scenario: Perform an advanced search using tags
+    Given I log in as "teacher1"
+    And I am on "Course 1" course homepage
+    And I follow "Announcements"
+    And I follow "My subject"
+    And I follow "Edit"
+    And I set the following fields to these values:
+        | Tags    | SearchedTag |
+    And I press "Save changes"
+    And I log out
+    And I log in as "student1"
+    And I am on "Course 1" course homepage
+    And I follow "Announcements"
+    And I press "Search forums"
+    And I should see "Advanced search"
+    And I set the field "Is tagged with" to "SearchedTag"
+    And I click on "[data-value='SearchedTag']" "css_element"
+    When I press "Search forums"
+    Then I should see "My subject"
+    And I should not see "Your subjective"
diff --git a/mod/forum/tests/behat/edit_tags.feature b/mod/forum/tests/behat/edit_tags.feature
new file mode 100644 (file)
index 0000000..d7c7058
--- /dev/null
@@ -0,0 +1,69 @@
+@mod @mod_forum @core_tag
+Feature: Edited forum posts handle tags correctly
+  In order to get forum posts properly labelled
+  As a user
+  I need to introduce the tags while editing
+
+  Background:
+    Given the following "users" exist:
+      | username | firstname | lastname | email |
+      | teacher1 | Teacher | 1 | teacher1@example.com |
+      | student1 | Student | 1 | student1@example.com |
+    And the following "courses" exist:
+      | fullname | shortname | format |
+      | Course 1 | C1 | topics |
+    And the following "course enrolments" exist:
+      | user | course | role |
+      | teacher1 | C1 | editingteacher |
+      | student1 | C1 | student |
+    And I log in as "teacher1"
+    And I am on "Course 1" course homepage with editing mode on
+    And I add a "Forum" to section "1" and I fill the form with:
+      | Forum name | Test forum name |
+      | Description | Test forum description |
+    And I add a new discussion to "Test forum name" forum with:
+      | Subject | Teacher post subject |
+      | Message | Teacher post message |
+    And I log out
+
+  @javascript
+  Scenario: Forum post edition of custom tags works as expected
+    Given I log in as "student1"
+    And I am on "Course 1" course homepage
+    And I reply "Teacher post subject" post from "Test forum name" forum with:
+      | Subject | Student post subject |
+      | Message | Student post message |
+      | Tags    | Tag1                 |
+    Then I should see "Tag1" in the ".forum-tags" "css_element"
+    And I click on "Edit" "link" in the "//div[@aria-label='Student post subject by Student 1']" "xpath_element"
+    Then I should see "Tag1" in the ".form-autocomplete-selection" "css_element"
+
+  @javascript
+  Scenario: Forum post edition of standard tags works as expected
+    Given I log in as "admin"
+    And I navigate to "Appearance > Manage tags" in site administration
+    And I follow "Default collection"
+    And I follow "Add standard tags"
+    And I set the field "Enter comma-separated list of new tags" to "OT1, OT2, OT3"
+    And I press "Continue"
+    And I log out
+    And I log in as "teacher1"
+    And I am on "Course 1" course homepage
+    And I follow "Test forum"
+    And I click on "Add a new discussion topic" "button"
+    And I expand all fieldsets
+    And I click on ".form-autocomplete-downarrow" "css_element"
+    And I should see "OT1" in the ".form-autocomplete-suggestions" "css_element"
+    And I should see "OT2" in the ".form-autocomplete-suggestions" "css_element"
+    And I should see "OT3" in the ".form-autocomplete-suggestions" "css_element"
+    And I reply "Teacher post subject" post from "Test forum name" forum with:
+      | Subject | Student post subject |
+      | Message | Student post message |
+      | Tags | OT1, OT3 |
+    Then I should see "OT1" in the ".forum-tags" "css_element"
+    And I should see "OT3" in the ".forum-tags" "css_element"
+    And I should not see "OT2" in the ".forum-tags" "css_element"
+    And I click on "Edit" "link" in the "//div[@aria-label='Student post subject by Teacher 1']" "xpath_element"
+    And I should see "OT1" in the ".form-autocomplete-selection" "css_element"
+    And I should see "OT3" in the ".form-autocomplete-selection" "css_element"
+    And I should not see "OT2" in the ".form-autocomplete-selection" "css_element"
index 818b8ce..151a38c 100644 (file)
@@ -202,9 +202,9 @@ class mod_forum_generator extends testing_module_generator {
         // Add the discussion.
         $record->id = forum_add_discussion($record, null, null, $record->userid);
 
-        if (isset($timemodified) || isset($mailed)) {
-            $post = $DB->get_record('forum_posts', array('discussion' => $record->id));
+        $post = $DB->get_record('forum_posts', array('discussion' => $record->id));
 
+        if (isset($timemodified) || isset($mailed)) {
             if (isset($mailed)) {
                 $post->mailed = $mailed;
             }
@@ -220,6 +220,14 @@ class mod_forum_generator extends testing_module_generator {
             $DB->update_record('forum_posts', $post);
         }
 
+        if (property_exists($record, 'tags')) {
+            $cm = get_coursemodule_from_instance('forum', $record->forum);
+            $tags = is_array($record->tags) ? $record->tags : preg_split('/,/', $record->tags);
+
+            core_tag_tag::set_item_tags('mod_forum', 'forum_posts', $post->id,
+                context_module::instance($cm->id), $tags);
+        }
+
         return $record;
     }
 
@@ -297,6 +305,15 @@ class mod_forum_generator extends testing_module_generator {
         // Add the post.
         $record->id = $DB->insert_record('forum_posts', $record);
 
+        if (property_exists($record, 'tags')) {
+            $discussion = $DB->get_record('forum_discussions', ['id' => $record->discussion]);
+            $cm = get_coursemodule_from_instance('forum', $discussion->forum);
+            $tags = is_array($record->tags) ? $record->tags : preg_split('/,/', $record->tags);
+
+            core_tag_tag::set_item_tags('mod_forum', 'forum_posts', $record->id,
+                context_module::instance($cm->id), $tags);
+        }
+
         // Update the last post.
         forum_discussion_update_last_post($record->discussion);
 
index 0be1d4a..8a4e20f 100644 (file)
@@ -117,6 +117,11 @@ class mod_forum_generator_testcase extends advanced_testcase {
         // Check the discussions were correctly created.
         $this->assertEquals(3, $DB->count_records_select('forum_discussions', 'forum = :forum',
             array('forum' => $forum->id)));
+
+        $record['tags'] = array('Cats', 'mice');
+        $record = self::getDataGenerator()->get_plugin_generator('mod_forum')->create_discussion($record);
+        $this->assertEquals(array('Cats', 'mice'),
+            array_values(core_tag_tag::get_item_tags_array('mod_forum', 'forum_posts', $record->firstpost)));
     }
 
     /**
@@ -160,6 +165,11 @@ class mod_forum_generator_testcase extends advanced_testcase {
         // is generated as well, so we should have 4 posts, not 3.
         $this->assertEquals(4, $DB->count_records_select('forum_posts', 'discussion = :discussion',
             array('discussion' => $discussion->id)));
+
+        $record->tags = array('Cats', 'mice');
+        $record = self::getDataGenerator()->get_plugin_generator('mod_forum')->create_post($record);
+        $this->assertEquals(array('Cats', 'mice'),
+            array_values(core_tag_tag::get_item_tags_array('mod_forum', 'forum_posts', $record->id)));
     }
 
     public function test_create_content() {
@@ -187,16 +197,21 @@ class mod_forum_generator_testcase extends advanced_testcase {
         $post3 = $generator->create_content($forum, array('discussion' => $post1->discussion));
         // This should create posts answering another post.
         $post4 = $generator->create_content($forum, array('parent' => $post2->id));
+        // This should create post with tags.
+        $post5 = $generator->create_content($forum, array('parent' => $post2->id, 'tags' => array('Cats', 'mice')));
 
         $discussionrecords = $DB->get_records('forum_discussions', array('forum' => $forum->id));
         $postrecords = $DB->get_records('forum_posts');
         $postrecords2 = $DB->get_records('forum_posts', array('discussion' => $post1->discussion));
         $this->assertEquals(1, count($discussionrecords));
-        $this->assertEquals(4, count($postrecords));
-        $this->assertEquals(4, count($postrecords2));
+        $this->assertEquals(5, count($postrecords));
+        $this->assertEquals(5, count($postrecords2));
         $this->assertEquals($post1->id, $discussionrecords[$post1->discussion]->firstpost);
         $this->assertEquals($post1->id, $postrecords[$post2->id]->parent);
         $this->assertEquals($post1->id, $postrecords[$post3->id]->parent);
         $this->assertEquals($post2->id, $postrecords[$post4->id]->parent);
+
+        $this->assertEquals(array('Cats', 'mice'),
+            array_values(core_tag_tag::get_item_tags_array('mod_forum', 'forum_posts', $post5->id)));
     }
 }
index 0af8f26..59ed1cb 100644 (file)
@@ -26,6 +26,7 @@ defined('MOODLE_INTERNAL') || die();
 
 global $CFG;
 require_once($CFG->dirroot . '/mod/forum/lib.php');
+require_once($CFG->dirroot . '/mod/forum/locallib.php');
 require_once($CFG->dirroot . '/rating/lib.php');
 
 class mod_forum_lib_testcase extends advanced_testcase {
@@ -3410,6 +3411,94 @@ class mod_forum_lib_testcase extends advanced_testcase {
         $this->assertNull($actionevent);
     }
 
+    public function test_mod_forum_get_tagged_posts() {
+        global $DB;
+
+        $this->resetAfterTest();
+        $this->setAdminUser();
+
+        // Setup test data.
+        $forumgenerator = $this->getDataGenerator()->get_plugin_generator('mod_forum');
+        $course3 = $this->getDataGenerator()->create_course();
+        $course2 = $this->getDataGenerator()->create_course();
+        $course1 = $this->getDataGenerator()->create_course();
+        $forum1 = $this->getDataGenerator()->create_module('forum', array('course' => $course1->id));
+        $forum2 = $this->getDataGenerator()->create_module('forum', array('course' => $course2->id));
+        $forum3 = $this->getDataGenerator()->create_module('forum', array('course' => $course3->id));
+        $post11 = $forumgenerator->create_content($forum1, array('tags' => array('Cats', 'Dogs')));
+        $post12 = $forumgenerator->create_content($forum1, array('tags' => array('Cats', 'mice')));
+        $post13 = $forumgenerator->create_content($forum1, array('tags' => array('Cats')));
+        $post14 = $forumgenerator->create_content($forum1);
+        $post15 = $forumgenerator->create_content($forum1, array('tags' => array('Cats')));
+        $post16 = $forumgenerator->create_content($forum1, array('tags' => array('Cats'), 'hidden' => true));
+        $post21 = $forumgenerator->create_content($forum2, array('tags' => array('Cats')));
+        $post22 = $forumgenerator->create_content($forum2, array('tags' => array('Cats', 'Dogs')));
+        $post23 = $forumgenerator->create_content($forum2, array('tags' => array('mice', 'Cats')));
+        $post31 = $forumgenerator->create_content($forum3, array('tags' => array('mice', 'Cats')));
+
+        $tag = core_tag_tag::get_by_name(0, 'Cats');
+
+        // Admin can see everything.
+        $res = mod_forum_get_tagged_posts($tag, /*$exclusivemode = */false,
+            /*$fromctx = */0, /*$ctx = */0, /*$rec = */1, /*$post = */0);
+        $this->assertRegExp('/'.$post11->subject.'</', $res->content);
+        $this->assertRegExp('/'.$post12->subject.'</', $res->content);
+        $this->assertRegExp('/'.$post13->subject.'</', $res->content);
+        $this->assertNotRegExp('/'.$post14->subject.'</', $res->content);
+        $this->assertRegExp('/'.$post15->subject.'</', $res->content);
+        $this->assertRegExp('/'.$post16->subject.'</', $res->content);
+        $this->assertNotRegExp('/'.$post21->subject.'</', $res->content);
+        $this->assertNotRegExp('/'.$post22->subject.'</', $res->content);
+        $this->assertNotRegExp('/'.$post23->subject.'</', $res->content);
+        $this->assertNotRegExp('/'.$post31->subject.'</', $res->content);
+        $this->assertEmpty($res->prevpageurl);
+        $this->assertNotEmpty($res->nextpageurl);
+        $res = mod_forum_get_tagged_posts($tag, /*$exclusivemode = */false,
+            /*$fromctx = */0, /*$ctx = */0, /*$rec = */1, /*$post = */1);
+        $this->assertNotRegExp('/'.$post11->subject.'</', $res->content);
+        $this->assertNotRegExp('/'.$post12->subject.'</', $res->content);
+        $this->assertNotRegExp('/'.$post13->subject.'</', $res->content);
+        $this->assertNotRegExp('/'.$post14->subject.'</', $res->content);
+        $this->assertNotRegExp('/'.$post15->subject.'</', $res->content);
+        $this->assertNotRegExp('/'.$post16->subject.'</', $res->content);
+        $this->assertRegExp('/'.$post21->subject.'</', $res->content);
+        $this->assertRegExp('/'.$post22->subject.'</', $res->content);
+        $this->assertRegExp('/'.$post23->subject.'</', $res->content);
+        $this->assertRegExp('/'.$post31->subject.'</', $res->content);
+        $this->assertNotEmpty($res->prevpageurl);
+        $this->assertEmpty($res->nextpageurl);
+
+        // Create and enrol a user.
+        $student = self::getDataGenerator()->create_user();
+        $studentrole = $DB->get_record('role', array('shortname' => 'student'));
+        $this->getDataGenerator()->enrol_user($student->id, $course1->id, $studentrole->id, 'manual');
+        $this->getDataGenerator()->enrol_user($student->id, $course2->id, $studentrole->id, 'manual');
+        $this->setUser($student);
+        core_tag_index_builder::reset_caches();
+
+        // User can not see posts in course 3 because he is not enrolled.
+        $res = mod_forum_get_tagged_posts($tag, /*$exclusivemode = */false,
+            /*$fromctx = */0, /*$ctx = */0, /*$rec = */1, /*$post = */1);
+        $this->assertRegExp('/'.$post22->subject.'/', $res->content);
+        $this->assertRegExp('/'.$post23->subject.'/', $res->content);
+        $this->assertNotRegExp('/'.$post31->subject.'/', $res->content);
+
+        // User can search forum posts inside a course.
+        $coursecontext = context_course::instance($course1->id);
+        $res = mod_forum_get_tagged_posts($tag, /*$exclusivemode = */false,
+            /*$fromctx = */0, /*$ctx = */$coursecontext->id, /*$rec = */1, /*$post = */0);
+        $this->assertRegExp('/'.$post11->subject.'/', $res->content);
+        $this->assertRegExp('/'.$post12->subject.'/', $res->content);
+        $this->assertRegExp('/'.$post13->subject.'/', $res->content);
+        $this->assertNotRegExp('/'.$post14->subject.'/', $res->content);
+        $this->assertRegExp('/'.$post15->subject.'/', $res->content);
+        $this->assertRegExp('/'.$post16->subject.'/', $res->content);
+        $this->assertNotRegExp('/'.$post21->subject.'/', $res->content);
+        $this->assertNotRegExp('/'.$post22->subject.'/', $res->content);
+        $this->assertNotRegExp('/'.$post23->subject.'/', $res->content);
+        $this->assertEmpty($res->nextpageurl);
+    }
+
     /**
      * Creates an action event.
      *
index 4246a2f..da77f51 100644 (file)
@@ -24,6 +24,6 @@
 
 defined('MOODLE_INTERNAL') || die();
 
-$plugin->version   = 2016120500;       // The current module version (Date: YYYYMMDDXX)
+$plugin->version   = 2016120501;       // The current module version (Date: YYYYMMDDXX)
 $plugin->requires  = 2016112900;       // Requires this Moodle version
 $plugin->component = 'mod_forum';      // Full name of the plugin (used for diagnostics)
index 064fb0c..a2a00ae 100644 (file)
@@ -4171,7 +4171,10 @@ function forum_get_fontawesome_icon_map() {
 }
 
 /**
- * Handles creating actions for events.
+ * This function receives a calendar event and returns the action associated with it, or null if there is none.
+ *
+ * This is used by block_myoverview in order to display the event appropriately. If null is returned then the event
+ * is not displayed on the block.
  *
  * @param calendar_event $event
  * @param \core_calendar\action_factory $factory
index 9ab5de7..bf096aa 100644 (file)
@@ -474,7 +474,10 @@ function imscp_check_updates_since(cm_info $cm, $from, $filter = array()) {
 }
 
 /**
- * Handles creating actions for events.
+ * This function receives a calendar event and returns the action associated with it, or null if there is none.
+ *
+ * This is used by block_myoverview in order to display the event appropriately. If null is returned then the event
+ * is not displayed on the block.
  *
  * @param calendar_event $event
  * @param \core_calendar\action_factory $factory
index cffa7ed..44ca596 100644 (file)
@@ -351,7 +351,10 @@ function label_check_updates_since(cm_info $cm, $from, $filter = array()) {
 }
 
 /**
- * Handles creating actions for events.
+ * This function receives a calendar event and returns the action associated with it, or null if there is none.
+ *
+ * This is used by block_myoverview in order to display the event appropriately. If null is returned then the event
+ * is not displayed on the block.
  *
  * @param calendar_event $event
  * @param \core_calendar\action_factory $factory
index 2ea9746..0e21644 100644 (file)
@@ -532,7 +532,7 @@ function lesson_user_complete($course, $user, $mod, $lesson) {
  * and it is available for taking.
  *
  * @deprecated since 3.3
- *
+ * @todo The final deprecation of this function will take place in Moodle 3.7 - see MDL-57487.
  * @global object
  * @global stdClass
  * @global object
@@ -1617,7 +1617,10 @@ function lesson_check_updates_since(cm_info $cm, $from, $filter = array()) {
 }
 
 /**
- * Handles creating actions for events.
+ * This function receives a calendar event and returns the action associated with it, or null if there is none.
+ *
+ * This is used by block_myoverview in order to display the event appropriately. If null is returned then the event
+ * is not displayed on the block.
  *
  * @param calendar_event $event
  * @param \core_calendar\action_factory $factory
index 215be10..18bc722 100644 (file)
@@ -649,7 +649,10 @@ function mod_lti_get_fontawesome_icon_map() {
 }
 
 /**
- * Handles creating actions for events.
+ * This function receives a calendar event and returns the action associated with it, or null if there is none.
+ *
+ * This is used by block_myoverview in order to display the event appropriately. If null is returned then the event
+ * is not displayed on the block.
  *
  * @param calendar_event $event
  * @param \core_calendar\action_factory $factory
index e52401f..93609fd 100644 (file)
@@ -538,7 +538,10 @@ function page_check_updates_since(cm_info $cm, $from, $filter = array()) {
 }
 
 /**
- * Handles creating actions for events.
+ * This function receives a calendar event and returns the action associated with it, or null if there is none.
+ *
+ * This is used by block_myoverview in order to display the event appropriately. If null is returned then the event
+ * is not displayed on the block.
  *
  * @param calendar_event $event
  * @param \core_calendar\action_factory $factory
index 3f47842..23d894c 100644 (file)
@@ -1555,8 +1555,8 @@ function quiz_reset_userdata($data) {
  * Prints quiz summaries on MyMoodle Page
  *
  * @deprecated since 3.3
- *
- * @param arry $courses
+ * @todo The final deprecation of this function will take place in Moodle 3.7 - see MDL-57487.
+ * @param array $courses
  * @param array $htmlarray
  */
 function quiz_print_overview($courses, &$htmlarray) {
@@ -2106,7 +2106,10 @@ function mod_quiz_get_fontawesome_icon_map() {
 }
 
 /**
- * Handles creating actions for events.
+ * This function receives a calendar event and returns the action associated with it, or null if there is none.
+ *
+ * This is used by block_myoverview in order to display the event appropriately. If null is returned then the event
+ * is not displayed on the block.
  *
  * @param calendar_event $event
  * @param \core_calendar\action_factory $factory
index 4819901..b26ec98 100644 (file)
@@ -553,7 +553,10 @@ function resource_check_updates_since(cm_info $cm, $from, $filter = array()) {
 }
 
 /**
- * Handles creating actions for events.
+ * This function receives a calendar event and returns the action associated with it, or null if there is none.
+ *
+ * This is used by block_myoverview in order to display the event appropriately. If null is returned then the event
+ * is not displayed on the block.
  *
  * @param calendar_event $event
  * @param \core_calendar\action_factory $factory
index 0b740f5..1ad9121 100644 (file)
@@ -143,13 +143,13 @@ class mod_resource_lib_testcase extends advanced_testcase {
         $info = resource_get_coursemodule_info(
                 $DB->get_record('course_modules', array('id' => $resource2->cmid)));
         $this->assertEquals('R2', $info->name);
-        $this->assertEquals('f/text', $info->icon);
+        $this->assertEquals('f/text-24', $info->icon);
 
         // For third one, it should use the highest sortorder icon.
         $info = resource_get_coursemodule_info(
                 $DB->get_record('course_modules', array('id' => $resource3->cmid)));
         $this->assertEquals('R3', $info->name);
-        $this->assertEquals('f/document', $info->icon);
+        $this->assertEquals('f/document-24', $info->icon);
     }
 
     public function test_resource_core_calendar_provide_event_action() {
index b1a306e..4bd3045 100644 (file)
@@ -1091,7 +1091,7 @@ function scorm_debug_log_remove($type, $scoid) {
  * writes overview info for course_overview block - displays upcoming scorm objects that have a due date
  *
  * @deprecated since 3.3
- *
+ * @todo The final deprecation of this function will take place in Moodle 3.7 - see MDL-57487.
  * @param object $type - type of log(aicc,scorm12,scorm13) used as prefix for filename
  * @param array $htmlarray
  * @return mixed
@@ -1616,7 +1616,10 @@ function scorm_refresh_events($courseid = 0) {
 }
 
 /**
- * Handles creating actions for events.
+ * This function receives a calendar event and returns the action associated with it, or null if there is none.
+ *
+ * This is used by block_myoverview in order to display the event appropriately. If null is returned then the event
+ * is not displayed on the block.
  *
  * @param calendar_event $event
  * @param \core_calendar\action_factory $factory
index 66c542d..15e13e4 100644 (file)
@@ -1110,7 +1110,10 @@ function survey_check_updates_since(cm_info $cm, $from, $filter = array()) {
 }
 
 /**
- * Handles creating actions for events.
+ * This function receives a calendar event and returns the action associated with it, or null if there is none.
+ *
+ * This is used by block_myoverview in order to display the event appropriately. If null is returned then the event
+ * is not displayed on the block.
  *
  * @param calendar_event $event
  * @param \core_calendar\action_factory $factory
index 3581efc..f4f1052 100644 (file)
@@ -3,11 +3,18 @@ information provided here is intended especially for developers.
 
 === 3.3 ===
 
- * External functions that were returning file information now return the following additional file fields:
-   - mimetype (the file mime type)
-   - isexternalfile (if is a file reference to a external repository)
-   - repositorytype (the repository name in case is a external file)
-   Those fields are VALUE_OPTIONAL for backwards compatibility.
+* External functions that were returning file information now return the following additional file fields:
+  - mimetype (the file mime type)
+  - isexternalfile (if is a file reference to a external repository)
+  - repositorytype (the repository name in case is a external file)
+  Those fields are VALUE_OPTIONAL for backwards compatibility.
+* The block_course_overview has been removed and the related core module *_print_overview functions have been deprecated.
+* The block_myoverview has replaced block_course_overview to provide better information to students. To support this,
+  actions can now be attached to calendar events. Documentation for the following new API callbacks introduced in
+  MDL-55611 can be found at https://docs.moodle.org/dev/Calendar_API. The 3 new callbacks are:
+  - mod_<modname>_core_calendar_is_event_visible
+  - mod_<modname>_core_calendar_provide_event_action
+  - mod_<modname>_core_calendar_event_action_show_items_acount
 
 === 3.2 ===
 
index 5eac9f2..779fd4a 100644 (file)
@@ -376,7 +376,10 @@ function url_check_updates_since(cm_info $cm, $from, $filter = array()) {
 }
 
 /**
- * Handles creating actions for events.
+ * This function receives a calendar event and returns the action associated with it, or null if there is none.
+ *
+ * This is used by block_myoverview in order to display the event appropriately. If null is returned then the event
+ * is not displayed on the block.
  *
  * @param calendar_event $event
  * @param \core_calendar\action_factory $factory
index aed68ce..7b08b74 100644 (file)
@@ -802,7 +802,10 @@ function mod_wiki_get_fontawesome_icon_map() {
 }
 
 /**
- * Handles creating actions for events.
+ * This function receives a calendar event and returns the action associated with it, or null if there is none.
+ *
+ * This is used by block_myoverview in order to display the event appropriately. If null is returned then the event
+ * is not displayed on the block.
  *
  * @param calendar_event $event
  * @param \core_calendar\action_factory $factory
index e34a7d6..635774d 100644 (file)
@@ -1787,7 +1787,10 @@ function workshop_calendar_update(stdClass $workshop, $cmid) {
 }
 
 /**
- * Handles creating actions for events.
+ * This function receives a calendar event and returns the action associated with it, or null if there is none.
+ *
+ * This is used by block_myoverview in order to display the event appropriately. If null is returned then the event
+ * is not displayed on the block.
  *
  * @param calendar_event $event
  * @param \core_calendar\action_factory $factory
diff --git a/pix/f/FileTypesIcons-LICENSE.txt b/pix/f/FileTypesIcons-LICENSE.txt
new file mode 100644 (file)
index 0000000..9adcbae
--- /dev/null
@@ -0,0 +1,15 @@
+
+   FileTypesIcons   Copyright 2012 Renato Veras
+
+   These icons are 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.
+
+   This program 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 this program.  If not, see <http://www.gnu.org/licenses/>.
diff --git a/pix/f/Oxygen-LICENSE.txt b/pix/f/Oxygen-LICENSE.txt
new file mode 100644 (file)
index 0000000..a62ec8e
--- /dev/null
@@ -0,0 +1,15 @@
+
+   FileTypesIcons   Copyright 2012 Oxygen Team