Merge branch 'MDL-58388-master' of https://github.com/abias/moodle
authorDamyon Wiese <damyon@moodle.com>
Thu, 19 Oct 2017 04:47:18 +0000 (12:47 +0800)
committerDamyon Wiese <damyon@moodle.com>
Thu, 19 Oct 2017 04:47:18 +0000 (12:47 +0800)
651 files changed:
.eslintignore
.stylelintignore
admin/index.php
admin/renderer.php
admin/settings/analytics.php
admin/tool/analytics/amd/build/model.min.js [new file with mode: 0644]
admin/tool/analytics/amd/src/model.js [new file with mode: 0644]
admin/tool/analytics/classes/output/invalid_analysables.php [new file with mode: 0644]
admin/tool/analytics/classes/output/models_list.php
admin/tool/analytics/classes/output/renderer.php
admin/tool/analytics/lang/en/tool_analytics.php
admin/tool/analytics/model.php
admin/tool/analytics/templates/invalid_analysables.mustache [new file with mode: 0644]
admin/tool/analytics/templates/models_list.mustache
admin/tool/httpsreplace/classes/form.php [new file with mode: 0644]
admin/tool/httpsreplace/classes/url_finder.php [new file with mode: 0644]
admin/tool/httpsreplace/cli/url_replace.php [new file with mode: 0644]
admin/tool/httpsreplace/index.php [new file with mode: 0644]
admin/tool/httpsreplace/lang/en/tool_httpsreplace.php [new file with mode: 0644]
admin/tool/httpsreplace/settings.php [new file with mode: 0644]
admin/tool/httpsreplace/tests/behat/httpsreplace.feature [new file with mode: 0644]
admin/tool/httpsreplace/tests/httpsreplace_test.php [new file with mode: 0644]
admin/tool/httpsreplace/tool.php [new file with mode: 0644]
admin/tool/httpsreplace/version.php [new file with mode: 0644]
admin/tool/mobile/classes/api.php
admin/tool/mobile/lang/en/tool_mobile.php
admin/tool/mobile/tests/api_test.php
admin/tool/usertours/amd/build/usertours.min.js
admin/tool/usertours/amd/src/usertours.js
admin/tool/usertours/templates/resettour.mustache [new file with mode: 0644]
admin/webservice/testclient.php
admin/webservice/testclient_forms.php
analytics/classes/admin_setting_predictor.php
analytics/classes/analysable.php
analytics/classes/calculable.php
analytics/classes/course.php
analytics/classes/dataset_manager.php
analytics/classes/local/analyser/base.php
analytics/classes/local/analyser/by_course.php
analytics/classes/local/analyser/sitewide.php
analytics/classes/local/target/base.php
analytics/classes/local/target/discrete.php
analytics/classes/local/target/linear.php
analytics/classes/manager.php
analytics/classes/model.php
analytics/classes/site.php
analytics/tests/dataset_manager_test.php
analytics/tests/fixtures/test_analyser.php [new file with mode: 0644]
analytics/tests/model_test.php
analytics/tests/prediction_test.php
auth/cas/CAS/CAS/Autoload.php
auth/cas/CAS/moodle_readme.txt
auth/ldap/classes/admin_setting_special_contexts_configtext.php
backup/backupfilesedit.php
backup/moodle2/restore_activity_task.class.php
backup/moodle2/restore_block_task.class.php
backup/moodle2/restore_course_task.class.php
backup/moodle2/restore_final_task.class.php
backup/moodle2/restore_stepslib.php
backup/moodle2/tests/moodle2_test.php
backup/restorefile.php
backup/util/structure/base_nested_element.class.php
backup/util/ui/renderer.php
badges/award.php
badges/criteria/award_criteria.php
badges/criteria/award_criteria_manual.php
badges/criteria/award_criteria_profile.php
badges/renderer.php
blocks/activity_results/block_activity_results.php
blocks/calendar_upcoming/block_calendar_upcoming.php
cache/disabledlib.php
calendar/amd/build/calendar.min.js
calendar/amd/build/calendar_filter.min.js
calendar/amd/build/drag_drop_data_store.min.js
calendar/amd/build/event_form.min.js
calendar/amd/build/month_navigation_drag_drop.min.js
calendar/amd/build/month_view_drag_drop.min.js
calendar/amd/src/calendar.js
calendar/amd/src/calendar_filter.js
calendar/amd/src/drag_drop_data_store.js
calendar/amd/src/event_form.js
calendar/amd/src/month_navigation_drag_drop.js
calendar/amd/src/month_view_drag_drop.js
calendar/classes/external/calendar_event_exporter.php
calendar/classes/external/day_exporter.php
calendar/classes/external/event_exporter_base.php
calendar/classes/external/event_subscription_exporter.php
calendar/classes/external/footer_options_exporter.php
calendar/classes/local/event/forms/create.php
calendar/classes/local/event/mappers/create_update_form_mapper.php
calendar/lib.php
calendar/templates/event_filter_key.mustache
calendar/templates/event_icon.mustache [new file with mode: 0644]
calendar/templates/event_subscription.mustache
calendar/templates/footer_options.mustache
calendar/templates/month_detailed.mustache
calendar/templates/upcoming_mini.mustache [new file with mode: 0644]
calendar/tests/behat/calendar.feature
calendar/tests/calendar_event_exporter_test.php [new file with mode: 0644]
calendar/tests/lib_test.php
cohort/lib.php
completion/classes/external.php
completion/tests/externallib_test.php
composer.json
composer.lock
course/classes/analytics/indicator/no_student.php [new file with mode: 0644]
course/classes/search/mycourse.php
course/externallib.php
course/lib.php
course/renderer.php
course/tests/courselib_test.php
course/tests/search_test.php
enrol/cohort/lib.php
enrol/cohort/tests/cohortlib_test.php
enrol/database/lib.php
enrol/database/tests/lib_test.php [new file with mode: 0644]
enrol/flatfile/lib.php
enrol/flatfile/tests/flatfile_test.php
enrol/locallib.php
enrol/lti/lib.php
enrol/lti/tests/lib_test.php
enrol/manual/lib.php
enrol/manual/tests/lib_test.php
enrol/meta/lib.php
enrol/meta/tests/plugin_test.php
enrol/paypal/lib.php
enrol/paypal/tests/paypal_test.php
enrol/self/lib.php
enrol/self/tests/self_test.php
files/converter/googledrive/classes/converter.php
filter/classes/external.php [new file with mode: 0644]
filter/mathjaxloader/db/upgrade.php
filter/mathjaxloader/db/upgradelib.php
filter/mathjaxloader/filter.php
filter/mathjaxloader/readme_moodle.txt
filter/mathjaxloader/settings.php
filter/mathjaxloader/tests/filter_test.php [new file with mode: 0644]
filter/mathjaxloader/tests/upgradelib_test.php
filter/mathjaxloader/version.php
filter/tests/external_test.php [new file with mode: 0644]
filter/urltolink/filter.php
group/tests/behat/create_groups.feature
install/lang/ja/install.php
install/lang/lt/admin.php
install/lang/lt/install.php
lang/en/admin.php
lang/en/analytics.php
lang/en/calendar.php
lang/en/completion.php
lang/en/deprecated.txt
lang/en/moodle.php
lang/en/role.php
lib/accesslib.php
lib/adminlib.php
lib/adodb/adodb-active-record.inc.php
lib/adodb/adodb-active-recordx.inc.php
lib/adodb/adodb-csvlib.inc.php
lib/adodb/adodb-datadict.inc.php
lib/adodb/adodb-error.inc.php
lib/adodb/adodb-errorhandler.inc.php
lib/adodb/adodb-errorpear.inc.php
lib/adodb/adodb-exceptions.inc.php
lib/adodb/adodb-iterator.inc.php
lib/adodb/adodb-lib.inc.php
lib/adodb/adodb-memcache.lib.inc.php
lib/adodb/adodb-pager.inc.php
lib/adodb/adodb-pear.inc.php
lib/adodb/adodb-perf.inc.php
lib/adodb/adodb-php4.inc.php
lib/adodb/adodb-time.inc.php
lib/adodb/adodb.inc.php
lib/adodb/datadict/datadict-access.inc.php
lib/adodb/datadict/datadict-db2.inc.php
lib/adodb/datadict/datadict-firebird.inc.php
lib/adodb/datadict/datadict-generic.inc.php
lib/adodb/datadict/datadict-ibase.inc.php
lib/adodb/datadict/datadict-informix.inc.php
lib/adodb/datadict/datadict-mssql.inc.php
lib/adodb/datadict/datadict-mssqlnative.inc.php
lib/adodb/datadict/datadict-mysql.inc.php
lib/adodb/datadict/datadict-oci8.inc.php
lib/adodb/datadict/datadict-postgres.inc.php
lib/adodb/datadict/datadict-sapdb.inc.php
lib/adodb/datadict/datadict-sqlite.inc.php
lib/adodb/datadict/datadict-sybase.inc.php
lib/adodb/drivers/adodb-access.inc.php
lib/adodb/drivers/adodb-ado.inc.php
lib/adodb/drivers/adodb-ado5.inc.php
lib/adodb/drivers/adodb-ado_access.inc.php
lib/adodb/drivers/adodb-ado_mssql.inc.php
lib/adodb/drivers/adodb-borland_ibase.inc.php
lib/adodb/drivers/adodb-csv.inc.php
lib/adodb/drivers/adodb-db2.inc.php
lib/adodb/drivers/adodb-db2oci.inc.php
lib/adodb/drivers/adodb-db2ora.inc.php
lib/adodb/drivers/adodb-fbsql.inc.php
lib/adodb/drivers/adodb-firebird.inc.php
lib/adodb/drivers/adodb-ibase.inc.php
lib/adodb/drivers/adodb-informix.inc.php
lib/adodb/drivers/adodb-informix72.inc.php
lib/adodb/drivers/adodb-ldap.inc.php
lib/adodb/drivers/adodb-mssql.inc.php
lib/adodb/drivers/adodb-mssqlnative.inc.php
lib/adodb/drivers/adodb-mssqlpo.inc.php
lib/adodb/drivers/adodb-mysql.inc.php
lib/adodb/drivers/adodb-mysqli.inc.php
lib/adodb/drivers/adodb-mysqlpo.inc.php
lib/adodb/drivers/adodb-mysqlt.inc.php
lib/adodb/drivers/adodb-netezza.inc.php
lib/adodb/drivers/adodb-oci8.inc.php
lib/adodb/drivers/adodb-oci805.inc.php
lib/adodb/drivers/adodb-oci8po.inc.php
lib/adodb/drivers/adodb-oci8quercus.inc.php
lib/adodb/drivers/adodb-odbc.inc.php
lib/adodb/drivers/adodb-odbc_db2.inc.php
lib/adodb/drivers/adodb-odbc_mssql.inc.php
lib/adodb/drivers/adodb-odbc_mssql2012.inc.php [deleted file]
lib/adodb/drivers/adodb-odbc_oracle.inc.php
lib/adodb/drivers/adodb-odbtp.inc.php
lib/adodb/drivers/adodb-odbtp_unicode.inc.php
lib/adodb/drivers/adodb-oracle.inc.php
lib/adodb/drivers/adodb-pdo.inc.php
lib/adodb/drivers/adodb-pdo_mssql.inc.php
lib/adodb/drivers/adodb-pdo_mysql.inc.php
lib/adodb/drivers/adodb-pdo_oci.inc.php
lib/adodb/drivers/adodb-pdo_pgsql.inc.php
lib/adodb/drivers/adodb-pdo_sqlite.inc.php
lib/adodb/drivers/adodb-postgres.inc.php
lib/adodb/drivers/adodb-postgres64.inc.php
lib/adodb/drivers/adodb-postgres7.inc.php
lib/adodb/drivers/adodb-postgres8.inc.php
lib/adodb/drivers/adodb-postgres9.inc.php
lib/adodb/drivers/adodb-proxy.inc.php
lib/adodb/drivers/adodb-sapdb.inc.php
lib/adodb/drivers/adodb-sqlanywhere.inc.php
lib/adodb/drivers/adodb-sqlite.inc.php
lib/adodb/drivers/adodb-sqlite3.inc.php
lib/adodb/drivers/adodb-sqlitepo.inc.php
lib/adodb/drivers/adodb-sybase.inc.php
lib/adodb/drivers/adodb-sybase_ase.inc.php
lib/adodb/drivers/adodb-text.inc.php
lib/adodb/drivers/adodb-vfp.inc.php
lib/adodb/perf/perf-db2.inc.php
lib/adodb/perf/perf-informix.inc.php
lib/adodb/perf/perf-mssql.inc.php
lib/adodb/perf/perf-mssqlnative.inc.php
lib/adodb/perf/perf-mysql.inc.php
lib/adodb/perf/perf-oci8.inc.php
lib/adodb/perf/perf-postgres.inc.php
lib/adodb/pivottable.inc.php
lib/adodb/readme_moodle.txt
lib/adodb/rsfilter.inc.php
lib/adodb/toexport.inc.php
lib/adodb/tohtml.inc.php
lib/amd/build/loglevel.min.js
lib/amd/build/mustache.min.js
lib/amd/src/loglevel.js
lib/amd/src/mustache.js
lib/behat/form_field/behat_form_select.php
lib/bennu/readme_moodle.txt
lib/classes/access/get_user_capability_course_helper.php [new file with mode: 0644]
lib/classes/analytics/target/course_dropout.php
lib/classes/analytics/target/no_teaching.php
lib/classes/component.php
lib/classes/event/course_module_completion_updated.php
lib/classes/event/user_info_category_created.php [new file with mode: 0644]
lib/classes/event/user_info_category_deleted.php [new file with mode: 0644]
lib/classes/event/user_info_category_updated.php [new file with mode: 0644]
lib/classes/event/user_info_field_created.php [new file with mode: 0644]
lib/classes/event/user_info_field_deleted.php [new file with mode: 0644]
lib/classes/event/user_info_field_updated.php [new file with mode: 0644]
lib/classes/oauth2/client.php
lib/classes/output/icon_system_fontawesome.php
lib/classes/plugin_manager.php
lib/classes/task/search_index_task.php
lib/classes/task/search_optimize_task.php
lib/classes/text.php
lib/completionlib.php
lib/db/access.php
lib/db/install.xml
lib/db/services.php
lib/db/upgrade.php
lib/dml/moodle_database.php
lib/dml/mysqli_native_moodle_database.php
lib/dml/pgsql_native_moodle_database.php
lib/dml/tests/dml_test.php
lib/enrollib.php
lib/filebrowser/file_info_context_course.php
lib/filebrowser/tests/file_browser_test.php
lib/filelib.php
lib/filestorage/file_storage.php
lib/form/form.js
lib/formslib.php
lib/html2text/Html2Text.php
lib/htmlpurifier/HTMLPurifier/AttrDef/URI/Host.php
lib/htmlpurifier/readme_moodle.txt
lib/jquery/jquery-3.1.0.min.js [deleted file]
lib/jquery/jquery-3.2.1.js [moved from lib/jquery/jquery-3.1.0.js with 94% similarity]
lib/jquery/jquery-3.2.1.min.js [new file with mode: 0644]
lib/jquery/plugins.php
lib/maxmind/GeoIp2/Database/Reader.php
lib/maxmind/GeoIp2/Exception/HttpException.php
lib/maxmind/GeoIp2/Exception/InvalidRequestException.php
lib/maxmind/GeoIp2/Model/AbstractModel.php
lib/maxmind/GeoIp2/Model/AnonymousIp.php
lib/maxmind/GeoIp2/Model/Asn.php [new file with mode: 0644]
lib/maxmind/GeoIp2/Model/City.php
lib/maxmind/GeoIp2/Model/ConnectionType.php
lib/maxmind/GeoIp2/Model/Country.php
lib/maxmind/GeoIp2/Model/Domain.php
lib/maxmind/GeoIp2/Model/Enterprise.php
lib/maxmind/GeoIp2/Model/Insights.php
lib/maxmind/GeoIp2/Model/Isp.php
lib/maxmind/GeoIp2/ProviderInterface.php
lib/maxmind/GeoIp2/Record/AbstractPlaceRecord.php
lib/maxmind/GeoIp2/Record/AbstractRecord.php
lib/maxmind/GeoIp2/Record/City.php
lib/maxmind/GeoIp2/Record/Continent.php
lib/maxmind/GeoIp2/Record/Country.php
lib/maxmind/GeoIp2/Record/Location.php
lib/maxmind/GeoIp2/Record/MaxMind.php
lib/maxmind/GeoIp2/Record/Postal.php
lib/maxmind/GeoIp2/Record/RepresentedCountry.php
lib/maxmind/GeoIp2/Record/Subdivision.php
lib/maxmind/GeoIp2/Record/Traits.php
lib/maxmind/GeoIp2/WebService/Client.php
lib/maxmind/MaxMind/Db/Reader.php
lib/maxmind/readme_moodle.txt [moved from lib/maxmind/README_moodle.txt with 77% similarity]
lib/mlbackend/php/classes/processor.php
lib/mlbackend/python/classes/processor.php
lib/moodlelib.php
lib/mustache/readme_moodle.txt
lib/mustache/src/Mustache/Autoloader.php
lib/mustache/src/Mustache/Cache.php
lib/mustache/src/Mustache/Cache/AbstractCache.php
lib/mustache/src/Mustache/Cache/FilesystemCache.php
lib/mustache/src/Mustache/Cache/NoopCache.php
lib/mustache/src/Mustache/Compiler.php
lib/mustache/src/Mustache/Context.php
lib/mustache/src/Mustache/Engine.php
lib/mustache/src/Mustache/Exception.php
lib/mustache/src/Mustache/Exception/InvalidArgumentException.php
lib/mustache/src/Mustache/Exception/LogicException.php
lib/mustache/src/Mustache/Exception/RuntimeException.php
lib/mustache/src/Mustache/Exception/SyntaxException.php
lib/mustache/src/Mustache/Exception/UnknownFilterException.php
lib/mustache/src/Mustache/Exception/UnknownHelperException.php
lib/mustache/src/Mustache/Exception/UnknownTemplateException.php
lib/mustache/src/Mustache/HelperCollection.php
lib/mustache/src/Mustache/LambdaHelper.php
lib/mustache/src/Mustache/Loader.php
lib/mustache/src/Mustache/Loader/ArrayLoader.php
lib/mustache/src/Mustache/Loader/CascadingLoader.php
lib/mustache/src/Mustache/Loader/FilesystemLoader.php
lib/mustache/src/Mustache/Loader/InlineLoader.php
lib/mustache/src/Mustache/Loader/MutableLoader.php
lib/mustache/src/Mustache/Loader/ProductionFilesystemLoader.php [new file with mode: 0644]
lib/mustache/src/Mustache/Loader/StringLoader.php
lib/mustache/src/Mustache/Logger.php
lib/mustache/src/Mustache/Logger/AbstractLogger.php
lib/mustache/src/Mustache/Logger/StreamLogger.php
lib/mustache/src/Mustache/Parser.php
lib/mustache/src/Mustache/Source.php [new file with mode: 0644]
lib/mustache/src/Mustache/Source/FilesystemSource.php [new file with mode: 0644]
lib/mustache/src/Mustache/Template.php
lib/mustache/src/Mustache/Tokenizer.php
lib/myprofilelib.php
lib/navigationlib.php
lib/outputcomponents.php
lib/pear/HTML/QuickForm/Rule/Compare.php
lib/pear/HTML/QuickForm/date.php
lib/pear/PEAR.php
lib/pear/README_MOODLE.txt
lib/phpmailer/README.md
lib/phpmailer/README_MOODLE.txt
lib/phpmailer/VERSION
lib/phpmailer/changelog.md
lib/phpmailer/language/phpmailer.lang-ba.php [new file with mode: 0644]
lib/phpmailer/language/phpmailer.lang-cs.php [moved from lib/phpmailer/language/phpmailer.lang-cz.php with 95% similarity]
lib/phpmailer/language/phpmailer.lang-da.php [moved from lib/phpmailer/language/phpmailer.lang-dk.php with 100% similarity]
lib/phpmailer/language/phpmailer.lang-fr.php
lib/phpmailer/language/phpmailer.lang-nb.php [new file with mode: 0644]
lib/phpmailer/language/phpmailer.lang-nl.php
lib/phpmailer/language/phpmailer.lang-no.php [deleted file]
lib/phpmailer/language/phpmailer.lang-pt_br.php [moved from lib/phpmailer/language/phpmailer.lang-br.php with 65% similarity]
lib/phpmailer/language/phpmailer.lang-rs.php [moved from lib/phpmailer/language/phpmailer.lang-sr.php with 96% similarity]
lib/phpmailer/language/phpmailer.lang-sv.php [moved from lib/phpmailer/language/phpmailer.lang-se.php with 73% similarity]
lib/phpmailer/language/phpmailer.lang-tr.php
lib/phpmailer/language/phpmailer.lang-zh_cn.php
lib/phpmailer/moodle_phpmailer.php
lib/phpmailer/src/Exception.php [new file with mode: 0644]
lib/phpmailer/src/PHPMailer.php [moved from lib/phpmailer/class.phpmailer.php with 66% similarity]
lib/phpmailer/src/SMTP.php [moved from lib/phpmailer/class.smtp.php with 70% similarity]
lib/phpunit/classes/base_testcase.php
lib/requirejs/moodle-config.js
lib/requirejs/require.js
lib/requirejs/require.min.js
lib/setuplib.php
lib/spout/readme_moodle.txt
lib/spout/src/Spout/Common/Escaper/ODS.php
lib/spout/src/Spout/Common/Escaper/XLSX.php
lib/spout/src/Spout/Common/Helper/FileSystemHelper.php
lib/spout/src/Spout/Reader/AbstractReader.php
lib/spout/src/Spout/Reader/CSV/Reader.php
lib/spout/src/Spout/Reader/CSV/ReaderOptions.php [new file with mode: 0644]
lib/spout/src/Spout/Reader/CSV/RowIterator.php
lib/spout/src/Spout/Reader/CSV/Sheet.php
lib/spout/src/Spout/Reader/CSV/SheetIterator.php
lib/spout/src/Spout/Reader/Common/ReaderOptions.php [new file with mode: 0644]
lib/spout/src/Spout/Reader/Common/XMLProcessor.php [new file with mode: 0644]
lib/spout/src/Spout/Reader/ODS/Helper/CellValueFormatter.php
lib/spout/src/Spout/Reader/ODS/Helper/SettingsHelper.php [new file with mode: 0644]
lib/spout/src/Spout/Reader/ODS/Reader.php
lib/spout/src/Spout/Reader/ODS/ReaderOptions.php [new file with mode: 0644]
lib/spout/src/Spout/Reader/ODS/RowIterator.php
lib/spout/src/Spout/Reader/ODS/Sheet.php
lib/spout/src/Spout/Reader/ODS/SheetIterator.php
lib/spout/src/Spout/Reader/Wrapper/SimpleXMLElement.php [deleted file]
lib/spout/src/Spout/Reader/Wrapper/XMLReader.php
lib/spout/src/Spout/Reader/XLSX/Helper/CellHelper.php
lib/spout/src/Spout/Reader/XLSX/Helper/CellValueFormatter.php
lib/spout/src/Spout/Reader/XLSX/Helper/DateFormatHelper.php
lib/spout/src/Spout/Reader/XLSX/Helper/SharedStringsHelper.php
lib/spout/src/Spout/Reader/XLSX/Helper/SheetHelper.php
lib/spout/src/Spout/Reader/XLSX/Helper/StyleHelper.php
lib/spout/src/Spout/Reader/XLSX/Reader.php
lib/spout/src/Spout/Reader/XLSX/ReaderOptions.php [new file with mode: 0644]
lib/spout/src/Spout/Reader/XLSX/RowIterator.php
lib/spout/src/Spout/Reader/XLSX/Sheet.php
lib/spout/src/Spout/Reader/XLSX/SheetIterator.php
lib/spout/src/Spout/Writer/AbstractWriter.php
lib/spout/src/Spout/Writer/Common/Helper/AbstractStyleHelper.php
lib/spout/src/Spout/Writer/Common/Helper/CellHelper.php
lib/spout/src/Spout/Writer/Common/Internal/AbstractWorkbook.php
lib/spout/src/Spout/Writer/Common/Sheet.php
lib/spout/src/Spout/Writer/ODS/Helper/FileSystemHelper.php
lib/spout/src/Spout/Writer/ODS/Helper/StyleHelper.php
lib/spout/src/Spout/Writer/ODS/Internal/Workbook.php
lib/spout/src/Spout/Writer/ODS/Internal/Worksheet.php
lib/spout/src/Spout/Writer/ODS/Writer.php
lib/spout/src/Spout/Writer/Style/Style.php
lib/spout/src/Spout/Writer/Style/StyleBuilder.php
lib/spout/src/Spout/Writer/WriterInterface.php
lib/spout/src/Spout/Writer/XLSX/Helper/FileSystemHelper.php
lib/spout/src/Spout/Writer/XLSX/Internal/Workbook.php
lib/spout/src/Spout/Writer/XLSX/Internal/Worksheet.php
lib/tcpdf/CHANGELOG.TXT
lib/tcpdf/README.TXT
lib/tcpdf/composer.json
lib/tcpdf/include/tcpdf_static.php
lib/tcpdf/readme_moodle.txt
lib/tcpdf/tcpdf.php
lib/templates/pix_icon_fontawesome.mustache
lib/tests/accesslib_test.php
lib/tests/admintree_test.php
lib/tests/completionlib_test.php
lib/tests/event_profile_field_test.php [new file with mode: 0644]
lib/tests/htmlpurifier_test.php
lib/tests/setuplib_test.php
lib/thirdpartylibs.xml
lib/upgrade.txt
lib/upgradelib.php
lib/webdavlib.php
media/player/videojs/amd/build/Youtube-lazy.min.js
media/player/videojs/amd/build/loader.min.js
media/player/videojs/amd/build/video-lazy.min.js
media/player/videojs/amd/build/videojs-flash-lazy.min.js [new file with mode: 0644]
media/player/videojs/amd/src/Youtube-lazy.js
media/player/videojs/amd/src/loader.js
media/player/videojs/amd/src/video-lazy.js
media/player/videojs/amd/src/videojs-flash-lazy.js [new file with mode: 0644]
media/player/videojs/classes/plugin.php
media/player/videojs/readme_moodle.txt
media/player/videojs/styles.css
media/player/videojs/thirdpartylibs.xml
media/player/videojs/videojs/lang/ar.js
media/player/videojs/videojs/lang/de.js
media/player/videojs/videojs/lang/en.js
media/player/videojs/videojs/lang/es.js
media/player/videojs/videojs/lang/fr.js
media/player/videojs/videojs/lang/gl.js [new file with mode: 0644]
media/player/videojs/videojs/lang/nl.js
media/player/videojs/videojs/lang/pt-PT.js [new file with mode: 0644]
media/player/videojs/videojs/lang/sk.js [new file with mode: 0644]
media/player/videojs/videojs/lang/tr.js
media/player/videojs/videojs/lang/vi.js
media/player/videojs/videojs/lang/zh-CN.js
media/player/videojs/videojs/video-js.swf
message/classes/search/base_message.php
message/classes/search/message_received.php
message/classes/search/message_sent.php
message/tests/api_test.php
message/tests/search_received_test.php [moved from message/tests/search_test_received.php with 77% similarity]
message/tests/search_sent_test.php [moved from message/tests/search_test_sent.php with 75% similarity]
mod/assign/amd/build/grading_actions.min.js
mod/assign/amd/build/grading_navigation.min.js
mod/assign/amd/build/grading_panel.min.js
mod/assign/amd/src/grading_actions.js
mod/assign/amd/src/grading_navigation.js
mod/assign/amd/src/grading_panel.js
mod/assign/feedback/editpdf/fpdi/LICENSE
mod/assign/feedback/editpdf/fpdi/filters/FilterASCII85.php
mod/assign/feedback/editpdf/fpdi/filters/FilterASCIIHexDecode.php
mod/assign/feedback/editpdf/fpdi/filters/FilterLZW.php
mod/assign/feedback/editpdf/fpdi/fpdf_tpl.php
mod/assign/feedback/editpdf/fpdi/fpdi.php
mod/assign/feedback/editpdf/fpdi/fpdi_bridge.php
mod/assign/feedback/editpdf/fpdi/fpdi_pdf_parser.php
mod/assign/feedback/editpdf/fpdi/pdf_context.php
mod/assign/feedback/editpdf/fpdi/pdf_parser.php
mod/assign/feedback/editpdf/fpdi/readme_moodle.txt
mod/assign/feedback/editpdf/thirdpartylibs.xml
mod/assign/feedback/editpdf/yui/build/moodle-assignfeedback_editpdf-editor/moodle-assignfeedback_editpdf-editor-debug.js
mod/assign/feedback/editpdf/yui/build/moodle-assignfeedback_editpdf-editor/moodle-assignfeedback_editpdf-editor-min.js
mod/assign/feedback/editpdf/yui/build/moodle-assignfeedback_editpdf-editor/moodle-assignfeedback_editpdf-editor.js
mod/assign/feedback/editpdf/yui/src/editor/js/comment.js
mod/assign/lib.php
mod/assign/locallib.php
mod/assign/templates/grading_actions.mustache
mod/assign/tests/lib_test.php
mod/book/classes/search/chapter.php
mod/book/lib.php
mod/book/tests/behat/reorganize_chapters.feature [new file with mode: 0644]
mod/book/tests/search_test.php
mod/chat/chatd.php
mod/choice/lib.php
mod/choice/tests/lib_test.php
mod/data/backup/moodle2/backup_data_stepslib.php
mod/data/backup/moodle2/restore_data_stepslib.php
mod/data/classes/search/entry.php
mod/data/db/tag.php [new file with mode: 0644]
mod/data/edit.php
mod/data/export.php
mod/data/export_form.php
mod/data/import.php
mod/data/lang/en/data.php
mod/data/lib.php
mod/data/locallib.php
mod/data/styles.css
mod/data/templates.php
mod/data/tests/behat/view_entries.feature
mod/data/tests/generator/lib.php
mod/data/tests/generator_test.php
mod/data/tests/lib_test.php
mod/data/tests/search_test.php
mod/data/version.php
mod/feedback/lang/en/feedback.php
mod/feedback/lib.php
mod/feedback/tests/lib_test.php
mod/forum/classes/search/post.php
mod/forum/db/install.xml
mod/forum/db/upgrade.php
mod/forum/lib.php
mod/forum/tests/search_test.php
mod/forum/version.php
mod/glossary/classes/search/entry.php
mod/glossary/lib.php
mod/glossary/tests/search_test.php
mod/lti/locallib.php
mod/quiz/attemptlib.php
mod/quiz/report/overview/report.php
mod/quiz/report/overview/tests/report_test.php
mod/quiz/report/responses/report.php
mod/quiz/report/statistics/tests/statistics_test.php
mod/scorm/aicc.php
mod/survey/classes/search/activity.php
mod/survey/graph.php
mod/survey/lib.php
mod/survey/tests/search_test.php [new file with mode: 0644]
mod/wiki/classes/search/collaborative_page.php
mod/wiki/diff/diff_nwiki.php
mod/wiki/diff/difflib.php
mod/wiki/tests/search_test.php
mod/workshop/allocation/random/lib.php
mod/workshop/classes/external.php
mod/workshop/lib.php
mod/workshop/tests/external_test.php
mod/workshop/tests/lib_test.php
phpunit.xml.dist
pix/i/categoryevent.png [new file with mode: 0644]
pix/i/categoryevent.svg
pix/i/completion-auto-n-override.png [new file with mode: 0644]
pix/i/completion-auto-n-override.svg [new file with mode: 0644]
pix/i/completion-auto-y-override.png [new file with mode: 0644]
pix/i/completion-auto-y-override.svg [new file with mode: 0644]
pix/i/completion-manual-n-override.png [new file with mode: 0644]
pix/i/completion-manual-n-override.svg [new file with mode: 0644]
pix/i/completion-manual-y-override.png [new file with mode: 0644]
pix/i/completion-manual-y-override.svg [new file with mode: 0644]
pix/i/siteevent.png
pix/i/siteevent.svg
question/classes/statistics/questions/calculator.php
question/type/multianswer/questiontype.php
report/insights/classes/output/insight.php
report/insights/classes/output/insights_list.php
report/insights/lang/en/report_insights.php
report/insights/templates/insight.mustache
report/insights/templates/insight_details.mustache
report/insights/templates/insights_list.mustache
report/progress/amd/build/completion_override.min.js [new file with mode: 0644]
report/progress/amd/src/completion_override.js [new file with mode: 0644]
report/progress/index.php
report/progress/tests/behat/activity_completion_report.feature [new file with mode: 0644]
search/classes/base.php
search/classes/base_activity.php
search/classes/base_block.php
search/classes/base_mod.php
search/classes/engine.php
search/classes/manager.php
search/classes/skip_future_documents_iterator.php [new file with mode: 0644]
search/cli/indexer.php
search/engine/solr/classes/engine.php
search/tests/base_activity_test.php
search/tests/base_block_test.php
search/tests/manager_test.php
search/upgrade.txt
tag/classes/tag.php
theme/boost/lang/en/theme_boost.php
theme/boost/lib.php
theme/boost/scss/moodle/calendar.scss
theme/boost/scss/moodle/modules.scss
theme/boost/settings.php
theme/boost/templates/columns2.mustache
theme/boost/templates/mod_assign/grading_actions.mustache
theme/bootstrapbase/layout/columns2.php
theme/bootstrapbase/less/moodle/bs4-compat.less
theme/bootstrapbase/less/moodle/calendar.less
theme/bootstrapbase/less/moodle/modules.less
theme/bootstrapbase/style/moodle.css
theme/bootstrapbase/templates/report_insights/insight.mustache [deleted file]
theme/bootstrapbase/templates/report_insights/insight_details.mustache [deleted file]
theme/bootstrapbase/templates/report_insights/insights_list.mustache [deleted file]
theme/upgrade.txt
user/amd/build/unified_filter.min.js
user/amd/src/unified_filter.js
user/classes/search/user.php
user/editlib.php
user/lib.php
user/profile/definelib.php
user/profile/field/checkbox/field.class.php
user/profile/field/menu/field.class.php
user/profile/field/upgrade.txt [new file with mode: 0644]
user/profile/lib.php
user/templates/unified_filter.mustache
user/tests/behat/filter_participants.feature
user/tests/search_test.php
user/tests/userlib_test.php
version.php
webservice/upgrade.txt
webservice/upload.php
webservice/xmlrpc/lib.php

index 0622612..7d19e9c 100644 (file)
@@ -62,6 +62,8 @@ lib/validateurlsyntax.php
 media/player/videojs/amd/src/video-lazy.js
 media/player/videojs/amd/src/Youtube-lazy.js
 media/player/videojs/videojs/
+media/player/videojs/amd/src/videojs-flash-lazy.js
+media/player/videojs/videojs/video-js.swf
 mod/assign/feedback/editpdf/fpdi/
 repository/s3/S3.php
 theme/boost/scss/bootstrap/
index cfcf702..c35c560 100644 (file)
@@ -63,6 +63,8 @@ lib/validateurlsyntax.php
 media/player/videojs/amd/src/video-lazy.js
 media/player/videojs/amd/src/Youtube-lazy.js
 media/player/videojs/videojs/
+media/player/videojs/amd/src/videojs-flash-lazy.js
+media/player/videojs/videojs/video-js.swf
 mod/assign/feedback/editpdf/fpdi/
 repository/s3/S3.php
 theme/boost/scss/bootstrap/
index b077d44..e8f0816 100644 (file)
@@ -863,6 +863,7 @@ $cachewarnings = cache_helper::warnings();
 // Check if there are events 1 API handlers.
 $eventshandlers = $DB->get_records_sql('SELECT DISTINCT component FROM {events_handlers}');
 $themedesignermode = !empty($CFG->themedesignermode);
+$mobileconfigured = !empty($CFG->enablemobilewebservice);
 
 // Check if a directory with development libraries exists.
 if (empty($CFG->disabledevlibdirscheck) && (is_dir($CFG->dirroot.'/vendor') || is_dir($CFG->dirroot.'/node_modules'))) {
@@ -877,4 +878,5 @@ $output = $PAGE->get_renderer('core', 'admin');
 
 echo $output->admin_notifications_page($maturity, $insecuredataroot, $errorsdisplayed, $cronoverdue, $dbproblems,
                                        $maintenancemode, $availableupdates, $availableupdatesfetch, $buggyiconvnomb,
-                                       $registered, $cachewarnings, $eventshandlers, $themedesignermode, $devlibdir);
+                                       $registered, $cachewarnings, $eventshandlers, $themedesignermode, $devlibdir,
+                                       $mobileconfigured);
index 7dd4808..1fd8cb5 100644 (file)
@@ -278,13 +278,14 @@ class core_admin_renderer extends plugin_renderer_base {
      * @param array $eventshandlers Events 1 API handlers.
      * @param bool $themedesignermode Warn about the theme designer mode.
      * @param bool $devlibdir Warn about development libs directory presence.
+     * @param bool $mobileconfigured Whether the mobile web services have been enabled
      *
      * @return string HTML to output.
      */
     public function admin_notifications_page($maturity, $insecuredataroot, $errorsdisplayed,
             $cronoverdue, $dbproblems, $maintenancemode, $availableupdates, $availableupdatesfetch,
             $buggyiconvnomb, $registered, array $cachewarnings = array(), $eventshandlers = 0,
-            $themedesignermode = false, $devlibdir = false) {
+            $themedesignermode = false, $devlibdir = false, $mobileconfigured = false) {
         global $CFG;
         $output = '';
 
@@ -303,6 +304,7 @@ class core_admin_renderer extends plugin_renderer_base {
         $output .= $this->cache_warnings($cachewarnings);
         $output .= $this->events_handlers($eventshandlers);
         $output .= $this->registration_warning($registered);
+        $output .= $this->mobile_configuration_warning($mobileconfigured);
 
         //////////////////////////////////////////////////////////////////////////////////////////////////
         ////  IT IS ILLEGAL AND A VIOLATION OF THE GPL TO HIDE, REMOVE OR MODIFY THIS COPYRIGHT NOTICE ///
@@ -830,6 +832,23 @@ class core_admin_renderer extends plugin_renderer_base {
         return $this->registration_warning(\core\hub\registration::is_registered());
     }
 
+    /**
+     * Display a warning about the Mobile Web Services being disabled.
+     *
+     * @param boolean $mobileconfigured true if mobile web services are enabled
+     * @return string HTML to output.
+     */
+    protected function mobile_configuration_warning($mobileconfigured) {
+        $output = '';
+        if (!$mobileconfigured) {
+            $settingslink = new moodle_url('/admin/settings.php', ['section' => 'mobilesettings']);
+            $configurebutton = $this->single_button($settingslink, get_string('enablemobilewebservice', 'admin'));
+            $output .= $this->warning(get_string('mobilenotconfiguredwarning', 'admin') . '&nbsp;' . $configurebutton);
+        }
+
+        return $output;
+    }
+
     /**
      * Helper method to render the information about the available Moodle update
      *
index e8247c8..b42a252 100644 (file)
@@ -101,5 +101,10 @@ if ($hassiteconfig) {
         // Disable web interface evaluation and get predictions.
         $settings->add(new admin_setting_configcheckbox('analytics/onlycli', new lang_string('onlycli', 'analytics'),
             new lang_string('onlycliinfo', 'analytics'), 1));
+
+        // Training and prediction time limit per model.
+        $settings->add(new admin_setting_configduration('analytics/modeltimelimit', new lang_string('modeltimelimit', 'analytics'),
+            new lang_string('modeltimelimitinfo', 'analytics'), 20 * MINSECS));
+
     }
 }
diff --git a/admin/tool/analytics/amd/build/model.min.js b/admin/tool/analytics/amd/build/model.min.js
new file mode 100644 (file)
index 0000000..e9e2e3c
Binary files /dev/null and b/admin/tool/analytics/amd/build/model.min.js differ
diff --git a/admin/tool/analytics/amd/src/model.js b/admin/tool/analytics/amd/src/model.js
new file mode 100644 (file)
index 0000000..8913431
--- /dev/null
@@ -0,0 +1,94 @@
+// 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/>.
+
+/**
+ * AMD module for model actions confirmation.
+ *
+ * @module     tool_analytics/model
+ * @copyright  2017 David Monllao
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+define(['jquery', 'core/str', 'core/log', 'core/notification', 'core/modal_factory', 'core/modal_events'],
+    function($, Str, log, Notification, ModalFactory, ModalEvents) {
+
+    /**
+     * List of actions that require confirmation and confirmation message.
+     */
+    var actionsList = {
+        clear: {
+            title: {
+                key: 'clearpredictions',
+                component: 'tool_analytics'
+            }, body: {
+                key: 'clearmodelpredictions',
+                component: 'tool_analytics'
+            }
+
+        }
+    };
+
+    /**
+     * Returns the model name.
+     *
+     * @param {Object} actionItem The action item DOM node.
+     * @return {String}
+     */
+    var getModelName = function(actionItem) {
+        return $(actionItem.closest('tr')[0]).find('span.target-name').text();
+    };
+
+    /** @alias module:tool_analytics/model */
+    return {
+
+        /**
+         * Displays a confirm modal window before executing the action.
+         *
+         * @param {String} actionId
+         * @param {String} actionType
+         */
+        confirmAction: function(actionId, actionType) {
+            $('[data-action-id="' + actionId + '"]').on('click', function(ev) {
+                ev.preventDefault();
+
+                var a = $(ev.currentTarget);
+
+                if (typeof actionsList[actionType] === "undefined") {
+                    log.error('Action "' + actionType + '" is not allowed.');
+                    return;
+                }
+
+                var reqStrings = [
+                    actionsList[actionType].title,
+                    actionsList[actionType].body
+                ];
+                reqStrings[1].param = getModelName(a);
+
+                var stringsPromise = Str.get_strings(reqStrings);
+                var modalPromise = ModalFactory.create({type: ModalFactory.types.SAVE_CANCEL});
+
+                $.when(stringsPromise, modalPromise).then(function(strings, modal) {
+                    modal.setTitle(strings[0]);
+                    modal.setBody(strings[1]);
+                    modal.setSaveButtonText(strings[0]);
+                    modal.getRoot().on(ModalEvents.save, function() {
+                        window.location.href = a.attr('href');
+                    });
+                    modal.show();
+                    return modal;
+                }).fail(Notification.exception);
+            });
+        }
+    };
+});
diff --git a/admin/tool/analytics/classes/output/invalid_analysables.php b/admin/tool/analytics/classes/output/invalid_analysables.php
new file mode 100644 (file)
index 0000000..a5bda09
--- /dev/null
@@ -0,0 +1,158 @@
+<?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/>.
+
+/**
+ * Invalid analysables renderable.
+ *
+ * @package    tool_analytics
+ * @copyright  2017 David Monllao {@link http://www.davidmonllao.com}
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace tool_analytics\output;
+
+defined('MOODLE_INTERNAL') || die;
+
+/**
+ * Invalid analysables renderable.
+ *
+ * @package    tool_analytics
+ * @copyright  2017 David Monllao {@link http://www.davidmonllao.com}
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class invalid_analysables implements \renderable, \templatable {
+
+    /**
+     * @var \core_analytics\model
+     */
+    protected $model = null;
+
+    /**
+     * @var int
+     */
+    protected $page = 0;
+
+    /**
+     * @var int
+     */
+    protected $perpage = 0;
+
+    /**
+     * Inits the invalid analysables renderable.
+     *
+     * @param \core_analytics\model $model
+     * @param int $page
+     * @param int $perpage
+     * @return \stdClass
+     */
+    public function __construct(\core_analytics\model $model, $page, $perpage) {
+
+        $this->model = $model;
+        $this->page = $page;
+        $this->perpage = $perpage;
+    }
+
+    /**
+     * Export the data.
+     *
+     * @param \renderer_base $output
+     * @return \stdClass
+     */
+    public function export_for_template(\renderer_base $output) {
+        global $PAGE;
+
+        $offset = $this->page * $this->perpage;
+
+        $analysables = $this->model->get_analyser(['notimesplitting' => true])->get_analysables();
+
+        $skipped = 0;
+        $enoughresults = false;
+        $morepages = false;
+        $results = array();
+        foreach ($analysables as $key => $analysable) {
+
+            $validtraining = $this->model->get_target()->is_valid_analysable($analysable, true);
+            if ($validtraining === true) {
+                if ($this->model->is_static()) {
+                    // We still want to show this analysable if it is not valid to get predictions.
+                    $validtraining = get_string('notrainingbasedassumptions', 'analytics');
+                } else {
+                    // We skip analysables that are valid for training or valid for prediction.
+                    continue;
+                }
+            }
+
+            $validprediction = $this->model->get_target()->is_valid_analysable($analysable, false);
+            if ($validprediction === true) {
+                // We skip analysables that are valid for training or valid for prediction.
+                continue;
+            }
+
+            if ($offset && $skipped < $offset) {
+                $skipped++;
+                continue;
+            }
+
+            // Add a new results if we don't have enough yet.
+            if (!$enoughresults) {
+                $results[$analysable->get_id()] = array($analysable, $validtraining, $validprediction);
+                if ($this->perpage && count($results) === $this->perpage) {
+                    $enoughresults = true;
+                }
+            } else {
+                // Confirmed that we have results we can not fit into this page.
+                $morepages = true;
+                break;
+            }
+
+            unset($analysables[$key]);
+        }
+
+        // Prepare the context object.
+        $data = new \stdClass();
+        $data->modelname = $this->model->get_target()->get_name();
+
+        if ($this->page > 0) {
+            $prev = clone $PAGE->url;
+            $prev->param('page', $this->page - 1);
+            $button = new \single_button($prev, get_string('previouspage', 'tool_analytics'), 'get');
+            $data->prev = $button->export_for_template($output);
+        }
+        if ($morepages) {
+            $next = clone $PAGE->url;
+            $next->param('page', $this->page + 1);
+            $button = new \single_button($next, get_string('nextpage', 'tool_analytics'), 'get');
+            $data->next = $button->export_for_template($output);
+        }
+
+        $data->analysables = [];
+        foreach ($results as list($analysable, $validtraining, $validprediction)) {
+            $obj = new \stdClass();
+            $obj->url = \html_writer::link($analysable->get_context()->get_url(), $analysable->get_name(),
+                array('target' => '_blank'));
+
+            if ($validtraining !== true) {
+                $obj->validtraining = $validtraining;
+            }
+            if ($validprediction !== true) {
+                $obj->validprediction = $validprediction;
+            }
+            $data->analysables[] = $obj;
+        }
+
+        return $data;
+    }
+}
index b05e102..f33980c 100644 (file)
@@ -59,6 +59,7 @@ class models_list implements \renderable, \templatable {
      * @return \stdClass
      */
     public function export_for_template(\renderer_base $output) {
+        global $PAGE;
 
         $data = new \stdClass();
 
@@ -120,11 +121,13 @@ class models_list implements \renderable, \templatable {
                 }
             }
 
+            // Has this model generated predictions?.
+            $predictioncontexts = $model->get_predictions_contexts();
+
             // Model predictions list.
             if (!$model->is_enabled()) {
                 $modeldata->noinsights = get_string('disabledmodel', 'analytics');
             } else if ($model->uses_insights()) {
-                $predictioncontexts = $model->get_predictions_contexts();
                 if ($predictioncontexts) {
 
                     foreach ($predictioncontexts as $contextid => $unused) {
@@ -166,9 +169,39 @@ class models_list implements \renderable, \templatable {
             $actionsmenu->set_owner_selector('model-actions-' . $model->get_id());
             $actionsmenu->set_alignment(\action_menu::TL, \action_menu::BL);
 
+            $urlparams = ['id' => $model->get_id(), 'sesskey' => sesskey()];
+
+            // Get predictions.
+            if (!$onlycli && $modeldata->enabled && !empty($modeldata->timesplitting)) {
+                $urlparams['action'] = 'getpredictions';
+                $url = new \moodle_url('model.php', $urlparams);
+                $icon = new \action_menu_link_secondary($url, new \pix_icon('i/notifications',
+                    get_string('getpredictions', 'tool_analytics')), get_string('getpredictions', 'tool_analytics'));
+                $actionsmenu->add($icon);
+            }
+
+            // Evaluate machine-learning-based models.
+            if (!$onlycli && $model->get_indicators() && !$model->is_static()) {
+                $urlparams['action'] = 'evaluate';
+                $url = new \moodle_url('model.php', $urlparams);
+                $icon = new \action_menu_link_secondary($url, new \pix_icon('i/calc', get_string('evaluate', 'tool_analytics')),
+                    get_string('evaluate', 'tool_analytics'));
+                $actionsmenu->add($icon);
+            }
+
+            // Machine-learning-based models evaluation log.
+            if (!$model->is_static()) {
+                $urlparams['action'] = 'log';
+                $url = new \moodle_url('model.php', $urlparams);
+                $icon = new \action_menu_link_secondary($url, new \pix_icon('i/report', get_string('viewlog', 'tool_analytics')),
+                    get_string('viewlog', 'tool_analytics'));
+                $actionsmenu->add($icon);
+            }
+
             // Edit model.
             if (!$model->is_static()) {
-                $url = new \moodle_url('model.php', array('action' => 'edit', 'id' => $model->get_id()));
+                $urlparams['action'] = 'edit';
+                $url = new \moodle_url('model.php', $urlparams);
                 $icon = new \action_menu_link_secondary($url, new \pix_icon('t/edit', get_string('edit')), get_string('edit'));
                 $actionsmenu->add($icon);
             }
@@ -183,39 +216,39 @@ class models_list implements \renderable, \templatable {
                 $text = get_string('enable');
                 $icontype = 'i/checked';
             }
-            $url = new \moodle_url('model.php', array('action' => $action, 'id' => $model->get_id()));
+            $urlparams['action'] = $action;
+            $url = new \moodle_url('model.php', $urlparams);
             $icon = new \action_menu_link_secondary($url, new \pix_icon($icontype, $text), $text);
             $actionsmenu->add($icon);
 
-            // Evaluate machine-learning-based models.
-            if (!$onlycli && $model->get_indicators() && !$model->is_static()) {
-                $url = new \moodle_url('model.php', array('action' => 'evaluate', 'id' => $model->get_id()));
-                $icon = new \action_menu_link_secondary($url, new \pix_icon('i/calc', get_string('evaluate', 'tool_analytics')),
-                    get_string('evaluate', 'tool_analytics'));
-                $actionsmenu->add($icon);
-            }
-
-            // Get predictions.
-            if (!$onlycli && $modeldata->enabled && !empty($modeldata->timesplitting)) {
-                $url = new \moodle_url('model.php', array('action' => 'getpredictions', 'id' => $model->get_id()));
-                $icon = new \action_menu_link_secondary($url, new \pix_icon('i/notifications',
-                    get_string('getpredictions', 'tool_analytics')), get_string('getpredictions', 'tool_analytics'));
+            // Export training data.
+            if (!$model->is_static() && $model->is_trained()) {
+                $urlparams['action'] = 'export';
+                $url = new \moodle_url('model.php', $urlparams);
+                $icon = new \action_menu_link_secondary($url, new \pix_icon('i/export',
+                    get_string('exporttrainingdata', 'tool_analytics')), get_string('export', 'tool_analytics'));
                 $actionsmenu->add($icon);
             }
 
-            // Machine-learning-based models evaluation log.
-            if (!$model->is_static()) {
-                $url = new \moodle_url('model.php', array('action' => 'log', 'id' => $model->get_id()));
-                $icon = new \action_menu_link_secondary($url, new \pix_icon('i/report', get_string('viewlog', 'tool_analytics')),
-                    get_string('viewlog', 'tool_analytics'));
+            // Invalid analysables.
+            $analyser = $model->get_analyser(['notimesplitting' => true]);
+            if (!$analyser instanceof \core_analytics\local\analyser\sitewide) {
+                $urlparams['action'] = 'invalidanalysables';
+                $url = new \moodle_url('model.php', $urlparams);
+                $pix = new \pix_icon('i/report', get_string('invalidanalysables', 'tool_analytics'));
+                $icon = new \action_menu_link_secondary($url, $pix, get_string('invalidanalysables', 'tool_analytics'));
                 $actionsmenu->add($icon);
             }
 
-            // Export training data.
-            if (!$model->is_static() && $model->is_trained()) {
-                $url = new \moodle_url('model.php', array('action' => 'export', 'id' => $model->get_id()));
-                $icon = new \action_menu_link_secondary($url, new \pix_icon('i/export',
-                    get_string('exporttrainingdata', 'tool_analytics')), get_string('export', 'tool_analytics'));
+            // Clear model.
+            if (!empty($predictioncontexts)) {
+                $actionid = 'clear-' . $model->get_id();
+                $PAGE->requires->js_call_amd('tool_analytics/model', 'confirmAction', [$actionid, 'clear']);
+                $urlparams['action'] = 'clear';
+                $url = new \moodle_url('model.php', $urlparams);
+                $icon = new \action_menu_link_secondary($url, new \pix_icon('e/cleanup_messy_code',
+                    get_string('clearpredictions', 'tool_analytics')), get_string('clearpredictions', 'tool_analytics'),
+                    ['data-action-id' => $actionid]);
                 $actionsmenu->add($icon);
             }
 
index 32bdcfc..4201482 100644 (file)
@@ -159,11 +159,12 @@ class renderer extends plugin_renderer_base {
             if ($trainresults->status == 0) {
                 $output .= $OUTPUT->notification(get_string('trainingprocessfinished', 'tool_analytics'),
                     \core\output\notification::NOTIFY_SUCCESS);
-            } else if ($trainresults->status === \core_analytics\model::NO_DATASET) {
+            } else if ($trainresults->status === \core_analytics\model::NO_DATASET ||
+                    $trainresults->status === \core_analytics\model::NOT_ENOUGH_DATA) {
                 $output .= $OUTPUT->notification(get_string('nodatatotrain', 'tool_analytics'),
                     \core\output\notification::NOTIFY_WARNING);
             } else {
-                $output .= $OUTPUT->notification(get_string('generalerror', 'analytics', $trainresults->status),
+                $output .= $OUTPUT->notification(get_string('generalerror', 'tool_analytics', $trainresults->status),
                     \core\output\notification::NOTIFY_ERROR);
             }
         }
@@ -183,11 +184,12 @@ class renderer extends plugin_renderer_base {
             if ($predictresults->status == 0) {
                 $output .= $OUTPUT->notification(get_string('predictionprocessfinished', 'tool_analytics'),
                     \core\output\notification::NOTIFY_SUCCESS);
-            } else if ($predictresults->status === \core_analytics\model::NO_DATASET) {
+            } else if ($predictresults->status === \core_analytics\model::NO_DATASET ||
+                    $predictresults->status === \core_analytics\model::NOT_ENOUGH_DATA) {
                 $output .= $OUTPUT->notification(get_string('nodatatopredict', 'tool_analytics'),
                     \core\output\notification::NOTIFY_WARNING);
             } else {
-                $output .= $OUTPUT->notification(get_string('generalerror', 'analytics', $predictresults->status),
+                $output .= $OUTPUT->notification(get_string('generalerror', 'tool_analytics', $predictresults->status),
                     \core\output\notification::NOTIFY_ERROR);
             }
         }
@@ -205,4 +207,15 @@ class renderer extends plugin_renderer_base {
 
         return $output;
     }
+
+    /**
+     * Defer to template.
+     *
+     * @param \tool_analytics\output\invalid_analysables $invalidanalysables
+     * @return string HTML
+     */
+    protected function render_invalid_analysables(\tool_analytics\output\invalid_analysables $invalidanalysables) {
+        $data = $invalidanalysables->export_for_template($this);
+        return parent::render_from_template('tool_analytics/invalid_analysables', $data);
+    }
 }
index df38a14..c4f0e5a 100644 (file)
@@ -29,8 +29,10 @@ $string['analyticmodels'] = 'Analytic models';
 $string['bettercli'] = 'Evaluating models and generating predictions may involve heavy processing. It is recommended to run these actions from the command line.';
 $string['cantguessstartdate'] = 'Can\'t guess the start date';
 $string['cantguessenddate'] = 'Can\'t guess the end date';
+$string['clearpredictions'] = 'Clear predictions';
+$string['clearmodelpredictions'] = 'Are you sure you want to clear all "{$a}" predictions?';
 $string['clienablemodel'] = 'You can enable the model by selecting a time-splitting method by its ID. Note that you can also enable it later using the web interface (\'none\' to exit).';
-$string['clievaluationandpredictions'] = 'A cron task iterates through enabled models and gets predictions. Models evaluation via command line is disabled. You can allow these processes to be executed manually via web interface by enabling <a href="{$a}">\'onlycli\' analytics setting</a>';
+$string['clievaluationandpredictions'] = 'A scheduled task iterates through enabled models and gets predictions. Models evaluation via the web interface is disabled. You can allow these processes to be executed manually via the web interface by disabling the <a href="{$a}">\'onlycli\'</a> analytics setting';
 $string['editmodel'] = 'Edit "{$a}" model';
 $string['edittrainedwarning'] = 'This model has already been trained. Note that changing its indicators or its time-splitting method will delete its previous predictions and start generating new predictions.';
 $string['enabled'] = 'Enabled';
@@ -57,10 +59,16 @@ $string['goodmodel'] = 'This is a good model for using to obtain predictions. En
 $string['indicators'] = 'Indicators';
 $string['info'] = 'Info';
 $string['insights'] = 'Insights';
+$string['invalidanalysables'] = 'Invalid site elements';
+$string['invalidanalysablesinfo'] = 'This pages lists this site analysable elements that can not be used by this prediction model. The listed elements can not be used neither to train the prediction model nor the prediction model can get predictions for them.';
+$string['invalidanalysablestable'] = 'Invalid site analysable elements table';
+$string['invalidprediction'] = 'Invalid to get predictions';
+$string['invalidtraining'] = 'Invalid to train the model';
 $string['loginfo'] = 'Log extra info';
+$string['modelinvalidanalysables'] = 'Invalid analysable elements for "{$a}" model';
 $string['modelresults'] = '{$a} results';
-$string['modelslist'] = 'Models list';
 $string['modeltimesplitting'] = 'Time splitting';
+$string['nextpage'] = 'Next page';
 $string['nodatatoevaluate'] = 'There is no data to evaluate the model';
 $string['nodatatopredict'] = 'No new elements to get predictions for';
 $string['nodatatotrain'] = 'There is no new data that can be used for training';
@@ -70,6 +78,7 @@ $string['predictionresults'] = 'Prediction results';
 $string['predictmodels'] = 'Predict models';
 $string['predictorresultsin'] = 'Predictor logged information in {$a} directory';
 $string['predictionprocessfinished'] = 'Prediction process finished';
+$string['previouspage'] = 'Previous page';
 $string['samestartdate'] = 'Current start date is good';
 $string['sameenddate'] = 'Current end date is good';
 $string['target'] = 'Target';
index 6d64c42..58f1129 100644 (file)
@@ -60,7 +60,12 @@ switch ($action) {
     case 'export':
         $title = get_string('export', 'tool_analytics');
         break;
-
+    case 'clear':
+        $title = get_string('clearpredictions', 'tool_analytics');
+        break;
+    case 'invalidanalysables':
+        $title = get_string('invalidanalysables', 'tool_analytics');
+        break;
     default:
         throw new moodle_exception('errorunknownaction', 'analytics');
 }
@@ -80,14 +85,21 @@ if ($onlycli === false) {
 switch ($action) {
 
     case 'enable':
+        confirm_sesskey();
+
         $model->enable();
         redirect(new \moodle_url('/admin/tool/analytics/index.php'));
+        break;
 
     case 'disable':
+        confirm_sesskey();
+
         $model->update(0, false, false);
         redirect(new \moodle_url('/admin/tool/analytics/index.php'));
+        break;
 
     case 'edit':
+        confirm_sesskey();
 
         if ($model->is_static()) {
             echo $OUTPUT->header();
@@ -106,7 +118,6 @@ switch ($action) {
             redirect(new \moodle_url('/admin/tool/analytics/index.php'));
 
         } else if ($data = $mform->get_data()) {
-            confirm_sesskey();
 
             // Converting option names to class names.
             $indicators = array();
@@ -131,6 +142,8 @@ switch ($action) {
         break;
 
     case 'evaluate':
+        confirm_sesskey();
+
         echo $OUTPUT->header();
 
         if ($model->is_static()) {
@@ -150,6 +163,8 @@ switch ($action) {
         break;
 
     case 'getpredictions':
+        confirm_sesskey();
+
         echo $OUTPUT->header();
 
         if ($onlycli) {
@@ -200,6 +215,27 @@ switch ($action) {
         $filename = 'training-data.' . $model->get_id() . '.' . time() . '.csv';
         send_file($file, $filename, null, 0, false, true);
         break;
+
+    case 'clear':
+        confirm_sesskey();
+
+        $model->clear();
+        redirect(new \moodle_url('/admin/tool/analytics/index.php'));
+        break;
+
+    case 'invalidanalysables':
+
+        echo $OUTPUT->header();
+
+        $page = optional_param('page', 0, PARAM_INT);
+        // No option in the UI to change this, only for url hackers ;).
+        $perpage = optional_param('perpage', 10, PARAM_INT);
+
+        $renderable = new \tool_analytics\output\invalid_analysables($model, $page, $perpage);
+        $renderer = $PAGE->get_renderer('tool_analytics');
+        echo $renderer->render($renderable);
+
+        break;
 }
 
 echo $OUTPUT->footer();
diff --git a/admin/tool/analytics/templates/invalid_analysables.mustache b/admin/tool/analytics/templates/invalid_analysables.mustache
new file mode 100644 (file)
index 0000000..c97dd6b
--- /dev/null
@@ -0,0 +1,78 @@
+{{!
+    This file is part of Moodle - http://moodle.org/
+
+    Moodle is free software: you can redistribute it and/or modify
+    it under the terms of the GNU General Public License as published by
+    the Free Software Foundation, either version 3 of the License, or
+    (at your option) any later version.
+
+    Moodle is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+    GNU General Public License for more details.
+
+    You should have received a copy of the GNU General Public License
+    along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+}}
+{{!
+    @template tool_analytics/invalid_analysables
+
+    Template for invalid analysables.
+
+    Classes required for JS:
+    * none
+
+    Data attributes required for JS:
+    * none
+
+    Context variables required for this template:
+    * none
+
+    Example context (json):
+    {
+        "modelname": "Not engaging courses",
+        "analysables": [
+            {
+                "url": "<a href=\"#\">Maths</a>",
+                "validtraining": "Ongoing course",
+                "validprediction": "Not enough students activity"
+            }, {
+                "url": "<a href=\"#\">Psichology</a>",
+                "validtraining": "No students",
+                "validprediction": "No students"
+            }
+        ]
+    }
+}}
+
+<div class="box">
+    <h3>{{#str}}modelinvalidanalysables, tool_analytics, {{modelname}}{{/str}}</h3>
+    <div>{{#str}}invalidanalysablesinfo, tool_analytics{{/str}}</div>
+    <div class="m-t-2 m-b-1">
+        <span>{{#prev}}{{> core/single_button}}{{/prev}}</span>
+        <span>{{#next}}{{> core/single_button}}{{/next}}</span>
+    </div>
+    <table class="generaltable fullwidth">
+        <caption class="accesshide">{{#str}}invalidanalysablestable, tool_analytics{{/str}}</caption>
+        <thead>
+            <tr>
+                <th scope="col">{{#str}}name{{/str}}</th>
+                <th scope="col">{{#str}}invalidtraining, tool_analytics{{/str}}</th>
+                <th scope="col">{{#str}}invalidprediction, tool_analytics{{/str}}</th>
+            </tr>
+        </thead>
+        <tbody>
+        {{#analysables}}
+            <tr>
+                <td>{{{url}}}</td>
+                <td>{{validtraining}}</td>
+                <td>{{validprediction}}</td>
+            </tr>
+        {{/analysables}}
+        </tbody>
+    </table>
+    <div class="m-t-1 m-b-2">
+        <span>{{#prev}}{{> core/single_button}}{{/prev}}</span>
+        <span>{{#next}}{{> core/single_button}}{{/next}}</span>
+    </div>
+</div>
index 9330c54..8eb6211 100644 (file)
 
 <div class="box">
     <table class="generaltable fullwidth">
-        <caption>{{#str}}modelslist, tool_analytics{{/str}}</caption>
+        <caption>{{#str}}analyticmodels, tool_analytics{{/str}}</caption>
         <thead>
             <tr>
                 <th scope="col">{{#str}}target, tool_analytics{{/str}}</th>
         {{#models}}
             <tr>
                 <td>
-                    {{target}}
+                    <span class="target-name">{{target}}</span>
                     {{#targethelp}}
                         {{>core/help_icon}}
                     {{/targethelp}}
diff --git a/admin/tool/httpsreplace/classes/form.php b/admin/tool/httpsreplace/classes/form.php
new file mode 100644 (file)
index 0000000..cd83473
--- /dev/null
@@ -0,0 +1,52 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * Site wide http -> https search-replace form.
+ *
+ * @package    tool_httpsreplace
+ * @copyright Copyright (c) 2016 Blackboard Inc. (http://www.blackboard.com)
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace tool_httpsreplace;
+
+defined('MOODLE_INTERNAL') || die();
+
+require_once("$CFG->libdir/formslib.php");
+
+/**
+ * Site wide http -> https search-replace form.
+ * @copyright Copyright (c) 2016 Blackboard Inc. (http://www.blackboard.com)
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class form extends \moodleform {
+
+    /**
+     * Define the form.
+     */
+    public function definition() {
+        $mform = $this->_form;
+
+        $mform->addElement('header', 'confirmhdr', get_string('confirm'));
+        $mform->setExpanded('confirmhdr', true);
+        $mform->addElement('checkbox', 'sure', get_string('disclaimer', 'tool_httpsreplace'));
+        $mform->addRule('sure', get_string('required'), 'required', null, 'client');
+        $mform->disable_form_change_checker();
+
+        $this->add_action_buttons(false, get_string('doit', 'tool_httpsreplace'));
+    }
+}
diff --git a/admin/tool/httpsreplace/classes/url_finder.php b/admin/tool/httpsreplace/classes/url_finder.php
new file mode 100644 (file)
index 0000000..af9fc9b
--- /dev/null
@@ -0,0 +1,261 @@
+<?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/>.
+
+/**
+ * url_finder class definition.
+ *
+ * @package    tool_httpsreplace
+ * @copyright Copyright (c) 2016 Blackboard Inc. (http://www.blackboard.com)
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace tool_httpsreplace;
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * Examines DB for non-https src or data links
+ *
+ * @package tool_httpsreplace
+ * @copyright Copyright (c) 2016 Blackboard Inc. (http://www.blackboard.com)
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class url_finder {
+
+    /**
+     * Returns a hash of what hosts are referred to over http and would need to be changed.
+     *
+     * @param progress_bar $progress Progress bar keeping track of this process.
+     * @return array Hash of domains with number of references as the value.
+     */
+    public function http_link_stats($progress = null) {
+        return $this->process(false, $progress);
+    }
+
+    /**
+     * Changes all resources referred to over http to https.
+     *
+     * @param progress_bar $progress Progress bar keeping track of this process.
+     * @return bool True upon success
+     */
+    public function upgrade_http_links($progress = null) {
+        return $this->process(true, $progress);
+    }
+
+    /**
+     * Replace http domains with https equivalent, with two types of exceptions
+     * for less straightforward swaps.
+     *
+     * @param string $table
+     * @param string $column
+     * @param string $domain
+     * @param string $search search string that has prefix, protocol, domain name and one extra character,
+     *      example1: src="http://host.com/
+     *      example2: DATA="HTTP://MYDOMAIN.EDU"
+     *      example3: src="HTTP://hello.world?
+     * @return void
+     */
+    protected function domain_swap($table, $column, $domain, $search) {
+        global $DB;
+
+        $renames = json_decode(get_config('tool_httpsreplace', 'renames'), true);
+
+        if (isset($renames[$domain])) {
+            $replace = preg_replace('|http://'.preg_quote($domain).'|i', 'https://' . $renames[$domain], $search);
+        } else {
+            $replace = preg_replace('|http://|i', 'https://', $search);
+        }
+        $DB->set_debug(true);
+        $DB->replace_all_text($table, $column, $search, $replace);
+        $DB->set_debug(false);
+    }
+
+    /**
+     * Returns SQL to be used to match embedded http links in the given column
+     *
+     * @param string $columnname name of the column (ready to be used in the SQL query)
+     * @return array
+     */
+    protected function get_select_search_in_column($columnname) {
+        global $DB;
+
+        if ($DB->sql_regex_supported()) {
+            // Database supports regex, use it for better match.
+            $select = $columnname . ' ' . $DB->sql_regex() . ' ?';
+            $params = ["(src|data)\ *=\ *[\\\"\']http://"];
+        } else {
+            // Databases without regex support should use case-insensitive LIKE.
+            // This will have false positive matches and more results than we need, we'll have to filter them in php.
+            $select = $DB->sql_like($columnname, '?', false);
+            $params = ['%=%http://%'];
+        }
+
+        return [$select, $params];
+    }
+
+    /**
+     * Originally forked from core function db_search().
+     * @param bool $replacing Whether or not to replace the found urls.
+     * @param progress_bar $progress Progress bar keeping track of this process.
+     * @return bool|array If $replacing, return true on success. If not, return hash of http urls to number of times used.
+     */
+    protected function process($replacing = false, $progress = null) {
+        global $DB, $CFG;
+
+        require_once($CFG->libdir.'/filelib.php');
+
+        // TODO: block_instances have HTML content as base64, need to decode then
+        // search, currently just skipped. See MDL-60024.
+        $skiptables = array(
+            'block_instances',
+            'config',
+            'config_log',
+            'config_plugins',
+            'events_queue',
+            'files',
+            'filter_config',
+            'grade_grades_history',
+            'grade_items_history',
+            'log',
+            'logstore_standard_log',
+            'repository_instance_config',
+            'sessions',
+            'upgrade_log',
+            'grade_categories_history',
+            '',
+        );
+
+        // Turn off time limits.
+        \core_php_time_limit::raise();
+        if (!$tables = $DB->get_tables() ) {    // No tables yet at all.
+            return false;
+        }
+
+        $urls = array();
+
+        $numberoftables = count($tables);
+        $tablenumber = 0;
+        foreach ($tables as $table) {
+            if ($progress) {
+                $progress->update($tablenumber, $numberoftables, get_string('searching', 'tool_httpsreplace', $table));
+                $tablenumber++;
+            }
+            if (in_array($table, $skiptables)) {
+                continue;
+            }
+            if ($columns = $DB->get_columns($table)) {
+                foreach ($columns as $column) {
+
+                    // Only convert columns that are either text or long varchar.
+                    if ($column->meta_type == 'X' || ($column->meta_type == 'C' && $column->max_length > 255)) {
+                        $columnname = $column->name;
+                        $columnnamequoted = $DB->get_manager()->generator->getEncQuoted($columnname);
+                        list($select, $params) = $this->get_select_search_in_column($columnnamequoted);
+                        $rs = $DB->get_recordset_select($table, $select, $params, '', $columnnamequoted);
+
+                        $found = array();
+                        foreach ($rs as $record) {
+                            // Regex to match src=http://etc. and data=http://etc.urls.
+                            // Standard warning on expecting regex to perfectly parse HTML
+                            // read http://stackoverflow.com/a/1732454 for more info.
+                            $regex = '#((src|data)\ *=\ *[\'\"])(http://)([^\s()<>]+(?:\([\w\d]+\)|([^[:punct:]\s]|/)))[\'\"]#i';
+                            preg_match_all($regex, $record->$columnname, $match);
+                            foreach ($match[0] as $i => $fullmatch) {
+                                if (strpos($fullmatch, $CFG->wwwroot) !== false) {
+                                    continue;
+                                }
+                                $prefix = $match[1][$i];
+                                $protocol = $match[3][$i];
+                                $url = $protocol . $match[4][$i];
+                                $host = \core_text::strtolower(parse_url($url, PHP_URL_HOST));
+                                if (empty($host)) {
+                                    continue;
+                                }
+                                if ($replacing) {
+                                    // For replace string use: prefix, protocol, host and one extra character.
+                                    $found[$prefix . substr($url, 0, strlen($host) + 8)] = $host;
+                                } else {
+                                    $entry["table"] = $table;
+                                    $entry["columnname"] = $columnname;
+                                    $entry["url"] = $url;
+                                    $entry["host"] = $host;
+                                    $entry["raw"] = $record->$columnname;
+                                    $entry["ssl"] = '';
+                                    $urls[] = $entry;
+                                }
+                            }
+                        }
+                        $rs->close();
+
+                        if ($replacing) {
+                            foreach ($found as $search => $domain) {
+                                $this->domain_swap($table, $column, $domain, $search);
+                            }
+                        }
+                    }
+                }
+            }
+        }
+
+        if ($replacing) {
+            rebuild_course_cache(0, true);
+            purge_all_caches();
+            return true;
+        }
+
+        $domains = array_map(function ($i) {
+            return $i['host'];
+        }, $urls);
+
+        $uniquedomains = array_unique($domains);
+
+        $sslfailures = array();
+
+        foreach ($uniquedomains as $domain) {
+            if (!$this->check_domain_availability("https://$domain/")) {
+                $sslfailures[] = $domain;
+            }
+        }
+
+        $results = array();
+        foreach ($urls as $url) {
+            $host = $url['host'];
+            foreach ($sslfailures as $badhost) {
+                if ($host == $badhost) {
+                    if (!isset($results[$host])) {
+                        $results[$host] = 1;
+                    } else {
+                        $results[$host]++;
+                    }
+                }
+            }
+        }
+        return $results;
+    }
+
+    /**
+     * Check if url is available (GET request returns 200)
+     *
+     * @param string $url
+     * @return bool
+     */
+    protected function check_domain_availability($url) {
+        $curl = new \curl();
+        $curl->head($url);
+        $info = $curl->get_info();
+        return !empty($info['http_code']) && $info['http_code'] == 200;
+    }
+}
diff --git a/admin/tool/httpsreplace/cli/url_replace.php b/admin/tool/httpsreplace/cli/url_replace.php
new file mode 100644 (file)
index 0000000..b8e4116
--- /dev/null
@@ -0,0 +1,93 @@
+<?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/>.
+
+/**
+ * url_replace cli script. Examines DB for non-https src or data links, and lists broken ones or replaces all links.
+ *
+ * @package    tool_httpsreplace
+ * @copyright Copyright (c) 2016 Blackboard Inc. (http://www.blackboard.com)
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+define('CLI_SCRIPT', true);
+require(__DIR__ . '/../../../../config.php');
+require_once($CFG->libdir.'/clilib.php');
+
+list($options, $unrecognized) = cli_get_params(
+    array(
+        'help' => false,
+        'list' => false,
+        'replace' => false,
+        'confirm' => false,
+    ),
+    array(
+        'h' => 'help',
+        'l' => 'list',
+        'r' => 'replace',
+    )
+);
+if ($unrecognized) {
+    $unrecognized = implode("\n  ", $unrecognized);
+    cli_error(get_string('cliunknowoption', 'admin', $unrecognized), 2);
+}
+if ($options['help'] || (!$options['list'] && !$options['replace'])) {
+    $help = "Examines DB for non-https src or data links, and lists broken links or replaces all links.
+Options:
+-h, --help            Print out this help
+-l, --list            List of http (not https) urls on a site in the DB that would become broken.
+-r, --replace         List of http (not https) urls on a site in the DB that would become broken.
+--confirm             Replaces http urls with https across a site's content.
+Example:
+\$ sudo -u www-data /usr/bin/php admin/tool/httpsreplace/cli/url_replace.php --list \n";
+    echo $help;
+    exit(0);
+}
+
+if (!$DB->replace_all_text_supported()) {
+    echo $OUTPUT->notification(get_string('notimplemented', 'tool_httpsreplace'));
+    exit(1);
+}
+
+if (!is_https()) {
+    echo $OUTPUT->notification(get_string('httpwarning', 'tool_httpsreplace'), 'warning');
+    echo "\n";
+}
+
+if ($options['replace']) {
+
+    if ($options['confirm']) {
+
+        $urlfinder = new \tool_httpsreplace\url_finder();
+        $urlfinder->upgrade_http_links();
+    } else {
+        echo "Once this is tool run, changes made can't be reverted. \n" .
+             "A complete backup should be made before running this script. \n\n" .
+             "There is a low risk that the wrong content will be replaced, introducing problems. \n" .
+             "If you are sure you want to continue, add --confirm\n\n";
+    }
+
+} else {
+
+    $urlfinder = new \tool_httpsreplace\url_finder();
+    $results = $urlfinder->http_link_stats();
+    asort($results);
+    $fp = fopen('php://stdout', 'w');
+    fputcsv($fp, ['clientsite', 'httpdomain', 'urlcount']);
+    foreach ($results as $domain => $count) {
+        fputcsv($fp, [$SITE->shortname, $domain, $count]);
+    }
+    fclose($fp);
+}
diff --git a/admin/tool/httpsreplace/index.php b/admin/tool/httpsreplace/index.php
new file mode 100644 (file)
index 0000000..a3cec32
--- /dev/null
@@ -0,0 +1,59 @@
+<?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/>.
+
+/**
+ * Search and replace http -> https throughout all texts in the whole database
+ *
+ * @package    tool_httpsreplace
+ * @copyright Copyright (c) 2016 Blackboard Inc. (http://www.blackboard.com)
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+require_once(__DIR__ . '/../../../config.php');
+require_once($CFG->libdir . '/adminlib.php');
+
+admin_externalpage_setup('toolhttpsreplace');
+
+$context = context_system::instance();
+
+require_login();
+require_capability('moodle/site:config', $context);
+
+$PAGE->set_context($context);
+$PAGE->set_url(new moodle_url('/admin/tool/httpsreplace/index.php'));
+$PAGE->set_title(get_string('pageheader', 'tool_httpsreplace'));
+$PAGE->set_pagelayout('admin');
+
+echo $OUTPUT->header();
+
+echo $OUTPUT->heading(get_string('pageheader', 'tool_httpsreplace'));
+
+if (!$DB->replace_all_text_supported()) {
+    echo $OUTPUT->notification(get_string('notimplemented', 'tool_httpsreplace'));
+    echo $OUTPUT->footer();
+    die;
+}
+
+if (!is_https()) {
+    echo $OUTPUT->notification(get_string('httpwarning', 'tool_httpsreplace'), 'warning');
+}
+
+echo '<p>'.get_string('domainexplain', 'tool_httpsreplace').'</p>';
+echo '<p>'.page_doc_link(get_string('doclink', 'tool_httpsreplace')).'</p>';
+
+echo $OUTPUT->continue_button(new moodle_url('/admin/tool/httpsreplace/tool.php'));
+
+echo $OUTPUT->footer();
diff --git a/admin/tool/httpsreplace/lang/en/tool_httpsreplace.php b/admin/tool/httpsreplace/lang/en/tool_httpsreplace.php
new file mode 100644 (file)
index 0000000..9566874
--- /dev/null
@@ -0,0 +1,41 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * Strings for component 'tool_httpsreplace'
+ *
+ * @package    tool_httpsreplace
+ * @copyright Copyright (c) 2016 Blackboard Inc. (http://www.blackboard.com)
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+$string['complete'] = 'Completed.';
+$string['count'] = 'Number of embeded content items';
+$string['disclaimer'] = 'I understand the risks of this operation';
+$string['doclink'] = 'Read more documentation on the wiki';
+$string['doit'] = 'Perform replacement';
+$string['domain'] = 'Problematic domain';
+$string['domainexplain'] = 'When an instance is moved from HTTP to HTTPS, all embeded HTTP content will stop working. This tool allows you to automatically convert the HTTP content to HTTPS. Below you can run a report of content that may not work once you run this script. You may want to check each one has HTTPS available or find alternative resources.';
+$string['domainexplainhelp'] = 'These domains are found in your content, but do not appear to support HTTPS content. After switching to HTTPS, the content included from these sites will no longer display within Moodle for users with secure modern browsers. It is possible that these sites are temporarily or permanently unavailable and will not work with either security setting. Proceed only after reviewing these results and determining if this externally hosted content is non-essential. Note: This content would no longer work upon switching to HTTPS anyway.';
+$string['httpwarning'] = 'This instance is still running on HTTP. You can still run this tool and external content will be changed to HTTPS, but internal content will remain on HTTP. You will need to run this script again after switching to HTTPS to convert internal content.';
+$string['notimplemented'] = 'Sorry, this feature is not implemented in your database driver.';
+$string['oktoprocede'] = 'The scan finds no issues with your content. You can proceed to upgrade any HTTP content to use HTTPS.';
+$string['pageheader'] = 'Upgrade externally hosted content urls to HTTPS';
+$string['pluginname'] = 'HTTPS conversion tool';
+$string['replacing'] = 'Replacing HTTP content with HTTPS...';
+$string['searching'] = 'Searching {$a}';
+$string['takeabackupwarning'] = 'Once this is tool run, changes made can\'t be reverted. A complete backup should be made before running this script. There is a low risk that the wrong content will be replaced, introducing problems.';
+$string['toolintro'] = 'If you are planning on converting your site to HTTPS, you can use the <a href="{$a}">HTTPS conversion tool</a> to convert your embeded content to HTTPS.';
diff --git a/admin/tool/httpsreplace/settings.php b/admin/tool/httpsreplace/settings.php
new file mode 100644 (file)
index 0000000..bf5c259
--- /dev/null
@@ -0,0 +1,41 @@
+<?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/>.
+
+/**
+ * Link to http -> https replace script.
+ *
+ * @package tool_httpsreplace
+ * @copyright Copyright (c) 2016 Blackboard Inc. (http://www.blackboard.com)
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die;
+
+if ($hassiteconfig) {
+
+    $pluginname = get_string('pluginname', 'tool_httpsreplace');
+    $url = $CFG->wwwroot.'/'.$CFG->admin.'/tool/httpsreplace/index.php';
+    $ADMIN->add('security', new admin_externalpage('toolhttpsreplace', $pluginname, $url, 'moodle/site:config', true));
+
+    $httpsreplaceurl = $CFG->wwwroot.'/'.$CFG->admin.'/tool/httpsreplace/index.php';
+    $ADMIN->locate('httpsecurity')->add(
+        new admin_setting_heading(
+            'tool_httpsreplaceheader',
+            new lang_string('pluginname', 'tool_httpsreplace'),
+            new lang_string('toolintro', 'tool_httpsreplace', $httpsreplaceurl)
+        )
+    );
+}
diff --git a/admin/tool/httpsreplace/tests/behat/httpsreplace.feature b/admin/tool/httpsreplace/tests/behat/httpsreplace.feature
new file mode 100644 (file)
index 0000000..82bbf59
--- /dev/null
@@ -0,0 +1,29 @@
+@tool @tool_httpsreplace
+Feature: View the httpsreplace report
+  In order to switch to https
+  As an admin
+  I need to be able to automatically replace http links
+
+  Background: Create some http links
+    Given I am on site homepage
+    And the following "courses" exist:
+      | fullname | shortname | category | summary                                                                                                     |
+      | Course 1 | C1        | 0        | <img src="http://intentionally.unavailable/test.png"> <img src="http://download.moodle.org/unittest/test.jpg"> |
+    And I log in as "admin"
+
+  @javascript
+  Scenario: Go to the HTTPS replace report screen. Make sure broken domains are reported.
+    When I navigate to "HTTP security" node in "Site administration > Security"
+    And I follow "HTTPS conversion tool"
+    And I press "Continue"
+    Then I should see "intentionally.unavailable"
+
+  @javascript
+  Scenario: Use the find and replace tool.
+    When I navigate to "HTTP security" node in "Site administration > Security"
+    And I follow "HTTPS conversion tool"
+    And I press "Continue"
+    And I set the field "I understand the risks of this operation" to "1"
+    And I press "Perform replacement"
+    Then I should see "intentionally.unavailable"
+    And I should see "download.moodle.org"
diff --git a/admin/tool/httpsreplace/tests/httpsreplace_test.php b/admin/tool/httpsreplace/tests/httpsreplace_test.php
new file mode 100644 (file)
index 0000000..c10ea2f
--- /dev/null
@@ -0,0 +1,413 @@
+<?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/>.
+
+/**
+ * HTTPS find and replace Tests
+ *
+ * @package   tool_httpsreplace
+ * @copyright Copyright (c) 2016 Blackboard Inc. (http://www.blackboard.com)
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace tool_httpsreplace\tests;
+
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * Tests the httpsreplace tool.
+ *
+ * @package   tool_httpsreplace
+ * @copyright Copyright (c) 2016 Blackboard Inc. (http://www.blackboard.com)
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class httpsreplace_test extends \advanced_testcase {
+
+    /**
+     * Data provider for test_upgrade_http_links
+     */
+    public function upgrade_http_links_provider() {
+        global $CFG;
+        // Get the http url, since the default test wwwroot is https.
+        $wwwroothttp = preg_replace('/^https:/', 'http:', $CFG->wwwroot);
+        return [
+            "Test image from another site should be replaced" => [
+                "content" => '<img src="' . $this->getExternalTestFileUrl('/test.jpg', false) . '">',
+                "outputregex" => '/UPDATE/',
+                "expectedcontent" => '<img src="' . $this->getExternalTestFileUrl('/test.jpg', true) . '">',
+            ],
+            "Test object from another site should be replaced" => [
+                "content" => '<object data="' . $this->getExternalTestFileUrl('/test.swf', false) . '">',
+                "outputregex" => '/UPDATE/',
+                "expectedcontent" => '<object data="' . $this->getExternalTestFileUrl('/test.swf', true) . '">',
+            ],
+            "Test image from a site with international name should be replaced" => [
+                "content" => '<img src="http://中国互联网络信息中心.中国/logosy/201706/W01.png">',
+                "outputregex" => '/UPDATE/',
+                "expectedcontent" => '<img src="https://中国互联网络信息中心.中国/logosy/201706/W01.png">',
+            ],
+            "Link that is from this site should be replaced" => [
+                "content" => '<img src="' . $wwwroothttp . '/logo.png">',
+                "outputregex" => '/UPDATE/',
+                "expectedcontent" => '<img src="' . $CFG->wwwroot . '/logo.png">',
+            ],
+            "Link that is from this site, https new so doesn't need replacing" => [
+                "content" => '<img src="' . $CFG->wwwroot . '/logo.png">',
+                "outputregex" => '/^$/',
+                "expectedcontent" => '<img src="' . $CFG->wwwroot . '/logo.png">',
+            ],
+            "Unavailable image should be replaced" => [
+                "content" => '<img src="http://intentionally.unavailable/link1.jpg">',
+                "outputregex" => '/UPDATE/',
+                "expectedcontent" => '<img src="https://intentionally.unavailable/link1.jpg">',
+            ],
+            "Https content that has an http url as a param should not be replaced" => [
+                "content" => '<img src="https://anothersite.com?param=http://asdf.com">',
+                "outputregex" => '/^$/',
+                "expectedcontent" => '<img src="https://anothersite.com?param=http://asdf.com">',
+            ],
+            "Search for params should be case insensitive" => [
+                "content" => '<object DATA="' . $this->getExternalTestFileUrl('/test.swf', false) . '">',
+                "outputregex" => '/UPDATE/',
+                "expectedcontent" => '<object DATA="' . $this->getExternalTestFileUrl('/test.swf', true) . '">',
+            ],
+            "URL should be case insensitive" => [
+                "content" => '<object data="HTTP://some.site/path?query">',
+                "outputregex" => '/UPDATE/',
+                "expectedcontent" => '<object data="https://some.site/path?query">',
+            ],
+            "More params should not interfere" => [
+                "content" => '<img alt="A picture" src="' . $this->getExternalTestFileUrl('/test.png', false) .
+                    '" width="1”><p style="font-size: \'20px\'"></p>',
+                "outputregex" => '/UPDATE/',
+                "expectedcontent" => '<img alt="A picture" src="' . $this->getExternalTestFileUrl('/test.png', true) .
+                    '" width="1”><p style="font-size: \'20px\'"></p>',
+            ],
+            "Broken URL should not be changed" => [
+                "content" => '<img src="broken.' . $this->getExternalTestFileUrl('/test.png', false) . '">',
+                "outputregex" => '/^$/',
+                "expectedcontent" => '<img src="broken.' . $this->getExternalTestFileUrl('/test.png', false) . '">',
+            ],
+            "Link URL should not be changed" => [
+                "content" => '<a href="' . $this->getExternalTestFileUrl('/test.png', false) . '">' .
+                    $this->getExternalTestFileUrl('/test.png', false) . '</a>',
+                "outputregex" => '/^$/',
+                "expectedcontent" => '<a href="' . $this->getExternalTestFileUrl('/test.png', false) . '">' .
+                    $this->getExternalTestFileUrl('/test.png', false) . '</a>',
+            ],
+            "Test image from another site should be replaced but link should not" => [
+                "content" => '<a href="' . $this->getExternalTestFileUrl('/test.png', false) . '"><img src="' .
+                    $this->getExternalTestFileUrl('/test.jpg', false) . '"></a>',
+                "outputregex" => '/UPDATE/',
+                "expectedcontent" => '<a href="' . $this->getExternalTestFileUrl('/test.png', false) . '"><img src="' .
+                    $this->getExternalTestFileUrl('/test.jpg', true) . '"></a>',
+            ],
+        ];
+    }
+
+    /**
+     * Test upgrade_http_links
+     * @param string $content Example content that we'll attempt to replace.
+     * @param string $ouputregex Regex for what output we expect.
+     * @param string $expectedcontent What content we are expecting afterwards.
+     * @dataProvider upgrade_http_links_provider
+     */
+    public function test_upgrade_http_links($content, $ouputregex, $expectedcontent) {
+        global $DB;
+
+        $this->resetAfterTest();
+        $this->expectOutputRegex($ouputregex);
+
+        $finder = new tool_httpreplace_url_finder_test();
+
+        $generator = $this->getDataGenerator();
+        $course = $generator->create_course((object) [
+            'summary' => $content,
+        ]);
+
+        $finder->upgrade_http_links();
+
+        $summary = $DB->get_field('course', 'summary', ['id' => $course->id]);
+        $this->assertContains($expectedcontent, $summary);
+    }
+
+    /**
+     * Data provider for test_http_link_stats
+     */
+    public function http_link_stats_provider() {
+        global $CFG;
+        // Get the http url, since the default test wwwroot is https.
+        $wwwrootdomain = 'www.example.com';
+        $wwwroothttp = preg_replace('/^https:/', 'http:', $CFG->wwwroot);
+        $testdomain = 'download.moodle.org';
+        return [
+            "Test image from an available site so shouldn't be reported" => [
+                "content" => '<img src="' . $this->getExternalTestFileUrl('/test.jpg', false) . '">',
+                "domain" => $testdomain,
+                "expectedcount" => 0,
+            ],
+            "Link that is from this site shouldn't be reported" => [
+                "content" => '<img src="' . $wwwroothttp . '/logo.png">',
+                "domain" => $wwwrootdomain,
+                "expectedcount" => 0,
+            ],
+            "Unavailable, but https shouldn't be reported" => [
+                "content" => '<img src="https://intentionally.unavailable/logo.png">',
+                "domain" => 'intentionally.unavailable',
+                "expectedcount" => 0,
+            ],
+            "Unavailable image should be reported" => [
+                "content" => '<img src="http://intentionally.unavailable/link1.jpg">',
+                "domain" => 'intentionally.unavailable',
+                "expectedcount" => 1,
+            ],
+            "Unavailable object should be reported" => [
+                "content" => '<object data="http://intentionally.unavailable/file.swf">',
+                "domain" => 'intentionally.unavailable',
+                "expectedcount" => 1,
+            ],
+            "Link should not be reported" => [
+                "content" => '<a href="http://intentionally.unavailable/page.php">Link</a>',
+                "domain" => 'intentionally.unavailable',
+                "expectedcount" => 0,
+            ],
+            "Text should not be reported" => [
+                "content" => 'http://intentionally.unavailable/page.php',
+                "domain" => 'intentionally.unavailable',
+                "expectedcount" => 0,
+            ],
+        ];
+    }
+
+    /**
+     * Test http_link_stats
+     * @param string $content Example content that we'll attempt to replace.
+     * @param string $domain The domain we will check was replaced.
+     * @param string $expectedcount Number of urls from that domain that we expect to be replaced.
+     * @dataProvider http_link_stats_provider
+     */
+    public function test_http_link_stats($content, $domain, $expectedcount) {
+        $this->resetAfterTest();
+
+        $finder = new tool_httpreplace_url_finder_test();
+
+        $generator = $this->getDataGenerator();
+        $course = $generator->create_course((object) [
+            'summary' => $content,
+        ]);
+
+        $results = $finder->http_link_stats();
+
+        $this->assertEquals($expectedcount, $results[$domain] ?? 0);
+    }
+
+    /**
+     * Test links and text are not changed
+     */
+    public function test_links_and_text() {
+        global $DB;
+
+        $this->resetAfterTest();
+        $this->expectOutputRegex('/^$/');
+
+        $finder = new tool_httpreplace_url_finder_test();
+
+        $generator = $this->getDataGenerator();
+        $course = $generator->create_course((object) [
+            'summary' => '<a href="http://intentionally.unavailable/page.php">Link</a> http://other.unavailable/page.php',
+        ]);
+
+        $results = $finder->http_link_stats();
+        $this->assertCount(0, $results);
+
+        $finder->upgrade_http_links();
+
+        $results = $finder->http_link_stats();
+        $this->assertCount(0, $results);
+
+        $summary = $DB->get_field('course', 'summary', ['id' => $course->id]);
+        $this->assertContains('http://intentionally.unavailable/page.php', $summary);
+        $this->assertContains('http://other.unavailable/page.php', $summary);
+        $this->assertNotContains('https://intentionally.unavailable', $summary);
+        $this->assertNotContains('https://other.unavailable', $summary);
+    }
+
+    /**
+     * If we have an http wwwroot then we shouldn't report it.
+     */
+    public function test_httpwwwroot() {
+        global $DB, $CFG;
+
+        $this->resetAfterTest();
+        $CFG->wwwroot = preg_replace('/^https:/', 'http:', $CFG->wwwroot);
+        $this->expectOutputRegex('/^$/');
+
+        $finder = new tool_httpreplace_url_finder_test();
+
+        $generator = $this->getDataGenerator();
+        $course = $generator->create_course((object) [
+            'summary' => '<img src="' . $CFG->wwwroot . '/image.png">',
+        ]);
+
+        $results = $finder->http_link_stats();
+        $this->assertCount(0, $results);
+
+        $finder->upgrade_http_links();
+        $summary = $DB->get_field('course', 'summary', ['id' => $course->id]);
+        $this->assertContains($CFG->wwwroot, $summary);
+    }
+
+    /**
+     * Test that links in excluded tables are not replaced
+     */
+    public function test_upgrade_http_links_excluded_tables() {
+        $this->resetAfterTest();
+
+        set_config('test_upgrade_http_links', '<img src="http://somesite/someimage.png" />');
+
+        $finder = new tool_httpreplace_url_finder_test();
+        ob_start();
+        $results = $finder->upgrade_http_links();
+        $output = ob_get_contents();
+        ob_end_clean();
+        $this->assertTrue($results);
+        $this->assertNotContains('https://somesite', $output);
+        $testconf = get_config('core', 'test_upgrade_http_links');
+        $this->assertContains('http://somesite', $testconf);
+        $this->assertNotContains('https://somesite', $testconf);
+    }
+
+    /**
+     * Test renamed domains
+     */
+    public function test_renames() {
+        global $DB, $CFG;
+        $this->resetAfterTest();
+        $this->expectOutputRegex('/UPDATE/');
+
+        $renames = [
+            'example.com' => 'secure.example.com',
+        ];
+
+        set_config('renames', json_encode($renames), 'tool_httpsreplace');
+
+        $finder = new tool_httpreplace_url_finder_test();
+
+        $generator = $this->getDataGenerator();
+        $course = $generator->create_course((object) [
+            'summary' => '<script src="http://example.com/test.js"><img src="http://EXAMPLE.COM/someimage.png">',
+        ]);
+
+        $results = $finder->http_link_stats();
+        $this->assertCount(0, $results);
+
+        $finder->upgrade_http_links();
+
+        $summary = $DB->get_field('course', 'summary', ['id' => $course->id]);
+        $this->assertContains('https://secure.example.com', $summary);
+        $this->assertNotContains('http://example.com', $summary);
+        $this->assertEquals('<script src="https://secure.example.com/test.js">' .
+            '<img src="https://secure.example.com/someimage.png">', $summary);
+    }
+
+    /**
+     * When there are many different pieces of contents from the same site, we should only run replace once
+     */
+    public function test_multiple() {
+        global $DB;
+        $this->resetAfterTest();
+        $original1 = '';
+        $expected1 = '';
+        $original2 = '';
+        $expected2 = '';
+        for ($i = 0; $i < 15; $i++) {
+            $original1 .= '<img src="http://example.com/image' . $i . '.png">';
+            $expected1 .= '<img src="https://example.com/image' . $i . '.png">';
+            $original2 .= '<img src="http://example.com/image' . ($i + 15 ) . '.png">';
+            $expected2 .= '<img src="https://example.com/image' . ($i + 15) . '.png">';
+        }
+        $finder = new tool_httpreplace_url_finder_test();
+
+        $generator = $this->getDataGenerator();
+        $course1 = $generator->create_course((object) ['summary' => $original1]);
+        $course2 = $generator->create_course((object) ['summary' => $original2]);
+
+        ob_start();
+        $finder->upgrade_http_links();
+        $output = ob_get_contents();
+        ob_end_clean();
+
+        // Make sure everything is replaced.
+        $summary1 = $DB->get_field('course', 'summary', ['id' => $course1->id]);
+        $this->assertEquals($expected1, $summary1);
+        $summary2 = $DB->get_field('course', 'summary', ['id' => $course2->id]);
+        $this->assertEquals($expected2, $summary2);
+
+        // Make sure only one UPDATE statment was called.
+        $this->assertEquals(1, preg_match_all('/UPDATE/', $output));
+    }
+
+    /**
+     * Test the tool when the column name is a reserved word in SQL (in this case 'where')
+     */
+    public function test_reserved_words() {
+        global $DB;
+
+        $this->resetAfterTest();
+        $this->expectOutputRegex('/UPDATE/');
+
+        // Create a table with a field that is a reserved SQL word.
+        $dbman = $DB->get_manager();
+        $table = new \xmldb_table('reserved_words_temp');
+        $table->add_field('id', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, XMLDB_SEQUENCE, null);
+        $table->add_field('where', XMLDB_TYPE_TEXT, null, null, null, null, null);
+        $table->add_key('primary', XMLDB_KEY_PRIMARY, array('id'));
+        $dbman->create_table($table);
+
+        // Insert a record with an <img> in this table and run tool.
+        $content = '<img src="http://example.com/image.png">';
+        $expectedcontent = '<img src="https://example.com/image.png">';
+        $columnamequoted = $dbman->generator->getEncQuoted('where');
+        $DB->execute("INSERT INTO {reserved_words_temp} ($columnamequoted) VALUES (?)", [$content]);
+
+        $finder = new tool_httpreplace_url_finder_test();
+        $finder->upgrade_http_links();
+
+        $record = $DB->get_record('reserved_words_temp', []);
+        $this->assertContains($expectedcontent, $record->where);
+
+        $dbman->drop_table($table);
+    }
+}
+
+/**
+ * Class tool_httpreplace_url_finder_test for testing replace tool without calling curl
+ *
+ * @package   tool_httpsreplace
+ * @copyright 2017 Marina Glancy
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class tool_httpreplace_url_finder_test extends \tool_httpsreplace\url_finder {
+    /**
+     * Check if url is available (check hardcoded for unittests)
+     *
+     * @param string $url
+     * @return bool
+     */
+    protected function check_domain_availability($url) {
+        return !preg_match('|\.unavailable/$|', $url);
+    }
+}
diff --git a/admin/tool/httpsreplace/tool.php b/admin/tool/httpsreplace/tool.php
new file mode 100644 (file)
index 0000000..12d5acb
--- /dev/null
@@ -0,0 +1,109 @@
+<?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/>.
+
+/**
+ * Search and replace http -> https throughout all texts in the whole database
+ *
+ * @package    tool_httpsreplace
+ * @copyright Copyright (c) 2016 Blackboard Inc. (http://www.blackboard.com)
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+define('NO_OUTPUT_BUFFERING', true);
+
+require_once(__DIR__ . '/../../../config.php');
+require_once($CFG->dirroot . '/course/lib.php');
+require_once($CFG->libdir . '/adminlib.php');
+
+admin_externalpage_setup('toolhttpsreplace');
+
+$context = context_system::instance();
+
+require_login();
+require_capability('moodle/site:config', $context);
+
+$PAGE->set_context($context);
+$PAGE->set_url(new moodle_url('/admin/tool/httpsreplace/index.php'));
+$PAGE->set_title(get_string('pageheader', 'tool_httpsreplace'));
+$PAGE->set_pagelayout('admin');
+
+echo $OUTPUT->header();
+
+echo $OUTPUT->heading(get_string('pageheader', 'tool_httpsreplace'));
+
+if (!$DB->replace_all_text_supported()) {
+    echo $OUTPUT->notification(get_string('notimplemented', 'tool_httpsreplace'));
+    echo $OUTPUT->footer();
+    die;
+}
+
+if (!is_https()) {
+    echo $OUTPUT->notification(get_string('httpwarning', 'tool_httpsreplace'), 'warning');
+}
+
+$form = new \tool_httpsreplace\form();
+
+$finder = new \tool_httpsreplace\url_finder();
+
+$PAGE->set_cacheable(false);
+$progressbar = new progress_bar();
+
+if (!$data = $form->get_data()) {
+
+    echo $progressbar->create();
+
+    $results = $finder->http_link_stats($progressbar);
+
+    $progressbar->update_full(100, get_string('complete', 'tool_httpsreplace'));
+
+    if (empty($results)) {
+        echo '<p>'.get_string('oktoprocede', 'tool_httpsreplace').'</p>';
+    } else {
+        arsort($results);
+        $table = new html_table();
+        $table->id = 'plugins-check';
+        $table->head = array(
+            get_string('domain', 'tool_httpsreplace'),
+            get_string('count', 'tool_httpsreplace'),
+        );
+        $data = array();
+        foreach ($results as $domain => $count) {
+            $cleandomain = format_text($domain, FORMAT_PLAIN);
+            $data[] = [$cleandomain, $count];
+        }
+        $table->data = $data;
+        echo html_writer::table($table);
+        echo get_string('domainexplainhelp', 'tool_httpsreplace');
+    }
+    echo $OUTPUT->notification(get_string('takeabackupwarning', 'tool_httpsreplace'), 'warning');
+    $form->display();
+} else {
+    // Scroll to the end when finished.
+    $PAGE->requires->js_init_code("window.scrollTo(0, document.body.scrollHeight);");
+
+    echo html_writer::tag('p', get_string('replacing', 'tool_httpsreplace'));
+
+    echo $progressbar->create();
+
+    echo $OUTPUT->box_start();
+    $finder->upgrade_http_links($progressbar);
+    echo $OUTPUT->box_end();
+
+    $progressbar->update_full(100, get_string('complete', 'tool_httpsreplace'));
+
+    echo $OUTPUT->continue_button(new moodle_url('/admin/settings.php', ['section' => 'httpsecurity']));
+}
+echo $OUTPUT->footer();
diff --git a/admin/tool/httpsreplace/version.php b/admin/tool/httpsreplace/version.php
new file mode 100644 (file)
index 0000000..350331c
--- /dev/null
@@ -0,0 +1,29 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * Version details.
+ *
+ * @package    tool_httpsreplace
+ * @copyright Copyright (c) 2016 Blackboard Inc. (http://www.blackboard.com)
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die();
+
+$plugin->version   = 2017082500; // The current plugin version (Date: YYYYMMDDXX).
+$plugin->requires  = 2017082400; // Requires this Moodle version.
+$plugin->component = 'tool_httpsreplace'; // Full name of the plugin (used for diagnostics).
index 493b7da..8741016 100644 (file)
@@ -30,6 +30,7 @@ use context_system;
 use moodle_url;
 use moodle_exception;
 use lang_string;
+use curl;
 
 /**
  * API exposed by tool_mobile, to be used mostly by external functions and the plugin settings.
@@ -367,4 +368,86 @@ class api {
 
         return $features;
     }
+
+    /**
+     * This function check the current site for potential configuration issues that may prevent the mobile app to work.
+     *
+     * @return array list of potential issues
+     * @since  Moodle 3.4
+     */
+    public static function get_potential_config_issues() {
+        global $CFG;
+        require_once($CFG->dirroot . "/lib/filelib.php");
+        require_once($CFG->dirroot . '/message/lib.php');
+
+        $warnings = array();
+
+        $curl = new curl();
+        // Return certificate information and verify the certificate.
+        $curl->setopt(array('CURLOPT_CERTINFO' => 1, 'CURLOPT_SSL_VERIFYPEER' => true));
+        $httpswwwroot = str_replace('http:', 'https:', $CFG->wwwroot); // Force https url.
+        $curl->head($httpswwwroot . "/login/index.php");
+        $info = $curl->get_info();
+
+        // First of all, check the server certificate (if any).
+        if (empty($info['http_code']) or ($info['http_code'] >= 400)) {
+            $warnings[] = ['nohttpsformobilewarning', 'admin'];
+        } else {
+            // Check the certificate is not self-signed or has an untrusted-root.
+            // This may be weak in some scenarios (when the curl SSL verifier is outdated).
+            if (empty($info['certinfo'])) {
+                $warnings[] = ['selfsignedoruntrustedcertificatewarning', 'tool_mobile'];
+            } else {
+                $timenow = time();
+                $expectedissuer = null;
+                foreach ($info['certinfo'] as $cert) {
+                    // Check if the signature algorithm is weak (Android won't work with SHA-1).
+                    if ($cert['Signature Algorithm'] == 'sha1WithRSAEncryption' || $cert['Signature Algorithm'] == 'sha1WithRSA') {
+                        $warnings[] = ['insecurealgorithmwarning', 'tool_mobile'];
+                    }
+                    // Check certificate start date.
+                    if (strtotime($cert['Start date']) > $timenow) {
+                        $warnings[] = ['invalidcertificatestartdatewarning', 'tool_mobile'];
+                    }
+                    // Check certificate end date.
+                    if (strtotime($cert['Expire date']) < $timenow) {
+                        $warnings[] = ['invalidcertificateexpiredatewarning', 'tool_mobile'];
+                    }
+                    // Check the chain.
+                    if ($expectedissuer !== null) {
+                        if ($expectedissuer !== $cert['Subject'] || $cert['Subject'] === $cert['Issuer']) {
+                            $warnings[] = ['invalidcertificatechainwarning', 'tool_mobile'];
+                        }
+                    }
+                    $expectedissuer = $cert['Issuer'];
+                }
+            }
+        }
+        // Now check typical configuration problems.
+        if ((int) $CFG->userquota === PHP_INT_MAX) {
+            // In old Moodle version was a text so was possible to have numeric values > PHP_INT_MAX.
+            $warnings[] = ['invaliduserquotawarning', 'tool_mobile'];
+        }
+        // Check ADOdb debug enabled.
+        if (get_config('auth_db', 'debugauthdb') || get_config('enrol_database', 'debugdb')) {
+            $warnings[] = ['adodbdebugwarning', 'tool_mobile'];
+        }
+        // Check display errors on.
+        if (!empty($CFG->debugdisplay)) {
+            $warnings[] = ['displayerrorswarning', 'tool_mobile'];
+        }
+        // Check mobile notifications.
+        $processors = get_message_processors();
+        $enabled = false;
+        foreach ($processors as $processor => $status) {
+            if ($processor == 'airnotifier' && $status->enabled) {
+                $enabled = true;
+            }
+        }
+        if (!$enabled) {
+            $warnings[] = ['mobilenotificationsdisabledwarning', 'tool_mobile'];
+        }
+
+        return $warnings;
+    }
 }
index c8b207c..d82695a 100644 (file)
@@ -22,6 +22,7 @@
  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  */
 
+$string['adodbdebugwarning'] = 'ADOdb debugging is enabled. It should be disabled in the external database authentication or external database enrolment plugin settings.';
 $string['androidappid'] = 'Android app\'s unique identifier';
 $string['androidappid_desc'] = 'This setting may be left as default unless you have a custom Android app.';
 $string['autologinkeygenerationlockout'] = 'Auto-login key generation is blocked because of too many requests within an hour.';
@@ -49,6 +50,7 @@ Mis calificaciones|https://someurl.xyz/local/mygrades/index.php|embedded|es
 </pre>';
 $string['disabledfeatures'] = 'Disabled features';
 $string['disabledfeatures_desc'] = 'Select here the features you want to disable in the Mobile app for your site. Please note that some features listed here could be already disabled via other site settings. You will have to log out and log in again in the app to see the changes.';
+$string['displayerrorswarning'] = 'Display debug messages (debugdisplay) is enabled. It should be disabled.';
 $string['enablesmartappbanners'] = 'Enable App Banners';
 $string['enablesmartappbanners_desc'] = 'If enabled, a banner promoting the mobile app will be displayed when accessing the site using a mobile browser.';
 $string['forcedurlscheme'] = 'If you want to allow only your custom branded app to be opened via a browser window, then specify its URL scheme here; otherwise leave the field empty.';
@@ -56,7 +58,12 @@ $string['forcedurlscheme_key'] = 'URL scheme';
 $string['forcelogout'] = 'Force log out';
 $string['forcelogout_desc'] = 'If enabled, the app option \'Change site\' is replaced by \'Log out\'. This results in the user being completely logged out. They must then re-enter their password the next time they wish to access the site.';
 $string['httpsrequired'] = 'HTTPS required';
+$string['insecurealgorithmwarning'] = 'It seems that the HTTPS certificate uses an insecure algorithm for signing (SHA-1). Please try updating the certificate.';
+$string['invalidcertificatechainwarning'] = 'It seems that the certificate chain is invalid.';
+$string['invalidcertificateexpiredatewarning'] = 'It seems that the HTTPS certificate for the site has expired.';
+$string['invalidcertificatestartdatewarning'] = 'It seems that the HTTPS certificate for the site is not yet valid (with a start date in the future).';
 $string['invalidprivatetoken'] = 'Invalid private token. Token should not be empty or passed via GET parameter.';
+$string['invaliduserquotawarning'] = 'The user quota (userquota) is set to an invalid number. It should be set to a valid number (an integer value) in Site policies.';
 $string['iosappid'] = 'iOS app\'s unique identifier';
 $string['iosappid_desc'] = 'This setting may be left as default unless you have a custom iOS app.';
 $string['loginintheapp'] = 'Via the app';
@@ -70,8 +77,10 @@ $string['mobileappearance'] = 'Mobile appearance';
 $string['mobileauthentication'] = 'Mobile authentication';
 $string['mobilecssurl'] = 'CSS';
 $string['mobilefeatures'] = 'Mobile features';
+$string['mobilenotificationsdisabledwarning'] = 'Mobile notifications are not enabled. They should be enabled in Manage message outputs.';
 $string['mobilesettings'] = 'Mobile settings';
 $string['pluginname'] = 'Moodle Mobile tools';
+$string['selfsignedoruntrustedcertificatewarning'] = 'It seems that the HTTPS certificate is self-signed or not trusted. The mobile app will only work with trusted sites.';
 $string['setuplink'] = 'App download page';
 $string['setuplink_desc'] = 'URL of page with links to download the mobile app from the App Store and Google Play.';
 $string['smartappbanners'] = 'App Banners';
index ece75c4..f5ee434 100644 (file)
@@ -63,4 +63,35 @@ class tool_mobile_api_testcase extends externallib_advanced_testcase {
         $this->assertTimeCurrent($key->validuntil - api::LOGIN_KEY_TTL);
         $this->assertEquals('0.0.0.0', $key->iprestriction);
     }
+
+    /**
+     * Test get_potential_config_issues.
+     */
+    public function test_get_potential_config_issues() {
+        global $CFG;
+        require_once($CFG->dirroot . '/message/lib.php');
+
+        $this->resetAfterTest(true);
+        $this->setAdminUser();
+
+        $CFG->userquota = '73289234723498234723423489273423497234234';
+        $CFG->debugdisplay = 1;
+        set_config('debugauthdb', 1, 'auth_db');
+        set_config('debugdb', 1, 'enrol_database');
+        $expectedissues = array('nohttpsformobilewarning', 'invaliduserquotawarning', 'adodbdebugwarning', 'displayerrorswarning',
+            'mobilenotificationsdisabledwarning');
+
+        $processors = get_message_processors();
+        foreach ($processors as $processor => $status) {
+            if ($processor == 'airnotifier' && $status->enabled) {
+                unset($expectedissues['mobilenotificationsdisabledwarning']);
+            }
+        }
+
+        $issues = api::get_potential_config_issues();
+        $this->assertCount(count($expectedissues), $issues);
+        foreach ($issues as $issue) {
+            $this->assertTrue(in_array($issue[0], $expectedissues));
+        }
+    }
 }
index 417008a..efc5448 100644 (file)
Binary files a/admin/tool/usertours/amd/build/usertours.min.js and b/admin/tool/usertours/amd/build/usertours.min.js differ
index 4541f23..044180b 100644 (file)
@@ -78,18 +78,22 @@ function(ajax, BootstrapTour, $, templates, str, log, notification) {
          * @method  addResetLink
          */
         addResetLink: function() {
-            str.get_string('resettouronpage', 'tool_usertours')
-                .done(function(s) {
-                    // Grab the last item in the page of these.
-                    $('footer, .logininfo')
-                    .last()
-                    .append(
-                        '<div class="usertour">' +
-                            '<a href="#" data-action="tool_usertours/resetpagetour">' +
-                                s +
-                            '</a>' +
-                        '</div>'
-                    );
+            var ele;
+            // Append the link to the most suitable place on the page
+            // with fallback to legacy selectors and finally the body
+            // if there is no better place.
+            if ($('.tool_usertours-resettourcontainer').length) {
+                ele = $('.tool_usertours-resettourcontainer');
+            } else if ($('.logininfo').length) {
+                ele = $('.logininfo');
+            } else if ($('footer').length) {
+                ele = $('footer');
+            } else {
+                ele = $('body');
+            }
+            templates.render('tool_usertours/resettour', {})
+                .done(function(html, js) {
+                    templates.appendNodeContents(ele, html, js);
                 });
         },
 
diff --git a/admin/tool/usertours/templates/resettour.mustache b/admin/tool/usertours/templates/resettour.mustache
new file mode 100644 (file)
index 0000000..b1fe728
--- /dev/null
@@ -0,0 +1,27 @@
+{{!
+    This file is part of Moodle - http://moodle.org/
+
+    Moodle is free software: you can redistribute it and/or modify
+    it under the terms of the GNU General Public License as published by
+    the Free Software Foundation, either version 3 of the License, or
+    (at your option) any later version.
+
+    Moodle is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+    GNU General Public License for more details.
+
+    You should have received a copy of the GNU General Public License
+    along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+}}
+{{!
+    @template tool_usertours/resettour
+
+    Adds the Reset user tour on this page link.
+
+    Example context (json):
+    {}
+}}
+<div class="usertour">
+    <a href="#" data-action="tool_usertours/resetpagetour">{{#str}}resettouronpage, tool_usertours{{/str}}</a>
+</div>
index 444e384..e56c325 100644 (file)
@@ -54,7 +54,7 @@ foreach ($allfunctions as $f) {
         //some plugins may want to have own test client forms
         include_once($CFG->dirroot.'/'.$finfo->testclientpath);
     }
-    $class = $f->name.'_form';
+    $class = $f->name.'_testclient_form';
     if (class_exists($class)) {
         $functions[$f->name] = $f->name;
         continue;
@@ -103,7 +103,7 @@ if (!$function or !$protocol) {
     die;
 }
 
-$class = $function.'_form';
+$class = $function.'_testclient_form';
 
 $mform = new $class(null, array('authmethod' => $authmethod));
 $mform->set_data(array('function'=>$function, 'protocol'=>$protocol));
index b01b78b..9f89f30 100644 (file)
@@ -29,19 +29,23 @@ class webservice_test_client_form extends moodleform {
 // === Test client forms ===
 
 /**
- * Form class for create_categories() web service function test.
+ * Base class for implementations of WS test client forms.
  *
  * @package   core_webservice
  * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
- * @copyright 2012 Fabio Souto
+ * @copyright 2017 Marina Glancy
  */
-class core_course_create_categories_form extends moodleform {
+abstract class webservice_test_client_base_form extends moodleform {
+
+    /**
+     * Definition of the parameters used by this WS function
+     */
+    protected abstract function test_client_definition();
+
     /**
      * The form definition.
      */
     public function definition() {
-        global $CFG;
-
         $mform = $this->_form;
 
         $mform->addElement('header', 'wstestclienthdr', get_string('testclient', 'webservice'));
@@ -59,19 +63,7 @@ class core_course_create_categories_form extends moodleform {
         }
 
         $mform->addElement('hidden', 'authmethod', $data['authmethod']);
-        $mform->setType('authmethod', core_user::get_property_type('auth'));
-        $mform->addElement('text', 'name[0]', 'name[0]');
-        $mform->addElement('text', 'parent[0]', 'parent[0]');
-        $mform->addElement('text', 'idnumber[0]', 'idnumber[0]');
-        $mform->addElement('text', 'description[0]', 'description[0]');
-        $mform->addElement('text', 'name[1]', 'name[1]');
-        $mform->addElement('text', 'parent[1]', 'parent[1]');
-        $mform->addElement('text', 'idnumber[1]', 'idnumber[1]');
-        $mform->addElement('text', 'description[1]', 'description[1]');
-        $mform->setType('name', core_user::get_property_type('firstname'));
-        $mform->setType('parent', core_user::get_property_type('id'));
-        $mform->setType('idnumber', core_user::get_property_type('idnumber'));
-        $mform->setType('description', core_user::get_property_type('description'));
+        $mform->setType('authmethod', PARAM_ALPHA);
 
         $mform->addElement('hidden', 'function');
         $mform->setType('function', PARAM_PLUGIN);
@@ -79,6 +71,8 @@ class core_course_create_categories_form extends moodleform {
         $mform->addElement('hidden', 'protocol');
         $mform->setType('protocol', PARAM_ALPHA);
 
+        $this->test_client_definition();
+
         $this->add_action_buttons(true, get_string('execute', 'webservice'));
     }
 
@@ -90,19 +84,51 @@ class core_course_create_categories_form extends moodleform {
         if (!$data = $this->get_data()) {
             return null;
         }
-        // Remove unused from form data.
-        unset($data->submitbutton);
-        unset($data->protocol);
-        unset($data->function);
-        unset($data->wsusername);
-        unset($data->wspassword);
-        unset($data->token);
-        unset($data->authmethod);
+        return array_diff_key((array)$data, ['submitbutton' => 1, 'protocol' => 1, 'function' => 1,
+            'wsusername' => 1, 'wspassword' => 1, 'token' => 1, 'authmethod' => 1]);
+    }
+}
+
+/**
+ * Form class for create_categories() web service function test.
+ *
+ * @package   core_webservice
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ * @copyright 2012 Fabio Souto
+ */
+class core_course_create_categories_testclient_form extends webservice_test_client_base_form {
+    /**
+     * The form definition.
+     */
+    protected function test_client_definition() {
+        $mform = $this->_form;
+        $mform->addElement('text', 'name[0]', 'name[0]');
+        $mform->addElement('text', 'parent[0]', 'parent[0]');
+        $mform->addElement('text', 'idnumber[0]', 'idnumber[0]');
+        $mform->addElement('text', 'description[0]', 'description[0]');
+        $mform->addElement('text', 'name[1]', 'name[1]');
+        $mform->addElement('text', 'parent[1]', 'parent[1]');
+        $mform->addElement('text', 'idnumber[1]', 'idnumber[1]');
+        $mform->addElement('text', 'description[1]', 'description[1]');
+        $mform->setType('name', PARAM_TEXT);
+        $mform->setType('parent', PARAM_INT);
+        $mform->setType('idnumber', PARAM_RAW);
+        $mform->setType('description', PARAM_RAW);
+    }
+
+    /**
+     * Get the parameters that the user submitted using the form.
+     * @return array|null
+     */
+    public function get_params() {
+        if (!$data = $this->get_data()) {
+            return null;
+        }
 
         $params = array();
         $params['categories'] = array();
         for ($i=0; $i<10; $i++) {
-            if (empty($data->name[$i]) or empty($data->parent[$i])) {
+            if (empty($data->name[$i])) {
                 continue;
             }
             $params['categories'][] = array('name'=>$data->name[$i], 'parent'=>$data->parent[$i],
@@ -119,48 +145,21 @@ class core_course_create_categories_form extends moodleform {
  * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  * @copyright 2012 Fabio Souto
  */
-class core_course_delete_categories_form extends moodleform {
+class core_course_delete_categories_testclient_form extends webservice_test_client_base_form {
     /**
      * The form definition.
      */
-    public function definition() {
-        global $CFG;
-
+    protected function test_client_definition() {
         $mform = $this->_form;
-
-        $mform->addElement('header', 'wstestclienthdr', get_string('testclient', 'webservice'));
-
-        // Note: these values are intentionally PARAM_RAW - we want users to test any rubbish as parameters.
-        $data = $this->_customdata;
-        if ($data['authmethod'] == 'simple') {
-            $mform->addElement('text', 'wsusername', 'wsusername');
-            $mform->setType('wsusername', core_user::get_property_type('username'));
-            $mform->addElement('text', 'wspassword', 'wspassword');
-            $mform->setType('wspassword', core_user::get_property_type('password'));
-        } else if ($data['authmethod'] == 'token') {
-            $mform->addElement('text', 'token', 'token');
-            $mform->setType('token', PARAM_RAW_TRIMMED);
-        }
-
-        $mform->addElement('hidden', 'authmethod', $data['authmethod']);
-        $mform->setType('authmethod', core_user::get_property_type('auth'));
         $mform->addElement('text', 'id[0]', 'id[0]');
         $mform->addElement('text', 'newparent[0]', 'newparent[0]');
         $mform->addElement('text', 'recursive[0]', 'recursive[0]');
         $mform->addElement('text', 'id[1]', 'id[1]');
         $mform->addElement('text', 'newparent[1]', 'newparent[1]');
         $mform->addElement('text', 'recursive[1]', 'recursive[1]');
-        $mform->setType('id', core_user::get_property_type('id'));
+        $mform->setType('id', PARAM_INT);
         $mform->setType('newparent', PARAM_INT);
         $mform->setType('recursive', PARAM_BOOL);
-
-        $mform->addElement('hidden', 'function');
-        $mform->setType('function', PARAM_PLUGIN);
-
-        $mform->addElement('hidden', 'protocol');
-        $mform->setType('protocol', PARAM_ALPHA);
-
-        $this->add_action_buttons(true, get_string('execute', 'webservice'));
     }
 
     /**
@@ -171,15 +170,6 @@ class core_course_delete_categories_form extends moodleform {
         if (!$data = $this->get_data()) {
             return null;
         }
-        // Remove unused from form data.
-        unset($data->submitbutton);
-        unset($data->protocol);
-        unset($data->function);
-        unset($data->wsusername);
-        unset($data->wspassword);
-        unset($data->token);
-        unset($data->authmethod);
-
         $params = array();
         $params['categories'] = array();
         for ($i=0; $i<10; $i++) {
@@ -207,31 +197,12 @@ class core_course_delete_categories_form extends moodleform {
  * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  * @copyright 2012 Fabio Souto
  */
-class core_course_update_categories_form extends moodleform {
+class core_course_update_categories_testclient_form extends webservice_test_client_base_form {
     /**
      * The form definition.
      */
-    public function definition() {
-        global $CFG;
-
+    protected function test_client_definition() {
         $mform = $this->_form;
-
-        $mform->addElement('header', 'wstestclienthdr', get_string('testclient', 'webservice'));
-
-        // Note: these values are intentionally PARAM_RAW - we want users to test any rubbish as parameters.
-        $data = $this->_customdata;
-        if ($data['authmethod'] == 'simple') {
-            $mform->addElement('text', 'wsusername', 'wsusername');
-            $mform->setType('wsusername', core_user::get_property_type('username'));
-            $mform->addElement('text', 'wspassword', 'wspassword');
-            $mform->setType('wspassword', core_user::get_property_type('password'));
-        } else if ($data['authmethod'] == 'token') {
-            $mform->addElement('text', 'token', 'token');
-            $mform->setType('token', PARAM_RAW_TRIMMED);
-        }
-
-        $mform->addElement('hidden', 'authmethod', $data['authmethod']);
-        $mform->setType('authmethod', core_user::get_property_type('auth'));
         $mform->addElement('text', 'id[0]', 'id[0]');
         $mform->addElement('text', 'name[0]', 'name[0]');
         $mform->addElement('text', 'parent[0]', 'parent[0]');
@@ -242,19 +213,11 @@ class core_course_update_categories_form extends moodleform {
         $mform->addElement('text', 'parent[1]', 'parent[1]');
         $mform->addElement('text', 'idnumber[1]', 'idnumber[1]');
         $mform->addElement('text', 'description[1]', 'description[1]');
-        $mform->setType('id', core_user::get_property_type('id'));
-        $mform->setType('name', core_user::get_property_type('firstname'));
+        $mform->setType('id', PARAM_INT);
+        $mform->setType('name', PARAM_TEXT);
         $mform->setType('parent', PARAM_INT);
-        $mform->setType('idnumber', core_user::get_property_type('idnumber'));
-        $mform->setType('description', core_user::get_property_type('description'));
-
-        $mform->addElement('hidden', 'function');
-        $mform->setType('function', PARAM_PLUGIN);
-
-        $mform->addElement('hidden', 'protocol');
-        $mform->setType('protocol', PARAM_ALPHA);
-
-        $this->add_action_buttons(true, get_string('execute', 'webservice'));
+        $mform->setType('idnumber', PARAM_RAW);
+        $mform->setType('description', PARAM_RAW);
     }
 
     /**
@@ -265,15 +228,6 @@ class core_course_update_categories_form extends moodleform {
         if (!$data = $this->get_data()) {
             return null;
         }
-        // Remove unused from form data.
-        unset($data->submitbutton);
-        unset($data->protocol);
-        unset($data->function);
-        unset($data->wsusername);
-        unset($data->wspassword);
-        unset($data->token);
-        unset($data->authmethod);
-
         $params = array();
         $params['categories'] = array();
         for ($i=0; $i<10; $i++) {
@@ -299,4 +253,100 @@ class core_course_update_categories_form extends moodleform {
         }
         return $params;
     }
-}
\ No newline at end of file
+}
+
+/**
+ * Test class for WS function core_fetch_notifications
+ *
+ * @package   core_webservice
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ * @copyright 2017 Marina Glancy
+ */
+class core_fetch_notifications_testclient_form extends webservice_test_client_base_form {
+    /**
+     * The form definition.
+     */
+    protected function test_client_definition() {
+        $mform = $this->_form;
+        $mform->addElement('text', 'contextid', 'contextid');
+        $mform->setType('contextid', PARAM_INT);
+        $mform->setDefault('contextid', context_system::instance()->id);
+    }
+}
+
+/**
+ * Test class for WS function get_site_info
+ *
+ * @package   core_webservice
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ * @copyright 2017 Marina Glancy
+ */
+class core_webservice_get_site_info_testclient_form extends webservice_test_client_base_form {
+    /**
+     * The form definition.
+     */
+    protected function test_client_definition() {
+    }
+}
+
+/**
+ * Test class for WS function core_get_string
+ *
+ * @package   core_webservice
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ * @copyright 2017 Marina Glancy
+ */
+class core_get_string_testclient_form extends webservice_test_client_base_form {
+    /**
+     * The form definition.
+     */
+    protected function test_client_definition() {
+        $mform = $this->_form;
+        $mform->addElement('text', 'stringid', 'stringid');
+        $mform->setType('stringid', PARAM_STRINGID);
+        $mform->addElement('text', 'component', 'component');
+        $mform->setType('component', PARAM_COMPONENT);
+        $mform->addElement('text', 'lang', 'lang');
+        $mform->setType('lang', PARAM_LANG);
+        $mform->addElement('text', 'stringparams_name[1]', 'Parameter 1 name');
+        $mform->setType('stringparams_name[1]', PARAM_ALPHANUMEXT);
+        $mform->addElement('text', 'stringparams_value[1]', 'Parameter 1 value');
+        $mform->setType('stringparams_value[1]', PARAM_RAW);
+        $mform->addElement('text', 'stringparams_name[2]', 'Parameter 2 name');
+        $mform->setType('stringparams_name[2]', PARAM_ALPHANUMEXT);
+        $mform->addElement('text', 'stringparams_value[2]', 'Parameter 2 value');
+        $mform->setType('stringparams_value[2]', PARAM_RAW);
+        $mform->addElement('text', 'stringparams_name[3]', 'Parameter 3 name');
+        $mform->setType('stringparams_name[3]', PARAM_ALPHANUMEXT);
+        $mform->addElement('text', 'stringparams_value[3]', 'Parameter 3 value');
+        $mform->setType('stringparams_value[3]', PARAM_RAW);
+        $mform->addElement('static', 'paramnote', '', 'If a parameter is not an object, only specify "Parameter 1 value"');
+    }
+
+    /**
+     * Get the parameters that the user submitted using the form.
+     * @return array|null
+     */
+    public function get_params() {
+        $params = parent::get_params();
+        if ($params === null) {
+            return null;
+        }
+
+        $params['stringparams'] = [];
+        for ($idx = 1; $idx <= 3; $idx++) {
+            $name = isset($params['stringparams_name'][$idx]) ? strval($params['stringparams_name'][$idx]) : '';
+            $value = isset($params['stringparams_value'][$idx]) ? strval($params['stringparams_value'][$idx]) : '';
+            if ($name !== '' || $value !== '') {
+                if ($name === '') {
+                    $params['stringparams'][] = ['value' => $value];
+                } else {
+                    $params['stringparams'][] = ['name' => $name, 'value' => $value];
+                }
+            }
+        }
+        unset($params['stringparams_name']);
+        unset($params['stringparams_value']);
+        return $params;
+    }
+}
index 203ad55..c523697 100644 (file)
@@ -58,6 +58,15 @@ class admin_setting_predictor extends \admin_setting_configselect {
             return get_string('errorprocessornotready', 'analytics', $isready);
         }
 
+        $currentvalue = get_config('analytics', 'predictionsprocessor');
+        if (!empty($currentvalue) && $currentvalue != str_replace('\\\\', '\\', $data)) {
+            // Clear all models data.
+            $models = \core_analytics\manager::get_all_models();
+            foreach ($models as $model) {
+                $model->clear();
+            }
+        }
+
         return ($this->config_write($this->name, $data) ? '' : get_string('errorsetting', 'admin'));
     }
 }
index 88d10a6..97faf59 100644 (file)
@@ -47,6 +47,13 @@ interface analysable {
      */
     public function get_id();
 
+    /**
+     * The analysable human readable name
+     *
+     * @return string
+     */
+    public function get_name();
+
     /**
      * The analysable context.
      *
index 63ea392..af95120 100644 (file)
@@ -267,7 +267,7 @@ abstract class calculable {
 
         foreach ($arrays as $array) {
             reset($base);
-            while (list($key, $value) = each($array)) {
+            foreach ($array as $key => $value) {
                 if (is_array($value) && !empty($base[$key]) && is_array($base[$key])) {
                     $base[$key] = $this->array_merge_recursive_keep_keys($base[$key], $value);
                 } else {
index 791d7c2..07c5e9d 100644 (file)
@@ -195,6 +195,15 @@ class course implements \core_analytics\analysable {
         return $this->course->id;
     }
 
+    /**
+     * The course short name
+     *
+     * @return string
+     */
+    public function get_name() {
+        return format_string($this->course->shortname, true, array('context' => $this->get_context()));
+    }
+
     /**
      * get_context
      *
index 4b457e8..b467168 100644 (file)
@@ -202,6 +202,61 @@ class dataset_manager {
             '/timesplitting/' . self::clean_time_splitting_id($timesplittingid) . '/', self::EVALUATION_FILENAME);
     }
 
+    /**
+     * Gets the list of files that couldn't be previously used for training and prediction.
+     *
+     * @param int $modelid
+     * @param bool $includetarget
+     * @param string[] $timesplittingids
+     * @return null
+     */
+    public static function get_pending_files($modelid, $includetarget, $timesplittingids) {
+        global $DB;
+
+        $fs = get_file_storage();
+
+        if ($includetarget) {
+            $filearea = self::LABELLED_FILEAREA;
+            $usedfileaction = 'trained';
+        } else {
+            $filearea = self::UNLABELLED_FILEAREA;
+            $usedfileaction = 'predicted';
+        }
+
+        $select = 'modelid = :modelid AND action = :action';
+        $params = array('modelid' => $modelid, 'action' => $usedfileaction);
+        $usedfileids = $DB->get_fieldset_select('analytics_used_files', 'fileid', $select, $params);
+
+        // Very likely that we will only have 1 time splitting method here.
+        $filesbytimesplitting = array();
+        foreach ($timesplittingids as $timesplittingid) {
+
+            $filepath = '/timesplitting/' . self::clean_time_splitting_id($timesplittingid) . '/';
+            $files = $fs->get_directory_files(\context_system::instance()->id, 'analytics', $filearea, $modelid, $filepath);
+            foreach ($files as $file) {
+
+                // Discard evaluation files.
+                if ($file->get_filename() === self::EVALUATION_FILENAME) {
+                    continue;
+                }
+
+                // No dirs.
+                if ($file->is_directory()) {
+                    continue;
+                }
+
+                // Already used for training.
+                if (in_array($file->get_id(), $usedfileids)) {
+                    continue;
+                }
+
+                $filesbytimesplitting[$timesplittingid][] = $file;
+            }
+        }
+
+        return $filesbytimesplitting;
+    }
+
     /**
      * Deletes previous evaluation files of this model.
      *
index 63c75b5..a8277e5 100644 (file)
@@ -118,7 +118,7 @@ abstract class base {
      * \core_analytics\local\analyser\by_course and \core_analytics\local\analyser\sitewide are implementing
      * this method returning site courses (by_course) and the whole system (sitewide) as analysables.
      *
-     * @return \core_analytics\analysable[]
+     * @return \core_analytics\analysable[] Array of analysable elements using the analysable id as array key.
      */
     abstract public function get_analysables();
 
@@ -180,23 +180,63 @@ abstract class base {
      * @return \stored_file[]
      */
     public function get_analysable_data($includetarget) {
+        global $DB;
+
+        // Time limit control.
+        $modeltimelimit = intval(get_config('analytics', 'modeltimelimit'));
 
         $filesbytimesplitting = array();
 
-        $analysables = $this->get_analysables();
-        foreach ($analysables as $analysable) {
+        list($analysables, $processedanalysables) = $this->get_sorted_analysables($includetarget);
+
+        $inittime = time();
+        foreach ($analysables as $key => $analysable) {
 
             $files = $this->process_analysable($analysable, $includetarget);
 
             // Later we will need to aggregate data by time splitting method.
             foreach ($files as $timesplittingid => $file) {
-                $filesbytimesplitting[$timesplittingid][$analysable->get_id()] = $file;
+                $filesbytimesplitting[$timesplittingid][] = $file;
+            }
+
+            $this->update_analysable_analysed_time($processedanalysables, $analysable->get_id(), $includetarget);
+
+            // Apply time limit.
+            if (!$this->options['evaluation']) {
+                $timespent = time() - $inittime;
+                if ($modeltimelimit <= $timespent) {
+                    break;
+                }
+            }
+
+            unset($analysables[$key]);
+        }
+
+        if ($this->options['evaluation'] === false) {
+            // Look for previous training and prediction files we generated and couldn't be used
+            // by machine learning backends because they weren't big enough.
+
+            $pendingfiles = \core_analytics\dataset_manager::get_pending_files($this->modelid, $includetarget,
+                array_keys($filesbytimesplitting));
+            foreach ($pendingfiles as $timesplittingid => $files) {
+                foreach ($files as $file) {
+                    $filesbytimesplitting[$timesplittingid][] = $file;
+                }
             }
         }
 
         // We join the datasets by time splitting method.
         $timesplittingfiles = $this->merge_analysable_files($filesbytimesplitting, $includetarget);
 
+        if (!empty($pendingfiles)) {
+            // We must remove them now as they are already part of another dataset.
+            foreach ($pendingfiles as $timesplittingid => $files) {
+                foreach ($files as $file) {
+                    $file->delete();
+                }
+            }
+        }
+
         return $timesplittingfiles;
     }
 
@@ -323,7 +363,7 @@ abstract class base {
         $result = $this->analysabletarget->is_valid_analysable($analysable, $includetarget);
         if ($result !== true) {
             $a = new \stdClass();
-            $a->analysableid = $analysable->get_id();
+            $a->analysableid = $analysable->get_name();
             $a->result = $result;
             $this->add_log(get_string('analysablenotvalidfortarget', 'analytics', $a));
             return array();
@@ -367,7 +407,7 @@ abstract class base {
             }
 
             $a = new \stdClass();
-            $a->analysableid = $analysable->get_id();
+            $a->analysableid = $analysable->get_name();
             $a->errors = implode(', ', $errors);
             $this->add_log(get_string('analysablenotused', 'analytics', $a));
         }
@@ -721,4 +761,86 @@ abstract class base {
             $data[1][] = $value;
         }
     }
+
+    /**
+     * Returns the list of analysables sorted in processing priority order.
+     *
+     * It will first return analysables that have never been analysed before
+     * and it will continue with the ones we have already seen by timeanalysed DESC
+     * order.
+     *
+     * @param bool $includetarget
+     * @return array(0 => \core_analytics\analysable[], 1 => \stdClass[])
+     */
+    protected function get_sorted_analysables($includetarget) {
+
+        $analysables = $this->get_analysables();
+
+        // Get the list of analysables that have been already processed.
+        $processedanalysables = $this->get_processed_analysables($includetarget);
+
+        // We want to start processing analysables we have not yet processed and later continue
+        // with analysables that we already processed.
+        $unseen = array_diff_key($analysables, $processedanalysables);
+
+        // Var $processed first as we want to respect its timeanalysed DESC order so analysables that
+        // have recently been processed are on the bottom of the stack.
+        $seen = array_intersect_key($processedanalysables, $analysables);
+        array_walk($seen, function(&$value, $analysableid) use ($analysables) {
+            // We replace the analytics_used_analysables record by the analysable object.
+            $value = $analysables[$analysableid];
+        });
+
+        return array($unseen + $seen, $processedanalysables);
+    }
+
+    /**
+     * Get analysables that have been already processed.
+     *
+     * @param bool $includetarget
+     * @return \stdClass[]
+     */
+    protected function get_processed_analysables($includetarget) {
+        global $DB;
+
+        $params = array('modelid' => $this->modelid);
+        $params['action'] = ($includetarget) ? 'training' : 'prediction';
+        $select = 'modelid = :modelid and action = :action';
+
+        // Weird select fields ordering for performance (analysableid key matching, analysableid is also unique by modelid).
+        return $DB->get_records_select('analytics_used_analysables', $select,
+            $params, 'timeanalysed DESC', 'analysableid, modelid, action, timeanalysed, id AS primarykey');
+    }
+
+    /**
+     * Updates the analysable analysis time.
+     *
+     * @param array $processedanalysables
+     * @param int $analysableid
+     * @param bool $includetarget
+     * @return null
+     */
+    protected function update_analysable_analysed_time($processedanalysables, $analysableid, $includetarget) {
+        global $DB;
+
+        if (!empty($processedanalysables[$analysableid])) {
+            $obj = $processedanalysables[$analysableid];
+
+            $obj->id = $obj->primarykey;
+            unset($obj->primarykey);
+
+            $obj->timeanalysed = time();
+            $DB->update_record('analytics_used_analysables', $obj);
+
+        } else {
+
+            $obj = new \stdClass();
+            $obj->modelid = $this->modelid;
+            $obj->action = ($includetarget) ? 'training' : 'prediction';
+            $obj->analysableid = $analysableid;
+            $obj->timeanalysed = time();
+
+            $DB->insert_record('analytics_used_analysables', $obj);
+        }
+    }
 }
index 8e5b266..6661806 100644 (file)
@@ -55,7 +55,7 @@ abstract class by_course extends base {
         foreach ($courses as $course) {
             // Skip the frontpage course.
             $analysable = \core_analytics\course::instance($course);
-            $analysables[] = $analysable;
+            $analysables[$analysable->get_id()] = $analysable;
         }
 
         if (empty($analysables)) {
index 2a21359..075b930 100644 (file)
@@ -42,6 +42,6 @@ abstract class sitewide extends base {
      */
     public function get_analysables() {
         $analysable = new \core_analytics\site();
-        return array($analysable);
+        return array(SYSCONTEXTID => $analysable);
     }
 }
index 5b741d2..3e84120 100644 (file)
@@ -261,7 +261,7 @@ abstract class base extends \core_analytics\calculable {
     }
 
     /**
-     * Should the model callback be triggered?
+     * This method determines if a prediction is interesing for the model or not.
      *
      * @param mixed $predictedvalue
      * @param float $predictionscore
index cbd8fe0..26044c7 100644 (file)
@@ -152,7 +152,10 @@ abstract class discrete extends base {
     }
 
     /**
-     * Should the model callback be triggered?
+     * This method determines if a prediction is interesing for the model or not.
+     *
+     * This method internally calls ignored_predicted_classes to skip classes
+     * flagged by the target as not important for users.
      *
      * @param mixed $predictedvalue
      * @param float $predictionscore
index d16ad96..f10d843 100644 (file)
@@ -84,7 +84,7 @@ abstract class linear extends base {
     }
 
     /**
-     * Should the model callback be triggered?
+     * This method determines if a prediction is interesing for the model or not.
      *
      * @param mixed $predictedvalue
      * @param float $predictionscore
index 3f116f3..0fdbb3a 100644 (file)
@@ -469,6 +469,13 @@ class manager {
             '\mod_wiki\analytics\indicator\social_breadth',
             '\mod_workshop\analytics\indicator\cognitive_depth',
             '\mod_workshop\analytics\indicator\social_breadth',
+            '\core_course\analytics\indicator\completion_enabled',
+            '\core_course\analytics\indicator\potential_cognitive_depth',
+            '\core_course\analytics\indicator\potential_social_breadth',
+            '\core\analytics\indicator\any_access_after_end',
+            '\core\analytics\indicator\any_access_before_start',
+            '\core\analytics\indicator\any_write_action',
+            '\core\analytics\indicator\read_actions',
         );
         $indicators = array();
         foreach ($coiindicators as $coiindicator) {
@@ -483,7 +490,8 @@ class manager {
         $target = self::get_target('\core\analytics\target\no_teaching');
         $timesplittingmethod = '\core\analytics\time_splitting\single_range';
         $noteacher = self::get_indicator('\core_course\analytics\indicator\no_teacher');
-        $indicators = array($noteacher->get_id() => $noteacher);
+        $nostudent = self::get_indicator('\core_course\analytics\indicator\no_student');
+        $indicators = array($noteacher->get_id() => $noteacher, $nostudent->get_id() => $nostudent);
         if (!\core_analytics\model::exists($target, $indicators)) {
             \core_analytics\model::create($target, $indicators, $timesplittingmethod);
         }
index 4bf9ea1..d881645 100644 (file)
@@ -53,12 +53,12 @@ class model {
     /**
      * Model with low prediction accuracy.
      */
-    const EVALUATE_LOW_SCORE = 4;
+    const LOW_SCORE = 4;
 
     /**
      * Not enough data to evaluate the model properly.
      */
-    const EVALUATE_NOT_ENOUGH_DATA = 8;
+    const NOT_ENOUGH_DATA = 8;
 
     /**
      * Invalid analysable for the time splitting method.
@@ -437,7 +437,7 @@ class model {
                 $this->model->indicators !== $indicatorsstr) {
 
             // Delete generated predictions before changing the model version.
-            $this->clear_model();
+            $this->clear();
 
             // It needs to be reset as the version changes.
             $this->uniqueid = null;
@@ -474,9 +474,9 @@ class model {
 
         \core_analytics\manager::check_can_manage_models();
 
-        $this->clear_model();
+        $this->clear();
 
-        // Method self::clear_model is already clearing the current model version.
+        // Method self::clear is already clearing the current model version.
         $predictor = \core_analytics\manager::get_predictions_processor();
         $predictor->delete_output_dir($this->get_output_dir(array(), true));
 
@@ -633,6 +633,10 @@ class model {
         $result->status = $predictorresult->status;
         $result->info = $predictorresult->info;
 
+        if ($result->status !== self::OK) {
+            return $result;
+        }
+
         $this->flag_file_as_used($samplesfile, 'trained');
 
         // Mark the model as trained if it wasn't.
@@ -689,7 +693,7 @@ class model {
         $samplesfile = $samplesdata[$this->model->timesplitting];
 
         // We need to throw an exception if we are trying to predict stuff that was already predicted.
-        $params = array('modelid' => $this->model->id, 'fileid' => $samplesfile->get_id(), 'action' => 'predicted');
+        $params = array('modelid' => $this->model->id, 'action' => 'predicted', 'fileid' => $samplesfile->get_id());
         if ($predicted = $DB->get_record('analytics_used_files', $params)) {
             throw new \moodle_exception('erroralreadypredict', 'analytics', '', $samplesfile->get_id());
         }
@@ -717,6 +721,10 @@ class model {
             $result->predictions = $this->format_predictor_predictions($predictorresult);
         }
 
+        if ($result->status !== self::OK) {
+            return $result;
+        }
+
         if ($result->predictions) {
             $samplecontexts = $this->execute_prediction_callbacks($result->predictions, $indicatorcalculations);
         }
@@ -739,7 +747,7 @@ class model {
     private function format_predictor_predictions($predictorresult) {
 
         $predictions = array();
-        if ($predictorresult->predictions) {
+        if (!empty($predictorresult->predictions)) {
             foreach ($predictorresult->predictions as $sampleinfo) {
 
                 // We parse each prediction.
@@ -780,15 +788,16 @@ class model {
 
         // Here we will store all predictions' contexts, this will be used to limit which users will see those predictions.
         $samplecontexts = array();
+        $records = array();
 
         foreach ($predictions as $uniquesampleid => $prediction) {
 
-            if ($this->get_target()->triggers_callback($prediction->prediction, $prediction->predictionscore)) {
+            // The unique sample id contains both the sampleid and the rangeindex.
+            list($sampleid, $rangeindex) = $this->get_time_splitting()->infer_sample_info($uniquesampleid);
 
-                // The unique sample id contains both the sampleid and the rangeindex.
-                list($sampleid, $rangeindex) = $this->get_time_splitting()->infer_sample_info($uniquesampleid);
+            if ($this->get_target()->triggers_callback($prediction->prediction, $prediction->predictionscore)) {
 
-                // Store the predicted values.
+                // Prepare the record to store the predicted values.
                 list($record, $samplecontext) = $this->prepare_prediction_record($sampleid, $rangeindex, $prediction->prediction,
                     $prediction->predictionscore, json_encode($indicatorcalculations[$uniquesampleid]));
 
@@ -803,7 +812,9 @@ class model {
             }
         }
 
-        $this->save_predictions($records);
+        if (!empty($records)) {
+            $this->save_predictions($records);
+        }
 
         return $samplecontexts;
     }
@@ -932,8 +943,6 @@ class model {
      * @return \context
      */
     protected function prepare_prediction_record($sampleid, $rangeindex, $prediction, $predictionscore, $calculations) {
-        global $DB;
-
         $context = $this->get_analyser()->sample_access_context($sampleid);
 
         $record = new \stdClass();
@@ -946,6 +955,15 @@ class model {
         $record->calculations = $calculations;
         $record->timecreated = time();
 
+        $analysable = $this->get_analyser()->get_sample_analysable($sampleid);
+        $timesplitting = $this->get_time_splitting();
+        $timesplitting->set_analysable($analysable);
+        $range = $timesplitting->get_range_by_index($rangeindex);
+        if ($range) {
+            $record->timestart = $range['start'];
+            $record->timeend = $range['end'];
+        }
+
         return array($record, $context);
     }
 
@@ -966,7 +984,7 @@ class model {
      * @return void
      */
     public function enable($timesplittingid = false) {
-        global $DB;
+        global $DB, $USER;
 
         \core_analytics\manager::check_can_manage_models();
 
@@ -983,7 +1001,7 @@ class model {
             }
 
             // Delete generated predictions before changing the model version.
-            $this->clear_model();
+            $this->clear();
 
             // It needs to be reset as the version changes.
             $this->uniqueid = null;
@@ -1004,6 +1022,7 @@ class model {
 
         $this->model->enabled = 1;
         $this->model->timemodified = $now;
+        $this->model->usermodified = $USER->id;
 
         // We don't always update timemodified intentionally as we reserve it for target, indicators or timesplitting updates.
         $DB->update_record('analytics_models', $this->model);
@@ -1261,7 +1280,7 @@ class model {
             $outputdir = rtrim($CFG->dataroot, '/') . DIRECTORY_SEPARATOR . 'models';
         }
 
-        // Append model id
+        // Append model id.
         $outputdir .= DIRECTORY_SEPARATOR . $this->model->id;
         if (!$onlymodelid) {
             // Append version + subdirs.
@@ -1428,8 +1447,10 @@ class model {
      *
      * @return void
      */
-    private function clear_model() {
-        global $DB;
+    public function clear() {
+        global $DB, $USER;
+
+        \core_analytics\manager::check_can_manage_models();
 
         // Delete current model version stored stuff.
         $predictor = \core_analytics\manager::get_predictions_processor();
@@ -1446,6 +1467,7 @@ class model {
         $DB->delete_records('analytics_predict_samples', array('modelid' => $this->model->id));
         $DB->delete_records('analytics_train_samples', array('modelid' => $this->model->id));
         $DB->delete_records('analytics_used_files', array('modelid' => $this->model->id));
+        $DB->delete_records('analytics_used_analysables', array('modelid' => $this->model->id));
 
         // Purge all generated files.
         \core_analytics\dataset_manager::clear_model_files($this->model->id);
@@ -1453,6 +1475,11 @@ class model {
         // We don't expect people to clear models regularly and the cost of filling the cache is
         // 1 db read per context.
         $this->purge_insights_cache();
+
+        $this->model->trained = 0;
+        $this->model->timemodified = time();
+        $this->model->usermodified = $USER->id;
+        $DB->update_record('analytics_models', $this->model);
     }
 
     /**
index 69fe722..c503e8a 100644 (file)
@@ -54,6 +54,15 @@ class site implements \core_analytics\analysable {
         return SYSCONTEXTID;
     }
 
+    /**
+     * Site.
+     *
+     * @return string
+     */
+    public function get_name() {
+        return get_string('site');
+    }
+
     /**
      * Analysable context.
      *
index d36baf5..3856c5f 100644 (file)
@@ -34,22 +34,30 @@ defined('MOODLE_INTERNAL') || die();
 class dataset_manager_testcase extends advanced_testcase {
 
     /**
-     * test_create_dataset
+     * setUp
      *
-     * @return
+     * @return null
      */
-    public function test_create_dataset() {
+    public function setUp() {
         $this->resetAfterTest(true);
 
-        $sharedtoprows = array(
+        $this->sharedtoprows = array(
             array('var1', 'var2'),
             array('value1', 'value2'),
             array('header1', 'header2')
         );
+    }
+
+    /**
+     * test_create_dataset
+     *
+     * @return null
+     */
+    public function test_create_dataset() {
 
         $dataset1 = new \core_analytics\dataset_manager(1, 1, 'whatever', \core_analytics\dataset_manager::LABELLED_FILEAREA, false);
         $dataset1->init_process();
-        $dataset1data = array_merge($sharedtoprows, array(array('yeah', 'yeah', 'yeah')));
+        $dataset1data = array_merge($this->sharedtoprows, array(array('yeah', 'yeah', 'yeah')));
         $f1 = $dataset1->store($dataset1data);
         $dataset1->close_process();
 
@@ -63,26 +71,19 @@ class dataset_manager_testcase extends advanced_testcase {
     /**
      * test_merge_datasets
      *
-     * @return
+     * @return null
      */
     public function test_merge_datasets() {
-        $this->resetAfterTest(true);
-
-        $sharedtoprows = array(
-            array('var1', 'var2'),
-            array('value1', 'value2'),
-            array('header1', 'header2')
-        );
 
         $dataset1 = new \core_analytics\dataset_manager(1, 1, 'whatever', \core_analytics\dataset_manager::LABELLED_FILEAREA, false);
         $dataset1->init_process();
-        $dataset1data = array_merge($sharedtoprows, array(array('yeah', 'yeah', 'yeah')));
+        $dataset1data = array_merge($this->sharedtoprows, array(array('yeah', 'yeah', 'yeah')));
         $f1 = $dataset1->store($dataset1data);
         $dataset1->close_process();
 
         $dataset2 = new \core_analytics\dataset_manager(1, 2, 'whatever', \core_analytics\dataset_manager::LABELLED_FILEAREA, false);
         $dataset2->init_process();
-        $dataset2data = array_merge($sharedtoprows, array(array('no', 'no', 'no')));
+        $dataset2data = array_merge($this->sharedtoprows, array(array('no', 'no', 'no')));
         $f2 = $dataset2->store($dataset2data);
         $dataset2->close_process();
 
@@ -97,4