Compare commits

..

123 Commits

Author SHA1 Message Date
Sara Arjona 52bd62f2be Moodle release 3.4.8 2019-03-08 11:03:07 +01:00
Sara Arjona 53392df219 Merge branch 'install_34_STABLE' of https://git.in.moodle.com/amosbot/moodle-install into MOODLE_34_STABLE 2019-03-08 11:03:06 +01:00
AMOS bot 344386f873 Automatically generated installer lang files 2019-03-08 00:15:41 +00:00
Andrew Nicols 8a2e9542c9 MDL-64971 access: Ensure that the capability exists when fetching 2019-03-06 10:18:03 +08:00
AMOS bot 1f01b678df Automatically generated installer lang files 2019-03-06 00:09:48 +00:00
Jun Pataleta 90740716be Merge branch 'MDL-64799_34' of https://github.com/timhunt/moodle into MOODLE_34_STABLE 2019-03-05 15:52:15 +08:00
Shamim Rezaie 2cebb87682 MDL-64830 core_calendar: Do cap check on event edit form 2019-03-05 04:17:42 +01:00
Damyon Wiese 907b377e51 MDL-64651 comments: Do not send referrer
Use blanktarget option on all comments to prevent malicious links.
2019-03-05 04:17:42 +01:00
Viktor b77dcd23d8 MDL-62702 core_lib: fix broken verification signature 2019-03-05 04:17:42 +01:00
Mark Nelson 1a3612bb5b MDL-62702 core_lib: updated the 'ltiprovider' readme_moodle.txt 2019-03-05 04:17:42 +01:00
Brendan Cox fba7dcd90a MDL-62702 core_lib: parameters in POST override other LTI values
This fixes an exploit where you could alter the 'Content-Type' of the
request and bypass the case-sensitive check 'strstr'. For example,
setting it to 'application/x-www-FORM-urlencoded'. However, changing
this to use 'stristr' was also not an acceptable approach as you
could also bypass it by setting the value to 'multipart/form-data'.
2019-03-05 04:17:42 +01:00
Ryan Wyllie e836242e1c MDL-63786 core_my: prevent dashboard access when logged in as
Stop a "logged in as" session from accessing the user's dashboard
to prevent user JavaScript from being executed in the logged in
as session
2019-03-05 04:17:42 +01:00
Jun Pataleta 6f05d22622 Merge branch 'MDL-64652-34' of git://github.com/andrewnicols/moodle into MOODLE_34_STABLE 2019-03-05 11:13:03 +08:00
Andrew Nicols 141271b726 MDL-64652 mod_data: Break LEFT JOINs to multiple INNER 2019-03-05 10:32:22 +08:00
Andrew Nicols aae3a2fdd4 MDL-64652 glossary: Break LEFT JOINs to multiple INNER 2019-03-05 10:32:22 +08:00
Andrew Nicols abd634d64c MDL-64652 forum: Use an inner join when fetching ratings 2019-03-05 10:32:22 +08:00
Andrew Nicols e24f8994b5 MDL-64652 rating: Add optional inner join option to sql 2019-03-05 10:32:20 +08:00
Andrew Nicols d16e730116 MDL-64652 mod_forum: Performance improvement for provider
There are possibly some further improvements that can be made to:
- export_discussion_data
- export_all_posts
2019-03-05 10:31:51 +08:00
AMOS bot 3cc11668c8 Automatically generated installer lang files 2019-03-04 00:09:59 +00:00
AMOS bot 3c72d44b18 Automatically generated installer lang files 2019-03-01 00:16:04 +00:00
AMOS bot 4546269170 Automatically generated installer lang files 2019-02-27 00:09:59 +00:00
Tim Hunt ad4dfb1e8c MDL-64799 qtype_random: orphaned random questions should be deleted
This is a back-port of MDL-63260

This fix is based on some original code written by Bo Pierce.
2019-02-26 15:59:05 +00:00
AMOS bot 4f6373bf05 Automatically generated installer lang files 2019-02-16 00:14:36 +00:00
Andrew Nicols 31c05704b8 weekly release 3.4.7+ 2019-02-15 08:29:19 +08:00
Andrew Nicols 384c4c29db Merge branch 'install_34_STABLE' of https://git.in.moodle.com/amosbot/moodle-install into MOODLE_34_STABLE 2019-02-15 08:29:17 +08:00
Jun Pataleta 2282d6932b Merge branch 'MDL-63892-34-rework' of git://github.com/mickhawkins/moodle into MOODLE_34_STABLE 2019-02-13 21:00:24 +08:00
Michael Hawkins 47265ccf62 MDL-63892 mod_forum: Behat update for post order 2019-02-13 17:02:29 +08:00
Jun Pataleta 2d0d9a4748 Merge branch 'MDL-64835-34' of git://github.com/andrewnicols/moodle into MOODLE_34_STABLE 2019-02-13 16:26:51 +08:00
Andrew Nicols 0cbb46b944 Merge branch 'MDL-63892-34-rework' of git://github.com/mickhawkins/moodle into MOODLE_34_STABLE 2019-02-13 10:20:13 +08:00
Andrew Nicols 3dcf5f9930 MDL-64835 JS: Reduce cache invalidation checks
The cache invalidation check was previously occuring every time either
the `set` or `get` function was called on the cache. However, the cache
invalidation check is based on the jsrev which is static for the
lifetime of the page.

This change moved the invalidation to happen during the setup of the AMD
module such that it only happens one time per storage type (Local +
Session).
2019-02-13 10:14:33 +08:00
Andrew Nicols 3456505930 MDL-64835 JS: Stop using the jsrev in the jsrevPrefix
We only use the jsrevPrefix to determine if the cache should be
invalidated, but the prefix that we were using is based on the new
jsrev.

For example, the jsrevPrefix will be:

    hash(wwwroot + '/ + config.jsrev) + '/jsrev'

Where config.jsrev is the _current_ (new) jsrev.

As a result when searching for the jsrev used to store the data which is
currently in the storage cache, no key is returned, and we instead set
an 'initial' value and the cache is not cleared

This patch changes the jsrevPrefix to be:

    hash(wwwroot) + '/jsrev'

Since the wwwroot does not change, the key remains static for the
current site. As a result, when the jsrev is bumped via a Moodle cache
purge, we are able to correctly fetch the old jsrev from the cache,
determine that the jsrev has changed, and purge the cache.
2019-02-13 10:14:33 +08:00
AMOS bot 18c80ae8a5 Automatically generated installer lang files 2019-02-13 00:12:01 +00:00
Michael Hawkins 4fccbafa46 MDL-63892 mod_forum: Updating lib unit test 2019-02-12 17:28:23 +08:00
Michael Hawkins 11fc8b3d02 MDL-63892 mod_forum: Fix last post details in forum view 2019-02-12 17:28:17 +08:00
Michael Hawkins 4e190d7eae MDL-63892 mod_forum: Correction to timestamp added to edit strings 2019-02-12 17:28:07 +08:00
Eloy Lafuente (stronk7) acd27d90c8 Merge branch 'MDL-64725-34' of git://github.com/junpataleta/moodle into MOODLE_34_STABLE 2019-02-11 17:06:15 +01:00
Jun Pataleta d81447751e Merge branch 'MDL-64644_34' of git://github.com/markn86/moodle into MOODLE_34_STABLE 2019-02-11 16:46:21 +08:00
Andrew Nicols eb3df7ecad Merge branch 'MDL-64597-34' of git://github.com/junpataleta/moodle into MOODLE_34_STABLE 2019-02-11 16:34:30 +08:00
Mark Nelson a9d153e61d MDL-64644 completion: fixed assertions in privacy test 2019-02-11 14:11:05 +08:00
Mark Nelson c2c8ce4b6d MDL-64644 completion: set status correctly in provider 2019-02-11 14:11:05 +08:00
Mark Nelson 27f608a5c9 MDL-64644 completion: ensure we return array for provider 2019-02-11 14:11:04 +08:00
AMOS bot 181cf34341 Automatically generated installer lang files 2019-02-10 00:10:33 +00:00
Jun Pataleta 56c15f5c99 MDL-64725 composer: Use lowercase for package name of dev dependencies 2019-02-04 09:07:48 +08:00
Jun Pataleta 87711249c3 weekly release 3.4.7+ 2019-02-01 08:57:05 +08:00
Jun Pataleta 0a03b2fd63 Merge branch 'install_34_STABLE' of https://git.in.moodle.com/amosbot/moodle-install into MOODLE_34_STABLE 2019-02-01 08:57:04 +08:00
Eloy Lafuente (stronk7) 398c4c107d Merge branch 'MDL-64737-34-attoprivacysql' of git://github.com/mudrd8mz/moodle into MOODLE_34_STABLE 2019-01-31 22:47:07 +01:00
David Mudrák 5789961754 MDL-64737 editor_atto: Fix the privacy export_user_data() implementation
Multiple issues fixed:

* Fixed order of the statements to avoid access to undefined variables.
* Fixed population of the SQL query parameter to make sure they match
  the placeholder in the query.
* Fixed missing table aliases in the second query to make sure the query
  actually works as expected.
2019-01-31 12:51:19 +01:00
David Mudrák 587a8a9732 MDL-64737 privacy: Fix privacy provider testsuite file names
Only files ending with "_test.php" suffix are implicit part of a
testsuite. So these two were not actually executed.
2019-01-31 12:51:19 +01:00
AMOS bot 53d0efba1f Automatically generated installer lang files 2019-01-30 00:10:32 +00:00
Eloy Lafuente (stronk7) 16045c4846 Merge branch 'MDL-64424-34-expiredfrontpage' of git://github.com/mudrd8mz/moodle into MOODLE_34_STABLE 2019-01-29 23:48:55 +01:00
Jun Pataleta 57d2a28425 MDL-64597 behat: Properly import behat_util when dropping parallel sites 2019-01-24 12:51:35 +08:00
David Mudrák 33cbe249dd MDL-64424 privacy: Use system context purpose on the front page, too
For activities on the site's front page, use the purpose and retention
periods as defined for the system context, not for the courses.
2019-01-23 11:09:02 +01:00
AMOS bot 82e23b9443 Automatically generated installer lang files 2019-01-22 00:10:14 +00:00
Eloy Lafuente (stronk7) ea1d1cf9e1 Moodle release 3.4.7 2019-01-12 13:25:52 +01:00
Sara Arjona 7bd7754ad0 weekly release 3.4.6+ 2019-01-10 17:51:42 +01:00
Sara Arjona 734ce33b99 Merge branch 'install_34_STABLE' of https://git.in.moodle.com/amosbot/moodle-install into MOODLE_34_STABLE 2019-01-10 17:51:42 +01:00
AMOS bot 4f19471b9e Automatically generated installer lang files 2019-01-10 00:08:57 +00:00
Eloy Lafuente (stronk7) 78275d44f0 Merge branch 'MDL-64285-34' of git://github.com/junpataleta/moodle into MOODLE_34_STABLE 2019-01-09 00:03:59 +01:00
Michael Hawkins cfde0b8d38 MDL-64395 groups: Added XSS Risk to the manage groups capability 2019-01-08 00:07:45 +01:00
Michael Hawkins 74746ce412 MDL-64372 userpix: Escape fullname string on userpix index page 2019-01-08 00:07:45 +01:00
Sara Arjona 5dd590a3bd MDL-64222 badge: Removed backpackurl from the edit_form 2019-01-08 00:07:45 +01:00
Andrew Nicols 7b97be9f25 Merge branch 'MDL-63276_34' of https://github.com/stronk7/moodle into MOODLE_34_STABLE 2019-01-08 06:56:50 +08:00
Mathew aaf4b2b489 MDL-49651 mod_forum: Respect course visibility in cron
This checks the courses' current visibility and the users permissions.
If they have the permission to view the course when it is hidden they'll get messages.

We already have checks for the users enrolled & subscription status.
2019-01-03 10:50:39 +08:00
Eloy Lafuente (stronk7) 5b1d246758 MDL-63276 environment: PHP 7.1 and Moodle 3.2 required 2019-01-03 01:19:15 +01:00
Eloy Lafuente (stronk7) 3aef542a35 MDL-63276 environment: 3.7 base information
This is a direct copy of the 3.6 branch to
have changes under control easier in next commits.
2019-01-03 01:19:15 +01:00
AMOS bot 07b8bb1ae7 Automatically generated installer lang files 2018-12-19 00:09:24 +00:00
Jun Pataleta 18c55facc0 MDL-64285 core: Fix environment check test
* Looping the environment results and skipping some items mean that
if a test gets skipped, the rest of the environment results won't be
tested. It's better to use a data provider for this in order to
individually test each environment result.
2018-12-13 16:46:53 +08:00
AMOS bot e8a62dc4c2 Automatically generated installer lang files 2018-12-07 00:09:43 +00:00
Eloy Lafuente (stronk7) b63c41d762 weekly release 3.4.6+ 2018-11-30 18:02:05 +01:00
Eloy Lafuente (stronk7) 1aa56ceaca Merge branch 'install_34_STABLE' of https://git.in.moodle.com/amosbot/moodle-install into MOODLE_34_STABLE 2018-11-30 18:02:03 +01:00
Jun Pataleta fdf3d59a00 Merge branch 'MDL-64214-34' of git://github.com/junpataleta/moodle into MOODLE_34_STABLE 2018-11-30 09:43:50 +08:00
Jun Pataleta f648211569 MDL-64214 calendar: Populate the groups select box on page load 2018-11-30 09:26:41 +08:00
Simey Lameze f8de60ce57 MDL-64214 calendar: fix failing unit tests 2018-11-30 09:26:41 +08:00
Simey Lameze 48b2284317 MDL-64214 core_calendar: fix event types form element handling 2018-11-30 09:26:41 +08:00
AMOS bot 1720738ca9 Automatically generated installer lang files 2018-11-30 00:07:50 +00:00
AMOS bot f6d48eeb87 Automatically generated installer lang files 2018-11-29 00:08:36 +00:00
David Monllao 66774319b3 weekly release 3.4.6+ 2018-11-28 11:23:17 +01:00
Eloy Lafuente (stronk7) c3238ac39b Merge branch 'MDL-64220-34' of git://github.com/damyon/moodle into MOODLE_34_STABLE 2018-11-27 16:44:38 +01:00
David Monllao a922416edb Merge branch 'MDL-64205-34-sarunconfirmeduser' of git://github.com/mudrd8mz/moodle into MOODLE_34_STABLE 2018-11-27 08:10:29 +01:00
Damyon Wiese fef96d285e MDL-64220 privacy: Clean path params
We need to consistently clean the path params before we use them. This
shows a difference on windows where dir separators are different.
2018-11-27 15:02:54 +08:00
Jun Pataleta bd38b68684 Merge branch 'MDL-64181-34-fix' of git://github.com/andrewnicols/moodle into MOODLE_34_STABLE 2018-11-27 14:43:27 +08:00
Andrew Nicols 8a5095d453 MDL-64181 core: Ensure that self is included in parentage 2018-11-27 14:42:52 +08:00
Jun Pataleta c9cd95223a Merge branch 'MDL-64181-34' of git://github.com/damyon/moodle into MOODLE_34_STABLE 2018-11-27 11:44:46 +08:00
Damyon Wiese 80221982fa MDL-64181 javascript: Recursive template fix
This change prevents template rendering from waiting for itself, but does
allow it to wait for another async call to render.
2018-11-27 08:43:28 +08:00
David Mudrák 03edaa4290 MDL-64205 privacy: Delete orphaned data request records on upgrade
As a result of the bug, some requests can be orphaned - the linked user
record may not exist any more. This is a cleanup of such records.
2018-11-26 14:18:31 +01:00
David Mudrák 0401ae362c MDL-64205 users: Do not delete the actual records of unconfirmed users
There is an automatic data privacy request created to get rid of all the
personal data upon the user deletion. With the actual user record
absent, the data privacy requests page throws an error. Let's do here
same as we do in `delete_incomplete_users_task` and let us not delete
the actual user record.
2018-11-25 07:27:10 +01:00
Eloy Lafuente (stronk7) f5fcf31947 weekly release 3.4.6+ 2018-11-23 16:36:43 +01:00
Eloy Lafuente (stronk7) e5e3cb4577 Merge branch 'install_34_STABLE' of https://git.in.moodle.com/amosbot/moodle-install into MOODLE_34_STABLE 2018-11-23 16:36:41 +01:00
Jun Pataleta 999c187559 Merge branch 'MDL-64030-34' of git://github.com/andrewnicols/moodle into MOODLE_34_STABLE 2018-11-23 11:15:20 +08:00
AMOS bot b5fa0b3893 Automatically generated installer lang files 2018-11-23 00:07:40 +00:00
Andrew Nicols 9e75d8d7fc MDL-64030 tool_dataprivacy: Process requests when system purpose not set 2018-11-23 07:55:28 +08:00
Jun Pataleta 0f908de1e3 Merge branch 'MDL-64119-34' of git://github.com/sarjona/moodle into MOODLE_34_STABLE 2018-11-22 20:26:10 +08:00
Sara Arjona bbd79a469a MDL-64119 output: Use context of renderer_base's page object 2018-11-22 12:44:44 +01:00
Jun Pataleta a67ed33ed3 Merge branch 'MDL-64119-34' of git://github.com/sarjona/moodle into MOODLE_34_STABLE 2018-11-22 09:51:45 +08:00
Sara Arjona f757055b9f MDL-64119 user: Display full names consistently for teachers
Fix an error with users with the moodle/site:viewfullnames capability
in a course context (like teachers), to display properly the full
names in pages like the participants one.
2018-11-20 19:48:52 +01:00
David Monllao 1b1465210b weekly release 3.4.6+ 2018-11-20 12:55:10 +01:00
Jun Pataleta f06b51262f Merge branch 'MDL-63994-34-castoken' of git://github.com/mudrd8mz/moodle into MOODLE_34_STABLE 2018-11-19 15:45:18 +08:00
Eloy Lafuente (stronk7) e4a6f46527 weekly release 3.4.6+ 2018-11-16 12:03:05 +01:00
Jun Pataleta ec12901c8c Merge branch 'MDL-63660-34' of git://github.com/junpataleta/moodle into MOODLE_34_STABLE 2018-11-16 11:06:37 +08:00
Jun Pataleta b871a7a7e1 MDL-63660 tool_dataprivacy: Fix illegal whitespaces 2018-11-16 11:04:22 +08:00
Jun Pataleta 876302149f Merge branch 'MDL-63660-34' of git://github.com/junpataleta/moodle into MOODLE_34_STABLE 2018-11-16 10:52:45 +08:00
Jun Pataleta 1164fa8370 MDL-63660 tool_dataprivacy: Increase expected export file size 2018-11-16 10:34:47 +08:00
Eloy Lafuente (stronk7) e087069258 Merge branch 'MDL-64012_34' of https://github.com/timhunt/moodle into MOODLE_34_STABLE 2018-11-16 01:03:56 +01:00
David Mudrák 8275b2d502 MDL-63994 auth_cas: Include the login token in the simulated login form
The CAS login process relies on the standard authenticate_user_login()
call to set up the user. So we need to inject the login token to pass
the validation.
2018-11-15 21:26:24 +01:00
David Mudrák c49c373209 MDL-63994 login: Improve the logintoken param input
The logintoken is supposed to arrive as a part of the login form ($frm)
together with the username and password. So it should be handled the
same way - including the opportunity for the auth plugins to provide the
form data via the loginpage_hook().

This also implies that only logintoken coming as a part of the POST
request are taken into account, which is a good thing and another thin
layer in this security mechanism.
2018-11-15 21:26:24 +01:00
David Mudrák 2e80552524 MDL-63994 login: Clarify inline comments on loginpage_hook() usage
While working on the issue, I found these inline comments outdated and
confusing. The auth plugin types can implement a loginpage_hook()
method, the purpose of which is to inject the $user and/or $frm into
this login/index.php script. The new comments should make it more clear.

Note the second comment mentioned a prelogin_hook() which was an old
name of what is now called loginpage_hook(). It had nothing to do with
the existing pre_loginpage_hook() and was only confusing.
2018-11-15 21:26:23 +01:00
Mark Johnson d9cab9de67 MDL-63660 privacy: Include CSS locally for data export 2018-11-14 16:12:38 +00:00
Tim Hunt 6546fd1043 MDL-64012 filters: activity name autolinking fails for names with () 2018-11-14 16:07:54 +00:00
Eloy Lafuente (stronk7) 4c39ccf8a3 Merge branch 'MDL-63842-34' of git://github.com/andrewnicols/moodle into MOODLE_34_STABLE 2018-11-14 16:12:13 +01:00
David Monllao 270ba66408 weekly release 3.4.6+ 2018-11-13 13:10:53 +01:00
David Monllao 8fdf8a4ed5 Merge branch 'install_34_STABLE' of https://git.in.moodle.com/amosbot/moodle-install into MOODLE_34_STABLE 2018-11-13 13:10:51 +01:00
Damyon Wiese 9d7e968130 Merge branch 'MDL-63927_34' of git://github.com/stronk7/moodle into MOODLE_34_STABLE 2018-11-13 11:58:33 +08:00
AMOS bot 668e1a2ade Automatically generated installer lang files 2018-11-13 00:07:47 +00:00
Eloy Lafuente (stronk7) 8e3bb05807 Merge branch 'MDL-63907-34' of git://github.com/cescobedo/moodle into MOODLE_34_STABLE 2018-11-12 12:09:11 +01:00
David Monllao 6d67532717 Merge branch 'MDL-63925-34' of git://github.com/mickhawkins/moodle into MOODLE_34_STABLE 2018-11-12 11:58:00 +01:00
David Monllao 529d402ca6 Merge branch 'MDL-63940-34-recaptchaurl' of git://github.com/mudrd8mz/moodle into MOODLE_34_STABLE 2018-11-12 10:56:18 +01:00
David Monllao 6c9d494f35 Merge branch 'MDL-63823-34' of git://github.com/lameze/moodle into MOODLE_34_STABLE 2018-11-12 10:10:13 +01:00
Simey Lameze 327acfdadb MDL-63823 tool_lpimportcsv: supress progress bar debugging 2018-11-12 09:54:05 +08:00
cescobedo 3758b9c6ab MDL-63907 core_message: Pass user object to show_online_status.
Fix create_contact() to pass user object as parameter in show_online_status.
2018-11-09 10:02:18 +01:00
David Mudrák db1002a36a MDL-63940 recaptcha: Use www.recaptcha.net in the service endpoint URL
There are countries where www.google.com is a being blocked and
recaptcha does not work there. As shown at
https://developers.google.com/recaptcha/docs/faq#can-i-use-recaptcha-globally
it is enough to simply use www.recaptcha.net in the service endpoint
URL.
2018-11-08 22:59:38 +01:00
Eloy Lafuente (stronk7) e48cc26687 MDL-63927 performance: Update the plan to support the new login token 2018-11-08 10:27:16 +01:00
Michael Hawkins a5f813e039 MDL-63925 privacy: Fixed Moodle URL in template 2018-11-08 15:54:43 +08:00
Andrew Nicols 622be7e630 MDL-63842 core_user: Remove unnecessary DB fetch 2018-11-02 09:08:07 +08:00
94 changed files with 18642 additions and 333 deletions
+1
View File
@@ -2,6 +2,7 @@
theme/bootstrapbase/style/
theme/clean/style/custom.css
theme/more/style/custom.css
theme/boost/style/moodle.css
node_modules/
vendor/
admin/tool/policy/amd/src/jquery-eu-cookie-law-popup.js
+2 -1
View File
@@ -214,7 +214,8 @@ module.exports = function(grunt) {
'# Generated by "grunt ignorefiles"',
'theme/bootstrapbase/style/',
'theme/clean/style/custom.css',
'theme/more/style/custom.css'
'theme/more/style/custom.css',
'theme/boost/style/moodle.css'
].concat(thirdPartyPaths);
grunt.file.write('.stylelintignore', stylelintIgnores.join('\n'));
};
+181
View File
@@ -2618,4 +2618,185 @@
</CUSTOM_CHECK>
</CUSTOM_CHECKS>
</MOODLE>
<MOODLE version="3.7" requires="3.2">
<UNICODE level="required">
<FEEDBACK>
<ON_ERROR message="unicoderequired" />
</FEEDBACK>
</UNICODE>
<DATABASE level="required">
<VENDOR name="mariadb" version="5.5.31" />
<VENDOR name="mysql" version="5.6" />
<VENDOR name="postgres" version="9.4" />
<VENDOR name="mssql" version="10.0" />
<VENDOR name="oracle" version="11.2" />
</DATABASE>
<PHP version="7.1.0" level="required">
</PHP>
<PCREUNICODE level="optional">
<FEEDBACK>
<ON_CHECK message="pcreunicodewarning" />
</FEEDBACK>
</PCREUNICODE>
<PHP_EXTENSIONS>
<PHP_EXTENSION name="iconv" level="required">
<FEEDBACK>
<ON_ERROR message="iconvrequired" />
</FEEDBACK>
</PHP_EXTENSION>
<PHP_EXTENSION name="mbstring" level="optional">
<FEEDBACK>
<ON_CHECK message="mbstringrecommended" />
</FEEDBACK>
</PHP_EXTENSION>
<PHP_EXTENSION name="curl" level="required">
<FEEDBACK>
<ON_ERROR message="curlrequired" />
</FEEDBACK>
</PHP_EXTENSION>
<PHP_EXTENSION name="openssl" level="required">
<FEEDBACK>
<ON_ERROR message="opensslrequired" />
</FEEDBACK>
</PHP_EXTENSION>
<PHP_EXTENSION name="tokenizer" level="optional">
<FEEDBACK>
<ON_CHECK message="tokenizerrecommended" />
</FEEDBACK>
</PHP_EXTENSION>
<PHP_EXTENSION name="xmlrpc" level="optional">
<FEEDBACK>
<ON_CHECK message="xmlrpcrecommended" />
</FEEDBACK>
</PHP_EXTENSION>
<PHP_EXTENSION name="soap" level="optional">
<FEEDBACK>
<ON_CHECK message="soaprecommended" />
</FEEDBACK>
</PHP_EXTENSION>
<PHP_EXTENSION name="ctype" level="required">
<FEEDBACK>
<ON_ERROR message="ctyperequired" />
</FEEDBACK>
</PHP_EXTENSION>
<PHP_EXTENSION name="zip" level="required">
<FEEDBACK>
<ON_ERROR message="ziprequired" />
</FEEDBACK>
</PHP_EXTENSION>
<PHP_EXTENSION name="zlib" level="required">
</PHP_EXTENSION>
<PHP_EXTENSION name="gd" level="required">
<FEEDBACK>
<ON_ERROR message="gdrequired" />
</FEEDBACK>
</PHP_EXTENSION>
<PHP_EXTENSION name="simplexml" level="required">
<FEEDBACK>
<ON_ERROR message="simplexmlrequired" />
</FEEDBACK>
</PHP_EXTENSION>
<PHP_EXTENSION name="spl" level="required">
<FEEDBACK>
<ON_ERROR message="splrequired" />
</FEEDBACK>
</PHP_EXTENSION>
<PHP_EXTENSION name="pcre" level="required">
</PHP_EXTENSION>
<PHP_EXTENSION name="dom" level="required">
</PHP_EXTENSION>
<PHP_EXTENSION name="xml" level="required">
</PHP_EXTENSION>
<PHP_EXTENSION name="xmlreader" level="required">
</PHP_EXTENSION>
<PHP_EXTENSION name="intl" level="required">
<FEEDBACK>
<ON_ERROR message="intlrequired" />
</FEEDBACK>
</PHP_EXTENSION>
<PHP_EXTENSION name="json" level="required">
</PHP_EXTENSION>
<PHP_EXTENSION name="hash" level="required"/>
<PHP_EXTENSION name="fileinfo" level="required"/>
</PHP_EXTENSIONS>
<PHP_SETTINGS>
<PHP_SETTING name="memory_limit" value="96M" level="required">
<FEEDBACK>
<ON_ERROR message="settingmemorylimit" />
</FEEDBACK>
</PHP_SETTING>
<PHP_SETTING name="file_uploads" value="1" level="optional">
<FEEDBACK>
<ON_CHECK message="settingfileuploads" />
</FEEDBACK>
</PHP_SETTING>
<PHP_SETTING name="opcache.enable" value="1" level="optional">
<FEEDBACK>
<ON_CHECK message="opcacherecommended" />
</FEEDBACK>
</PHP_SETTING>
</PHP_SETTINGS>
<CUSTOM_CHECKS>
<CUSTOM_CHECK file="lib/upgradelib.php" function="check_database_storage_engine" level="required">
<FEEDBACK>
<ON_ERROR message="unsupporteddbstorageengine" />
</FEEDBACK>
</CUSTOM_CHECK>
<CUSTOM_CHECK file="question/engine/upgrade/upgradelib.php" function="quiz_attempts_upgraded" level="required">
<FEEDBACK>
<ON_ERROR message="quizattemptsupgradedmessage" />
</FEEDBACK>
</CUSTOM_CHECK>
<CUSTOM_CHECK file="lib/upgradelib.php" function="check_slasharguments" level="optional">
<FEEDBACK>
<ON_CHECK message="slashargumentswarning" />
</FEEDBACK>
</CUSTOM_CHECK>
<CUSTOM_CHECK file="lib/upgradelib.php" function="check_database_tables_row_format" level="optional">
<FEEDBACK>
<ON_CHECK message="unsupporteddbtablerowformat" />
</FEEDBACK>
</CUSTOM_CHECK>
<CUSTOM_CHECK file="lib/upgradelib.php" function="check_unoconv_version" level="optional">
<FEEDBACK>
<ON_CHECK message="unoconvwarning" />
</FEEDBACK>
</CUSTOM_CHECK>
<CUSTOM_CHECK file="lib/upgradelib.php" function="check_libcurl_version" level="optional">
<FEEDBACK>
<ON_CHECK message="libcurlwarning" />
</FEEDBACK>
</CUSTOM_CHECK>
<CUSTOM_CHECK file="lib/upgradelib.php" function="check_mysql_file_format" level="required">
<FEEDBACK>
<ON_ERROR message="unsupporteddbfileformat" />
</FEEDBACK>
</CUSTOM_CHECK>
<CUSTOM_CHECK file="lib/upgradelib.php" function="check_mysql_file_per_table" level="required">
<FEEDBACK>
<ON_ERROR message="unsupporteddbfilepertable" />
</FEEDBACK>
</CUSTOM_CHECK>
<CUSTOM_CHECK file="lib/upgradelib.php" function="check_mysql_large_prefix" level="required">
<FEEDBACK>
<ON_ERROR message="unsupporteddblargeprefix" />
</FEEDBACK>
</CUSTOM_CHECK>
<CUSTOM_CHECK file="lib/upgradelib.php" function="check_is_https" level="optional">
<FEEDBACK>
<ON_CHECK message="ishttpswarning" />
</FEEDBACK>
</CUSTOM_CHECK>
<CUSTOM_CHECK file="lib/upgradelib.php" function="check_mysql_incomplete_unicode_support" level="optional">
<FEEDBACK>
<ON_CHECK message="incompleteunicodesupport" />
</FEEDBACK>
</CUSTOM_CHECK>
<CUSTOM_CHECK file="lib/upgradelib.php" function="check_sixtyfour_bits" level="optional">
<FEEDBACK>
<ON_CHECK message="sixtyfourbitswarning" />
</FEEDBACK>
</CUSTOM_CHECK>
</CUSTOM_CHECKS>
</MOODLE>
</COMPATIBILITY_MATRIX>
+11
View File
@@ -109,6 +109,17 @@ require_once(__DIR__ . '/../../../../lib/behat/lib.php');
require_once(__DIR__ . '/../../../../lib/behat/classes/behat_command.php');
require_once(__DIR__ . '/../../../../lib/behat/classes/behat_config_manager.php');
// Remove error handling overrides done in config.php. This is consistent with admin/tool/behat/cli/util_single_run.php.
$CFG->debug = (E_ALL | E_STRICT);
$CFG->debugdisplay = 1;
error_reporting($CFG->debug);
ini_set('display_errors', '1');
ini_set('log_errors', '1');
// Import the necessary libraries.
require_once($CFG->libdir . '/setuplib.php');
require_once($CFG->libdir . '/behat/classes/util.php');
// For drop option check if parallel site.
if ((empty($options['parallel'])) && ($options['drop']) || $options['updatesteps']) {
$options['parallel'] = behat_config_manager::get_behat_run_config_value('parallel');
+12 -3
View File
@@ -1075,6 +1075,7 @@ class api {
public static function add_request_contexts_with_status(contextlist_collection $clcollection, int $requestid, int $status) {
$request = new data_request($requestid);
$user = \core_user::get_user($request->get('userid'));
$isconfigured = data_registry::defaults_set();
foreach ($clcollection as $contextlist) {
// Convert the \core_privacy\local\request\contextlist into a contextlist persistent and store it.
$clp = \tool_dataprivacy\contextlist::from_contextlist($contextlist);
@@ -1083,7 +1084,11 @@ class api {
// Store the associated contexts in the contextlist.
foreach ($contextlist->get_contextids() as $contextid) {
if ($request->get('type') == static::DATAREQUEST_TYPE_DELETE) {
if ($isconfigured && self::DATAREQUEST_TYPE_DELETE == $request->get('type')) {
// Data can only be deleted from it if the context is either expired, or unprotected.
// Note: We can only check whether a context is expired or unprotected if the site is configured and
// defaults are set appropriately. If they are not, we treat all contexts as though they are
// unprotected.
$context = \context::instance_by_id($contextid);
$purpose = static::get_effective_context_purpose($context);
@@ -1163,10 +1168,11 @@ class api {
* @return contextlist_collection the collection of approved_contextlist objects.
*/
public static function get_approved_contextlist_collection_for_request(data_request $request) : contextlist_collection {
global $DB;
$foruser = core_user::get_user($request->get('userid'));
$isconfigured = data_registry::defaults_set();
// Fetch all approved contextlists and create the core_privacy\local\request\contextlist objects here.
global $DB;
$sql = "SELECT cl.component, ctx.contextid
FROM {" . request_contextlist::TABLE . "} rcl
JOIN {" . contextlist::TABLE . "} cl ON rcl.contextlistid = cl.id
@@ -1190,10 +1196,13 @@ class api {
$contexts = [];
}
if ($request->get('type') == static::DATAREQUEST_TYPE_DELETE) {
if ($isconfigured && $request->get('type') == static::DATAREQUEST_TYPE_DELETE) {
$context = \context::instance_by_id($record->contextid);
$purpose = static::get_effective_context_purpose($context);
// Data can only be deleted from it if the context is either expired, or unprotected.
// Note: We can only check whether a context is expired or unprotected if the site is configured and
// defaults are set appropriately. If they are not, we treat all contexts as though they are
// unprotected.
if (!expired_contexts_manager::is_context_expired_or_unprotected_for_user($context, $foruser)) {
continue;
}
@@ -920,9 +920,17 @@ class expired_contexts_manager {
* @return bool
*/
protected static function is_course_context_expired_or_unprotected_for_user(\context $context, \stdClass $user) {
$expiryrecords = self::get_nested_expiry_info_for_courses($context->path);
$info = $expiryrecords[$context->path]->info;
if ($context->get_course_context()->instanceid == SITEID) {
// The is an activity in the site course (front page).
$purpose = data_registry::get_effective_contextlevel_value(CONTEXT_SYSTEM, 'purpose');
$info = static::get_expiry_info($purpose);
} else {
$expiryrecords = self::get_nested_expiry_info_for_courses($context->path);
$info = $expiryrecords[$context->path]->info;
}
if ($info->is_fully_expired()) {
// This context is fully expired.
return true;
@@ -74,11 +74,9 @@ class process_data_request_task extends adhoc_task {
return;
}
// If no site purpose is defined, reject requests since they cannot be processed.
if (!\tool_dataprivacy\data_registry::defaults_set()) {
api::update_request_status($requestid, api::DATAREQUEST_STATUS_REJECTED);
mtrace('No site purpose defined. Request ' . $requestid . ' rejected.');
return;
// Warn if no site purpose is defined.
mtrace('Warning: No purpose is defined at the system level. Deletion will delete all.');
}
// Get the user details now. We might not be able to retrieve it later if it's a deletion processing.
+4
View File
@@ -38,6 +38,10 @@ $title = get_string('datarequests', 'tool_dataprivacy');
echo $OUTPUT->header();
echo $OUTPUT->heading($title);
if (!\tool_dataprivacy\data_registry::defaults_set()) {
\core\notification::error(get_string('systemconfignotsetwarning', 'tool_dataprivacy'));
}
if (\tool_dataprivacy\api::is_site_dpo($USER->id)) {
$filtersapplied = optional_param_array('request-filters', [-1], PARAM_NOTAGS);
$filterscleared = optional_param('filters-cleared', 0, PARAM_INT);
+14
View File
@@ -279,5 +279,19 @@ function xmldb_tool_dataprivacy_upgrade($oldversion) {
upgrade_plugin_savepoint(true, 2017111356, 'tool', 'dataprivacy');
}
if ($oldversion < 2017111358) {
// Delete orphaned data privacy requests.
$sql = "SELECT r.id
FROM {tool_dataprivacy_request} r LEFT JOIN {user} u ON r.userid = u.id
WHERE u.id IS NULL";
$orphaned = $DB->get_fieldset_sql($sql);
if ($orphaned) {
$DB->delete_records_list('tool_dataprivacy_request', 'id', $orphaned);
}
upgrade_plugin_savepoint(true, 2017111358, 'tool', 'dataprivacy');
}
return true;
}
@@ -308,6 +308,7 @@ $string['statusrejected'] = 'Rejected';
$string['subjectscope'] = 'Subject scope';
$string['subjectscope_help'] = 'The subject scope lists the roles which may be assigned in this context.';
$string['summary'] = 'Registry configuration summary';
$string['systemconfignotsetwarning'] = 'A site purpose and category have not been defined. When these are not defined, all data will be removed when processing deletion requests.';
$string['user'] = 'User';
$string['userlistnoncompliant'] = 'Userlist provider missing';
$string['userlistexplanation'] = 'This plugin has the base provider but should also implement the userlist provider for full support of privacy functionality.';
+100
View File
@@ -1711,6 +1711,48 @@ class tool_dataprivacy_api_testcase extends advanced_testcase {
$this->assertEquals($data->contexts->used, $contextids, '', 0.0, 10, true);
}
/**
* Test that delete requests do not filter out protected purpose contexts if the the site is properly configured.
*/
public function test_add_request_contexts_with_status_delete_course_no_site_config() {
$this->resetAfterTest();
$user = $this->getDataGenerator()->create_user();
$course = $this->getDataGenerator()->create_course(['startdate' => time() - YEARSECS, 'enddate' => time() - YEARSECS]);
$coursecontext = \context_course::instance($course->id);
$forum = $this->getDataGenerator()->create_module('forum', ['course' => $course->id]);
list(, $forumcm) = get_course_and_cm_from_instance($forum->id, 'forum');
$contextforum = \context_module::instance($forumcm->id);
$this->getDataGenerator()->enrol_user($user->id, $course->id, 'student');
// Create the initial contextlist.
$initialcollection = new \core_privacy\local\request\contextlist_collection($user->id);
$contextlist = new \core_privacy\local\request\contextlist();
$contextlist->add_from_sql('SELECT id FROM {context} WHERE id = :contextid', ['contextid' => $coursecontext->id]);
$contextlist->set_component('tool_dataprivacy');
$initialcollection->add_contextlist($contextlist);
$contextlist = new \core_privacy\local\request\contextlist();
$contextlist->add_from_sql('SELECT id FROM {context} WHERE id = :contextid', ['contextid' => $contextforum->id]);
$contextlist->set_component('mod_forum');
$initialcollection->add_contextlist($contextlist);
$request = api::create_data_request($user->id, api::DATAREQUEST_TYPE_DELETE);
api::add_request_contexts_with_status(
$initialcollection, $request->get('id'), contextlist_context::STATUS_APPROVED);
$newcollection = \tool_dataprivacy\request_contextlist::get_records();
$this->assertCount(2, $newcollection);
foreach ($newcollection as $contextlist) {
$requests = contextlist_context::get_records(['contextlistid' => $contextlist->get('contextlistid')]);
$this->assertCount(1, $requests);
}
}
/**
* Test that delete requests do not filter out protected purpose contexts if they are already expired.
*/
@@ -1807,6 +1849,64 @@ class tool_dataprivacy_api_testcase extends advanced_testcase {
$this->assertCount(1, $requests);
}
/**
* Test that delete requests do not filter out protected purpose contexts if the the site is properly configured.
*/
public function test_get_approved_contextlist_collection_for_request_delete_course_no_config() {
$this->resetAfterTest();
$user = $this->getDataGenerator()->create_user();
$course = $this->getDataGenerator()->create_course(['startdate' => time() - YEARSECS, 'enddate' => time() - YEARSECS]);
$coursecontext = \context_course::instance($course->id);
$forum = $this->getDataGenerator()->create_module('forum', ['course' => $course->id]);
list(, $forumcm) = get_course_and_cm_from_instance($forum->id, 'forum');
$contextforum = \context_module::instance($forumcm->id);
$this->getDataGenerator()->enrol_user($user->id, $course->id, 'student');
// Create the initial contextlist.
$request = api::create_data_request($user->id, api::DATAREQUEST_TYPE_DELETE);
$initialcollection = new \core_privacy\local\request\contextlist_collection($user->id);
$contextlist = new \tool_dataprivacy\contextlist(0, (object) ['component' => 'tool_dataprivacy']);
$contextlist->save();
$clcontext = new \tool_dataprivacy\contextlist_context(0, (object) [
'contextid' => $coursecontext->id,
'status' => contextlist_context::STATUS_APPROVED,
'contextlistid' => $contextlist->get('id'),
]);
$clcontext->save();
$rcl = new \tool_dataprivacy\request_contextlist(0, (object) [
'requestid' => $request->get('id'),
'contextlistid' => $contextlist->get('id'),
]);
$rcl->save();
$contextlist = new \tool_dataprivacy\contextlist(0, (object) ['component' => 'mod_forum']);
$contextlist->save();
$clcontext = new \tool_dataprivacy\contextlist_context(0, (object) [
'contextid' => $contextforum->id,
'status' => contextlist_context::STATUS_APPROVED,
'contextlistid' => $contextlist->get('id'),
]);
$clcontext->save();
$rcl = new \tool_dataprivacy\request_contextlist(0, (object) [
'requestid' => $request->get('id'),
'contextlistid' => $contextlist->get('id'),
]);
$rcl->save();
$collection = api::get_approved_contextlist_collection_for_request($request);
$this->assertCount(2, $collection);
$list = $collection->get_contextlist_for_component('tool_dataprivacy');
$this->assertCount(1, $list);
$list = $collection->get_contextlist_for_component('mod_forum');
$this->assertCount(1, $list);
}
/**
* Test that delete requests do not filter out protected purpose contexts if they are already expired.
*/
@@ -49,7 +49,7 @@ Feature: Data export from the privacy API
And I reload the page
And I should see "Download ready" in the "Victim User 1" "table_row"
And I open the action menu in "Victim User 1" "table_row"
And following "Download" should download between "1" and "100000" bytes
And following "Download" should download between "1" and "130000" bytes
And the following config values are set as admin:
| privacyrequestexpiry | 1 | tool_dataprivacy |
And I wait "1" seconds
@@ -87,7 +87,7 @@ Feature: Data export from the privacy API
And I reload the page
And I should see "Download ready" in the "Export all of my personal data" "table_row"
And I open the action menu in "Victim User 1" "table_row"
And following "Download" should download between "1" and "100000" bytes
And following "Download" should download between "1" and "130000" bytes
And the following config values are set as admin:
| privacyrequestexpiry | 1 | tool_dataprivacy |
@@ -127,7 +127,7 @@ Feature: Data export from the privacy API
And I reload the page
And I should see "Download ready" in the "Victim User 1" "table_row"
And I open the action menu in "Victim User 1" "table_row"
And following "Download" should download between "1" and "100000" bytes
And following "Download" should download between "1" and "130000" bytes
And the following config values are set as admin:
| privacyrequestexpiry | 1 | tool_dataprivacy |
@@ -2222,6 +2222,40 @@ class tool_dataprivacy_expired_contexts_testcase extends advanced_testcase {
$this->assertTrue(expired_contexts_manager::is_context_expired_or_unprotected_for_user($blockcontext, $user));
}
/**
* Test the is_context_expired functions when supplied with the front page course.
*/
public function test_is_context_expired_frontpage() {
$this->resetAfterTest();
$purposes = $this->setup_basics('PT1H', 'PT1H', 'P1D');
$frontcourse = get_site();
$frontcoursecontext = \context_course::instance($frontcourse->id);
$sitenews = $this->getDataGenerator()->create_module('forum', ['course' => $frontcourse->id]);
$cm = get_coursemodule_from_instance('forum', $sitenews->id);
$sitenewscontext = \context_module::instance($cm->id);
$user = $this->getDataGenerator()->create_user(['lastaccess' => time() - YEARSECS]);
$this->assertFalse(expired_contexts_manager::is_context_expired($frontcoursecontext));
$this->assertFalse(expired_contexts_manager::is_context_expired($sitenewscontext));
$this->assertTrue(expired_contexts_manager::is_context_expired_or_unprotected_for_user($frontcoursecontext, $user));
$this->assertTrue(expired_contexts_manager::is_context_expired_or_unprotected_for_user($sitenewscontext, $user));
// Protecting the course contextlevel does not impact the front page.
$purposes->course->set('protected', 1)->save();
$this->assertTrue(expired_contexts_manager::is_context_expired_or_unprotected_for_user($frontcoursecontext, $user));
$this->assertTrue(expired_contexts_manager::is_context_expired_or_unprotected_for_user($sitenewscontext, $user));
// Protecting the system contextlevel affects the front page, too.
$purposes->system->set('protected', 1)->save();
$this->assertFalse(expired_contexts_manager::is_context_expired_or_unprotected_for_user($frontcoursecontext, $user));
$this->assertFalse(expired_contexts_manager::is_context_expired_or_unprotected_for_user($sitenewscontext, $user));
}
/**
* Test the is_context_expired functions when supplied with an expired course.
*/
+1 -1
View File
@@ -24,6 +24,6 @@
defined('MOODLE_INTERNAL') || die;
$plugin->version = 2017111357;
$plugin->version = 2017111358;
$plugin->requires = 2017111304.00; // Moodle 3.4.4 (Build: 20180517) and upwards.
$plugin->component = 'tool_dataprivacy';
+85 -1
View File
@@ -139,6 +139,36 @@
<stringProp name="HTTPSampler.embedded_url_re"></stringProp>
</HTTPSamplerProxy>
<hashTree/>
<HTTPSamplerProxy guiclass="HttpTestSampleGui" testclass="HTTPSamplerProxy" testname="View login page" enabled="true">
<elementProp name="HTTPsampler.Arguments" elementType="Arguments" guiclass="HTTPArgumentsPanel" testclass="Arguments" testname="User Defined Variables" enabled="true">
<collectionProp name="Arguments.arguments"/>
</elementProp>
<stringProp name="HTTPSampler.domain"></stringProp>
<stringProp name="HTTPSampler.port"></stringProp>
<stringProp name="HTTPSampler.connect_timeout"></stringProp>
<stringProp name="HTTPSampler.response_timeout"></stringProp>
<stringProp name="HTTPSampler.protocol"></stringProp>
<stringProp name="HTTPSampler.contentEncoding"></stringProp>
<stringProp name="HTTPSampler.path">${sitepath}/login/index.php</stringProp>
<stringProp name="HTTPSampler.method">GET</stringProp>
<boolProp name="HTTPSampler.follow_redirects">true</boolProp>
<boolProp name="HTTPSampler.auto_redirects">false</boolProp>
<boolProp name="HTTPSampler.use_keepalive">true</boolProp>
<boolProp name="HTTPSampler.DO_MULTIPART_POST">false</boolProp>
<boolProp name="HTTPSampler.monitor">false</boolProp>
<stringProp name="HTTPSampler.embedded_url_re"></stringProp>
</HTTPSamplerProxy>
<hashTree>
<RegexExtractor guiclass="RegexExtractorGui" testclass="RegexExtractor" testname="Get logintoken" enabled="true">
<stringProp name="RegexExtractor.useHeaders">false</stringProp>
<stringProp name="RegexExtractor.refname">LOGIN_TOKEN</stringProp>
<stringProp name="RegexExtractor.regex">type=&quot;hidden&quot;\sname=&quot;logintoken&quot;\svalue=&quot;(\w+)&quot;</stringProp>
<stringProp name="RegexExtractor.template">$1$</stringProp>
<stringProp name="RegexExtractor.default">0</stringProp>
<stringProp name="RegexExtractor.match_number">1</stringProp>
</RegexExtractor>
<hashTree/>
</hashTree>
<HTTPSamplerProxy guiclass="HttpTestSampleGui" testclass="HTTPSamplerProxy" testname="Login" enabled="true">
<elementProp name="HTTPsampler.Arguments" elementType="Arguments" guiclass="HTTPArgumentsPanel" testclass="Arguments" testname="User Defined Variables" enabled="true">
<collectionProp name="Arguments.arguments">
@@ -156,6 +186,13 @@
<boolProp name="HTTPArgument.use_equals">true</boolProp>
<stringProp name="Argument.name">password</stringProp>
</elementProp>
<elementProp name="logintoken" elementType="HTTPArgument">
<boolProp name="HTTPArgument.always_encode">false</boolProp>
<stringProp name="Argument.value">${LOGIN_TOKEN}</stringProp>
<stringProp name="Argument.metadata">=</stringProp>
<boolProp name="HTTPArgument.use_equals">true</boolProp>
<stringProp name="Argument.name">logintoken</stringProp>
</elementProp>
</collectionProp>
</elementProp>
<stringProp name="HTTPSampler.domain"></stringProp>
@@ -173,7 +210,17 @@
<boolProp name="HTTPSampler.monitor">false</boolProp>
<stringProp name="HTTPSampler.embedded_url_re"></stringProp>
</HTTPSamplerProxy>
<hashTree/>
<hashTree>
<ResponseAssertion guiclass="AssertionGui" testclass="ResponseAssertion" testname="Logged in" enabled="true">
<collectionProp name="Asserion.test_strings">
<stringProp name="615717117">&lt;div class=&quot;logininfo&quot;&gt;You are logged in as</stringProp>
</collectionProp>
<stringProp name="Assertion.test_field">Assertion.response_data</stringProp>
<boolProp name="Assertion.assume_success">false</boolProp>
<intProp name="Assertion.test_type">2</intProp>
</ResponseAssertion>
<hashTree/>
</hashTree>
<HTTPSamplerProxy guiclass="HttpTestSampleGui" testclass="HTTPSamplerProxy" testname="Frontpage logged" enabled="true">
<elementProp name="HTTPsampler.Arguments" elementType="Arguments" guiclass="HTTPArgumentsPanel" testclass="Arguments" testname="User Defined Variables" enabled="true">
<collectionProp name="Arguments.arguments"/>
@@ -335,6 +382,36 @@
<stringProp name="HTTPSampler.embedded_url_re"></stringProp>
</HTTPSamplerProxy>
<hashTree/>
<HTTPSamplerProxy guiclass="HttpTestSampleGui" testclass="HTTPSamplerProxy" testname="View login page" enabled="true">
<elementProp name="HTTPsampler.Arguments" elementType="Arguments" guiclass="HTTPArgumentsPanel" testclass="Arguments" testname="User Defined Variables" enabled="true">
<collectionProp name="Arguments.arguments"/>
</elementProp>
<stringProp name="HTTPSampler.domain"></stringProp>
<stringProp name="HTTPSampler.port"></stringProp>
<stringProp name="HTTPSampler.connect_timeout"></stringProp>
<stringProp name="HTTPSampler.response_timeout"></stringProp>
<stringProp name="HTTPSampler.protocol"></stringProp>
<stringProp name="HTTPSampler.contentEncoding"></stringProp>
<stringProp name="HTTPSampler.path">${sitepath}/login/index.php</stringProp>
<stringProp name="HTTPSampler.method">GET</stringProp>
<boolProp name="HTTPSampler.follow_redirects">true</boolProp>
<boolProp name="HTTPSampler.auto_redirects">false</boolProp>
<boolProp name="HTTPSampler.use_keepalive">true</boolProp>
<boolProp name="HTTPSampler.DO_MULTIPART_POST">false</boolProp>
<boolProp name="HTTPSampler.monitor">false</boolProp>
<stringProp name="HTTPSampler.embedded_url_re"></stringProp>
</HTTPSamplerProxy>
<hashTree>
<RegexExtractor guiclass="RegexExtractorGui" testclass="RegexExtractor" testname="Get logintoken" enabled="true">
<stringProp name="RegexExtractor.useHeaders">false</stringProp>
<stringProp name="RegexExtractor.refname">LOGIN_TOKEN</stringProp>
<stringProp name="RegexExtractor.regex">type=&quot;hidden&quot;\sname=&quot;logintoken&quot;\svalue=&quot;(\w+)&quot;</stringProp>
<stringProp name="RegexExtractor.template">$1$</stringProp>
<stringProp name="RegexExtractor.default">0</stringProp>
<stringProp name="RegexExtractor.match_number">1</stringProp>
</RegexExtractor>
<hashTree/>
</hashTree>
<HTTPSamplerProxy guiclass="HttpTestSampleGui" testclass="HTTPSamplerProxy" testname="Login" enabled="true">
<elementProp name="HTTPsampler.Arguments" elementType="Arguments" guiclass="HTTPArgumentsPanel" testclass="Arguments" testname="User Defined Variables" enabled="true">
<collectionProp name="Arguments.arguments">
@@ -352,6 +429,13 @@
<boolProp name="HTTPArgument.use_equals">true</boolProp>
<stringProp name="Argument.name">password</stringProp>
</elementProp>
<elementProp name="logintoken" elementType="HTTPArgument">
<boolProp name="HTTPArgument.always_encode">false</boolProp>
<stringProp name="Argument.value">${LOGIN_TOKEN}</stringProp>
<stringProp name="Argument.metadata">=</stringProp>
<boolProp name="HTTPArgument.use_equals">true</boolProp>
<stringProp name="Argument.name">logintoken</stringProp>
</elementProp>
</collectionProp>
</elementProp>
<stringProp name="HTTPSampler.domain"></stringProp>
+1 -1
View File
@@ -116,7 +116,7 @@ class tool_lp_external_testcase extends externallib_advanced_testcase {
$this->userrole = create_role('User role', 'lpuserrole', 'learning plan user role description');
assign_capability('moodle/competency:competencymanage', CAP_ALLOW, $this->creatorrole, $syscontext->id);
assign_capability('moodle/competency:competencycompetencyconfigure', CAP_ALLOW, $this->creatorrole, $syscontext->id);
assign_capability('moodle/competency:coursecompetencyconfigure', CAP_ALLOW, $this->creatorrole, $syscontext->id);
assign_capability('moodle/competency:planmanage', CAP_ALLOW, $this->creatorrole, $syscontext->id);
assign_capability('moodle/competency:planmanagedraft', CAP_ALLOW, $this->creatorrole, $syscontext->id);
assign_capability('moodle/competency:planmanageown', CAP_ALLOW, $this->creatorrole, $syscontext->id);
+1 -1
View File
@@ -21,7 +21,7 @@
* @copyright 2016 Damyon Wiese
* @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->libdir.'/adminlib.php');
+2
View File
@@ -154,6 +154,7 @@ class auth_plugin_cas extends auth_plugin_ldap {
$frm = new stdClass();
$frm->username = phpCAS::getUser();
$frm->password = 'passwdCas';
$frm->logintoken = \core\session\manager::get_login_token();
// Redirect to a course if multi-auth is activated, authCAS is set to CAS and the courseid is specified.
if ($this->config->multiauth && !empty($courseid)) {
@@ -167,6 +168,7 @@ class auth_plugin_cas extends auth_plugin_ldap {
$frm = new stdClass();
$frm->username = 'guest';
$frm->password = 'guest';
$frm->logintoken = \core\session\manager::get_login_token();
return;
}
+6
View File
@@ -4446,6 +4446,12 @@ class restore_create_categories_and_questions extends restore_structure_step {
// With newitemid = 0, let's create the question
if (!$questionmapping->newitemid) {
if ($data->qtype === 'random') {
// Ensure that this newly created question is considered by
// \qtype_random\task\remove_unused_questions.
$data->hidden = 0;
}
$newitemid = $DB->insert_record('question', $data);
$this->set_mapping('question', $oldid, $newitemid);
// Also annotate them as question_created, we need
+1 -4
View File
@@ -50,9 +50,6 @@ class edit_backpack_form extends moodleform {
$mform->addElement('hidden', 'userid', $USER->id);
$mform->setType('userid', PARAM_INT);
$mform->addElement('hidden', 'backpackurl', BADGE_BACKPACKURL);
$mform->setType('backpackurl', PARAM_URL);
if (isset($this->_customdata['email'])) {
// Email will be passed in when we're in the process of verifying the user's email address,
// so set the connection status, lock the email field, and provide options to resend the verification
@@ -94,7 +91,7 @@ class edit_backpack_form extends moodleform {
// We don't need to verify the email address if we're clearing a pending email verification attempt.
if (!isset($data['revertbutton'])) {
$check = new stdClass();
$check->backpackurl = $data['backpackurl'];
$check->backpackurl = BADGE_BACKPACKURL;
$check->email = $data['email'];
$bp = new OpenBadgesBackpackHandler($check);
@@ -40,6 +40,7 @@ class managesubscriptions extends \moodleform {
* Defines the form used to add calendar subscriptions.
*/
public function definition() {
global $PAGE;
$mform = $this->_form;
$eventtypes = calendar_get_allowed_event_types();
if (in_array(true, $eventtypes, true) === false) {
@@ -86,6 +87,9 @@ class managesubscriptions extends \moodleform {
// Eventtype: 0 = user, 1 = global, anything else = course ID.
$mform->addElement('submit', 'add', get_string('add'));
// Add the javascript required to enhance this mform.
$PAGE->requires->js_call_amd('core_calendar/event_form', 'init', [$mform->getAttribute('id')]);
}
/**
+5 -8
View File
@@ -2769,14 +2769,6 @@ function calendar_add_subscription($sub) {
unset($sub->groupcourseid);
}
// Pull the group id back out of the value. The form saves the value
// as "<courseid>-<groupid>" to allow the javascript to work correctly.
if (!empty($sub->groupid)) {
list($courseid, $groupid) = explode('-', $sub->groupid);
$sub->courseid = $courseid;
$sub->groupid = $groupid;
}
// Default course id if none is set.
if (empty($sub->courseid)) {
if ($sub->eventtype === 'site') {
@@ -3569,6 +3561,11 @@ function calendar_output_fragment_event_form($args) {
$mform->set_data($data);
} else {
$event = calendar_event::load($eventid);
if (!calendar_edit_event_allowed($event)) {
print_error('nopermissiontoupdatecalendar');
}
$mapper = new \core_calendar\local\event\mappers\create_update_form_mapper();
$eventdata = $mapper->from_legacy_event_to_data($event);
$data = array_merge((array) $eventdata, $data);
+17 -1
View File
@@ -33,6 +33,7 @@ $categoryid = optional_param('category', null, PARAM_INT);
// Used for processing subscription actions.
$subscriptionid = optional_param('id', 0, PARAM_INT);
$pollinterval = optional_param('pollinterval', 0, PARAM_INT);
$groupcourseid = optional_param('groupcourseid', 0, PARAM_INT);
$action = optional_param('action', '', PARAM_INT);
$url = new moodle_url('/calendar/managesubscriptions.php');
@@ -61,7 +62,22 @@ if (!calendar_user_can_add_event($course)) {
print_error('errorcannotimport', 'calendar');
}
$form = new \core_calendar\local\event\forms\managesubscriptions(null, ['courseid' => $course->id]);
// Populate the 'group' select box based on the given 'groupcourseid', if necessary.
$groups = [];
if (!empty($groupcourseid)) {
require_once($CFG->libdir . '/grouplib.php');
$groupcoursedata = groups_get_course_data($groupcourseid);
if (!empty($groupcoursedata->groups)) {
foreach ($groupcoursedata->groups as $groupid => $groupdata) {
$groups[$groupid] = $groupdata->name;
}
}
}
$customdata = [
'courseid' => $course->id,
'groups' => $groups,
];
$form = new \core_calendar\local\event\forms\managesubscriptions(null, $customdata);
$form->set_data(array(
'course' => $course->id
));
+7 -3
View File
@@ -533,7 +533,8 @@ class core_calendar_events_testcase extends advanced_testcase {
$subscription = new stdClass();
$subscription->eventtype = 'group';
$subscription->name = 'test';
$subscription->groupid = "{$courseid}-{$groupid}";
$subscription->courseid = $courseid;
$subscription->groupid = $groupid;
// Trigger and capture the event.
$sink = $this->redirectEvents();
@@ -668,7 +669,9 @@ class core_calendar_events_testcase extends advanced_testcase {
$subscription = new stdClass();
$subscription->eventtype = 'group';
$subscription->name = 'test';
$subscription->groupid = "{$courseid}-{$groupid}";
$subscription->courseid = $courseid;
$subscription->groupid = $groupid;
$subscription->id = calendar_add_subscription($subscription);
// Now edit it.
$subscription->name = 'awesome';
@@ -797,7 +800,8 @@ class core_calendar_events_testcase extends advanced_testcase {
$subscription = new stdClass();
$subscription->eventtype = 'group';
$subscription->name = 'test';
$subscription->groupid = "{$courseid}-{$groupid}";
$subscription->groupid = $groupid;
$subscription->courseid = $courseid;
$subscription->id = calendar_add_subscription($subscription);
// Trigger and capture the event.
+3 -1
View File
@@ -102,6 +102,7 @@ class core_comment_external extends external_api {
if ($comments === false) {
throw new moodle_exception('nopermissions', 'error', '', 'view comments');
}
$options = array('blanktarget' => true);
foreach ($comments as $key => $comment) {
@@ -110,7 +111,8 @@ class core_comment_external extends external_api {
$context->id,
$params['component'],
'',
0);
0,
$options);
}
$results = array(
+3 -2
View File
@@ -570,7 +570,7 @@ class comment {
$params['itemid'] = $this->itemid;
$comments = array();
$formatoptions = array('overflowdiv' => true);
$formatoptions = array('overflowdiv' => true, 'blanktarget' => true);
$rs = $DB->get_recordset_sql($sql, $params, $start, $perpage);
foreach ($rs as $u) {
$c = new stdClass();
@@ -717,7 +717,8 @@ class comment {
$newcmt->fullname = fullname($USER);
$url = new moodle_url('/user/view.php', array('id' => $USER->id, 'course' => $this->courseid));
$newcmt->profileurl = $url->out();
$newcmt->content = format_text($newcmt->content, $newcmt->format, array('overflowdiv'=>true));
$formatoptions = array('overflowdiv' => true, 'blanktarget' => true);
$newcmt->content = format_text($newcmt->content, $newcmt->format, $formatoptions);
$newcmt->avatar = $OUTPUT->user_picture($USER, array('size'=>16));
$commentlist = array($newcmt);
+1 -1
View File
@@ -68,7 +68,7 @@ class comment_manager {
ON u.id=c.userid
ORDER BY c.timecreated ASC";
$rs = $DB->get_recordset_sql($sql, null, $start, $this->perpage);
$formatoptions = array('overflowdiv' => true);
$formatoptions = array('overflowdiv' => true, 'blanktarget' => true);
foreach ($rs as $item) {
// Set calculated fields
$item->fullname = fullname($item);
+1 -1
View File
@@ -139,7 +139,7 @@ class core_competency_external_testcase extends externallib_advanced_testcase {
$this->userrole = create_role('User role', 'userrole', 'learning plan user role description');
assign_capability('moodle/competency:competencymanage', CAP_ALLOW, $this->creatorrole, $syscontext->id);
assign_capability('moodle/competency:competencycompetencyconfigure', CAP_ALLOW, $this->creatorrole, $syscontext->id);
assign_capability('moodle/competency:coursecompetencyconfigure', CAP_ALLOW, $this->creatorrole, $syscontext->id);
assign_capability('moodle/competency:competencyview', CAP_ALLOW, $this->userrole, $syscontext->id);
assign_capability('moodle/competency:planmanage', CAP_ALLOW, $this->creatorrole, $syscontext->id);
assign_capability('moodle/competency:planmanagedraft', CAP_ALLOW, $this->creatorrole, $syscontext->id);
+43 -35
View File
@@ -161,47 +161,55 @@ class provider implements
$completioninfo = new \completion_info($course);
$completion = $completioninfo->is_enabled();
if ($completion == COMPLETION_ENABLED) {
if ($completion != COMPLETION_ENABLED) {
return [];
}
$coursecomplete = $completioninfo->is_course_complete($user->id);
$coursecomplete = $completioninfo->is_course_complete($user->id);
if ($coursecomplete) {
$status = get_string('complete');
} else {
$criteriacomplete = $completioninfo->count_course_user_data($user->id);
$ccompletion = new \completion_completion(['userid' => $user->id, 'course' => $course->id]);
$status = ($coursecomplete) ? get_string('complete') : '';
$status = (!$criteriacomplete && !$ccompletion->timestarted) ? get_string('notyetstarted', 'completion') :
get_string('inprogress', 'completion');
$completions = $completioninfo->get_completions($user->id);
$overall = get_string('nocriteriaset', 'completion');
if (!empty($completions)) {
if ($completioninfo->get_aggregation_method() == COMPLETION_AGGREGATION_ALL) {
$overall = get_string('criteriarequiredall', 'completion');
} else {
$overall = get_string('criteriarequiredany', 'completion');
}
if (!$criteriacomplete && !$ccompletion->timestarted) {
$status = get_string('notyetstarted', 'completion');
} else {
$status = get_string('inprogress', 'completion');
}
$coursecompletiondata = [
'status' => $status,
'required' => $overall,
];
$coursecompletiondata['criteria'] = array_map(function($completion) use ($completioninfo) {
$criteria = $completion->get_criteria();
$aggregation = $completioninfo->get_aggregation_method($criteria->criteriatype);
$required = ($aggregation == COMPLETION_AGGREGATION_ALL) ? get_string('all', 'completion') :
get_string('any', 'completion');
$data = [
'required' => $required,
'completed' => transform::yesno($completion->is_complete()),
'timecompleted' => isset($completion->timecompleted) ? transform::datetime($completion->timecompleted) : ''
];
$details = $criteria->get_details($completion);
$data = array_merge($data, $details);
return $data;
}, $completions);
return $coursecompletiondata;
}
$completions = $completioninfo->get_completions($user->id);
$overall = get_string('nocriteriaset', 'completion');
if (!empty($completions)) {
if ($completioninfo->get_aggregation_method() == COMPLETION_AGGREGATION_ALL) {
$overall = get_string('criteriarequiredall', 'completion');
} else {
$overall = get_string('criteriarequiredany', 'completion');
}
}
$coursecompletiondata = [
'status' => $status,
'required' => $overall,
];
$coursecompletiondata['criteria'] = array_map(function($completion) use ($completioninfo) {
$criteria = $completion->get_criteria();
$aggregation = $completioninfo->get_aggregation_method($criteria->criteriatype);
$required = ($aggregation == COMPLETION_AGGREGATION_ALL) ? get_string('all', 'completion') :
get_string('any', 'completion');
$data = [
'required' => $required,
'completed' => transform::yesno($completion->is_complete()),
'timecompleted' => isset($completion->timecompleted) ? transform::datetime($completion->timecompleted) : ''
];
$details = $criteria->get_details($completion);
$data = array_merge($data, $details);
return $data;
}, $completions);
return $coursecompletiondata;
}
/**
+21 -3
View File
@@ -193,13 +193,31 @@ class core_completion_privacy_test extends \core_privacy\tests\provider_testcase
$hasno = array_search('No', $coursecompletion1['criteria'], true);
$this->assertFalse($hasno);
$coursecompletion2 = \core_completion\privacy\provider::get_course_completion_info($user2, $this->course);
$hasyes = array_search('Yes', $coursecompletion1['criteria'], true);
$hasyes = array_search('Yes', $coursecompletion2['criteria'], true);
$this->assertFalse($hasyes);
$coursecompletion3 = \core_completion\privacy\provider::get_course_completion_info($user3, $this->course);
$hasno = array_search('No', $coursecompletion1['criteria'], true);
$hasno = array_search('No', $coursecompletion3['criteria'], true);
$this->assertFalse($hasno);
$coursecompletion4 = \core_completion\privacy\provider::get_course_completion_info($user4, $this->course);
$hasyes = array_search('Yes', $coursecompletion1['criteria'], true);
$hasyes = array_search('Yes', $coursecompletion4['criteria'], true);
$this->assertFalse($hasyes);
}
/**
* Test getting course completion information with completion disabled.
*/
public function test_get_course_completion_info_completion_disabled() {
$this->resetAfterTest();
$user = $this->getDataGenerator()->create_user();
$course = $this->getDataGenerator()->create_course(['enablecompletion' => 0]);
$this->getDataGenerator()->enrol_user($user->id, $course->id, 'student');
$coursecompletion = \core_completion\privacy\provider::get_course_completion_info($user, $course);
$this->assertTrue(is_array($coursecompletion));
$this->assertEmpty($coursecompletion);
}
}
+2 -2
View File
@@ -6,8 +6,8 @@
"homepage": "https://moodle.org",
"require-dev": {
"phpunit/phpunit": "6.5.*",
"phpunit/dbUnit": "3.0.*",
"phpunit/dbunit": "3.0.*",
"moodlehq/behat-extension": "3.34.2",
"mikey179/vfsStream": "^1.6"
"mikey179/vfsstream": "^1.6"
}
}
@@ -0,0 +1,77 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* Unit tests.
*
* @package filter_activitynames
* @category test
* @copyright 2018 The Open University
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
defined('MOODLE_INTERNAL') || die();
global $CFG;
require_once($CFG->dirroot . '/filter/activitynames/filter.php'); // Include the code to test.
/**
* Test case for the activity names auto-linking filter.
*
* @copyright 2018 The Open University
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class filter_activitynames_filter_testcase extends advanced_testcase {
public function test_links() {
global $CFG;
$this->resetAfterTest(true);
// Create a test course.
$course = $this->getDataGenerator()->create_course();
$context = context_course::instance($course->id);
// Create two pages that will be linked to.
$page1 = $this->getDataGenerator()->create_module('page',
['course' => $course->id, 'name' => 'Test 1']);
$page2 = $this->getDataGenerator()->create_module('page',
['course' => $course->id, 'name' => 'Test (2)']);
// Format text with all three entries in HTML.
$html = '<p>Please read the two pages Test 1 and <i>Test (2)</i>.</p>';
$filtered = format_text($html, FORMAT_HTML, array('context' => $context));
// Find all the glossary links in the result.
$matches = [];
preg_match_all('~<a class="autolink" title="([^"]*)" href="[^"]*/mod/page/view.php\?id=([0-9]+)">([^<]*)</a>~',
$filtered, $matches);
// There should be 3 links links.
$this->assertEquals(2, count($matches[1]));
// Check text of title attribute.
$this->assertEquals($page1->name, $matches[1][0]);
$this->assertEquals($page2->name, $matches[1][1]);
// Check the ids in the links.
$this->assertEquals($page1->cmid, $matches[2][0]);
$this->assertEquals($page2->cmid, $matches[2][1]);
// Check the link text.
$this->assertEquals($page1->name, $matches[3][0]);
$this->assertEquals($page2->name, $matches[3][1]);
}
}
+46
View File
@@ -192,6 +192,52 @@ class filter_glossary_filter_testcase extends advanced_testcase {
$this->assertEquals($glossary->name . ': normal', $matches[2][2]);
}
/**
* Test brackets.
*/
public function test_brackets() {
global $CFG;
$this->resetAfterTest(true);
// Enable glossary filter at top level.
filter_set_global_state('glossary', TEXTFILTER_ON);
$CFG->glossary_linkentries = 1;
// Create a test course.
$course = $this->getDataGenerator()->create_course();
$context = context_course::instance($course->id);
// Create a glossary.
$glossary = $this->getDataGenerator()->create_module('glossary',
array('course' => $course->id, 'mainglossary' => 1));
// Create two entries with ampersands and one normal entry.
/** @var mod_glossary_generator $generator */
$generator = $this->getDataGenerator()->get_plugin_generator('mod_glossary');
$simple = $generator->create_content($glossary, array('concept' => 'simple'));
$withbrackets = $generator->create_content($glossary, array('concept' => 'more complex (perhaps)'));
$test2 = $generator->create_content($glossary, array('concept' => 'Test (2)'));
// Format text with all three entries in HTML.
$html = '<p>Some thigns are simple. Others are more complex (perhaps). Test (2).</p>';
$filtered = format_text($html, FORMAT_HTML, array('context' => $context));
// Find all the glossary links in the result.
$matches = array();
preg_match_all('~eid=([0-9]+).*?title="(.*?)"~', $filtered, $matches);
// There should be 3 glossary links.
$this->assertEquals(3, count($matches[1]));
$this->assertEquals($simple->id, $matches[1][0]);
$this->assertEquals($withbrackets->id, $matches[1][1]);
$this->assertEquals($test2->id, $matches[1][2]);
// Check text and escaping of title attribute.
$this->assertEquals($glossary->name . ': simple', $matches[2][0]);
$this->assertEquals($glossary->name . ': more complex (perhaps)', $matches[2][1]);
$this->assertEquals($glossary->name . ': Test (2)', $matches[2][2]);
}
public function test_exclude_excludes_link_to_entry_with_alias() {
global $CFG, $GLOSSARY_EXCLUDEENTRY;
+33
View File
@@ -0,0 +1,33 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* Automatically generated strings for Moodle installer
*
* Do not edit this file manually! It contains just a subset of strings
* needed during the very first steps of installation. This file was
* generated automatically by export-installer.php (which is part of AMOS
* {@link http://docs.moodle.org/dev/Languages/AMOS}) using the
* list of strings defined in /install/stringnames.txt.
*
* @package installer
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
defined('MOODLE_INTERNAL') || die();
$string['thislanguage'] = 'Bamanankan';
+1 -1
View File
@@ -43,7 +43,7 @@ $string['cannotunzipfile'] = 'No s\'ha pogut descomprimir el fitxer';
$string['componentisuptodate'] = 'El component està actualitzat';
$string['dmlexceptiononinstall'] = '<p>S\'ha produït un error de la base de dades [{$a->errorcode}].<br />{$a->debuginfo}</p>';
$string['downloadedfilecheckfailed'] = 'Ha fallat la comprovació del fitxer baixat';
$string['invalidmd5'] = 'L\'md5 no és vàlid. Torneu-ho a provar';
$string['invalidmd5'] = 'L\'md5 no és vàlid. Torneu a provar-ho';
$string['missingrequiredfield'] = 'Falta algun camp necessari';
$string['remotedownloaderror'] = '<p>No s\'ha pogut baixar el component al vostre servidor. Verifiqueu els paràmetres del servidor intermediari. Es recomana vivament l\'extensió cURL de PHP.</p>
<p>Haureu de baixar manualment el fitxer <a href="{$a->url}">{$a->url}</a>, copiar-lo a la ubicació «{$a->dest}» del vostre servidor i descomprimir-lo allí.</p>';
+1 -1
View File
@@ -38,7 +38,7 @@ $string['clitypevalue'] = 'πληκτρολογήστε μια τιμή';
$string['clitypevaluedefault'] = 'πληκτρολογήστε μια τιμή· πατήστε Enter για να χρησιμοποιήσετε τηνπροεπιλεγμένη τιμή ({$a})';
$string['cliunknowoption'] = 'Μη αναγνωρίσιμες επιλογές: {$a}<br />
Παρακαλούμε χρησιμοποιήστε την επιλογή --βοήθεια';
$string['cliyesnoprompt'] = 'πατώντας ν (σημαίνει ναι) αλλιώς πατώντας ο (σημαίνει όχι)';
$string['cliyesnoprompt'] = 'πατώντας y (σημαίνει yes=ναι) αλλιώς πατώντας n (σημαίνει no=όχι)';
$string['environmentrequireinstall'] = 'απαιτείται να εγκατασταθεί/ ενεργοποιηθεί';
$string['environmentrequireversion'] = 'απαιτείται η έκδοση {$a->needed} ενώ εσείς έχετε την {$a->current}';
$string['upgradekeyset'] = 'Κλειδί αναβάθμισης (αφήστε κενό για να μην το ορίσετε)';
+2 -2
View File
@@ -33,7 +33,7 @@ defined('MOODLE_INTERNAL') || die();
$string['admindirname'] = 'Φάκελος διαχειριστή';
$string['availablelangs'] = 'Λίστα διαθέσιμων πακέτων γλωσσών';
$string['chooselanguagehead'] = 'Επιλογή γλώσσας';
$string['chooselanguagesub'] = 'Παρακαλούμε, επιλέξτε γλώσσα για την εγκατάσταση ΜΟΝΟ. Θα μπορείτε να επιλέξετε γλώσσα ιστοσελίδας και χρηστών σε μια μετέπειτα οθόνη.';
$string['chooselanguagesub'] = 'Παρακαλούμε, επιλέξτε γλώσσα για την εγκατάσταση. Αυτή η γλώσσα θα χρησιμοποιηθεί επίσης ως προεπιλεγμένη γλώσσα για τον ιστότοπο, αν και μπορεί να αλλάξει αργότερα.';
$string['clialreadyconfigured'] = 'Το αρχείο ρυθμίσεων config.php υπάρχει ήδη. Χρησιμοποιήστε το admin/cli/install_database.php για να εγκαταστήσετε το Moodle για αυτόν τον ιστότοπο.';
$string['clialreadyinstalled'] = 'Το αρχείο ρυθμίσεων config.php υπάρχει ήδη. Χρησιμοποιήστε το admin/cli/install_database.php για να αναβαθμίσετε το Moodle για αυτόν τον ιστότοπο.';
$string['cliinstallheader'] = 'Moodle {$a} πρόγραμμα εγκατάστασης γραμμής εντολών';
@@ -75,4 +75,4 @@ $string['welcomep40'] = 'Το πακέτο περιλαμβάνει επίσης
$string['welcomep50'] = 'Η χρήση όλων των εφαρμογών σε αυτό το πακέτο υπόκειται στις αντίστοιχες άδειες. Ολόκληρο το πακέτο <strong>{$a->installername}</strong> είναι <a href="http://www.opensource.org/docs/definition_plain.html">λογισμικό ανοικτού κώδικα</a> και διανέμεται με την <a href="http://www.gnu.org/copyleft/gpl.html">GPL</a> άδεια.';
$string['welcomep60'] = 'Οι παρακάτω σελίδες θα σας καθοδηγήσουν με εύκολα βήματα στην εγκατάσταση και ρύθμιση του <strong>Moodle</strong> στον υπολογιστή σας. Μπορείτε να δεχθείτε τις προεπιλεγμένες ρυθμίσεις ή προαιρετικά, να τις τροποποιήσετε ανάλογα με τις ανάγκες σας.';
$string['welcomep70'] = 'Πατήστε το κουμπί "Συνέχεια" για να συνεχίσετε με την εκγατάσταση του <strong>Moodle</strong>.';
$string['wwwroot'] = 'Διεύθυνση ιστοσελίδας';
$string['wwwroot'] = 'Διεύθυνση ιστού';
+1
View File
@@ -30,5 +30,6 @@
defined('MOODLE_INTERNAL') || die();
$string['parentlanguage'] = '';
$string['thisdirection'] = 'ltr';
$string['thislanguage'] = 'Ελληνικά';
+7 -6
View File
@@ -32,12 +32,13 @@ defined('MOODLE_INTERNAL') || die();
$string['clianswerno'] = 'e';
$string['cliansweryes'] = 'b';
$string['cliincorrectvalueerror'] = 'Errorea, "{$a->value}" balorea ez da egokia "{$a->option}"-rako';
$string['cliincorrectvalueretry'] = 'Balore ezegokia, mesedez saiatu berriz';
$string['clitypevalue'] = 'Sartu balorea';
$string['clitypevaluedefault'] = 'sartu balorea, sakatu Enter-i berezko balorea erabiltzeko ({$a})';
$string['cliunknowoption'] = 'Aukera ezezagunak:{$a}
Mesedez, erabili --laguntza aukera.';
$string['cliincorrectvalueerror'] = 'Errorea, "{$a->value}" balioa ez da egokia "{$a->option}"-rako';
$string['cliincorrectvalueretry'] = 'Balio ezegokia, mesedez saiatu berriz';
$string['clitypevalue'] = 'Sartu balioa';
$string['clitypevaluedefault'] = 'sartu balioa, sakatu Enter-i lehenetsitako balioa erabiltzeko ({$a})';
$string['cliunknowoption'] = 'Aukera ezezagunak:
{$a}
Mesedez, erabili --help aukera.';
$string['cliyesnoprompt'] = 'idatzi b (bai esateko) edo e (ez esateko)';
$string['environmentrequireinstall'] = 'derrigorrezkoa da instalatuta eta gaituta izatea';
$string['environmentrequireversion'] = '{$a->needed} bertsioa beharrezkoa da eta zu {$a->current} ari zara egikaritzen';
+1 -1
View File
@@ -43,7 +43,7 @@ $string['cannotsavezipfile'] = 'Ezin da ZIP fitxategia gorde';
$string['cannotunzipfile'] = 'Ezin da fitxategia deskonprimatu';
$string['componentisuptodate'] = 'Osagaia eguneratu da';
$string['dmlexceptiononinstall'] = '<p>Datu-basearen errorea gertatu da [{$a->errorcode}].<br />{$a->debuginfo}</p>';
$string['downloadedfilecheckfailed'] = 'Jaitsitako fitxategiaren konprobazioak kale egin du.';
$string['downloadedfilecheckfailed'] = 'Jaitsitako fitxategiaren egiaztatzeak huts egin du.';
$string['invalidmd5'] = 'Kontrolerako aldagaia gaizki zegoen - saiatu berriz ere';
$string['missingrequiredfield'] = 'Beharrezko eremuren bat falta da.';
$string['remotedownloaderror'] = '<p>Errorea osagaia zure zerbitzarian jaistean, mesedez egiaztatu proxy-ezarpenak, PHP cURL luzapena erabat gomendatzen da.</p>
+6 -7
View File
@@ -33,7 +33,7 @@ defined('MOODLE_INTERNAL') || die();
$string['admindirname'] = 'Admin direktorioa';
$string['availablelangs'] = 'Hizkuntza-pakete eskuragarriak';
$string['chooselanguagehead'] = 'Aukeratu hizkuntza bat';
$string['chooselanguagesub'] = 'Mesedez, aukeratu instalaziorako hizkuntza bat. Hizkuntza hori erabiliko da gunearen berezko hizkuntza gisa, baina aurrerago alda daiteke.';
$string['chooselanguagesub'] = 'Mesedez, aukeratu instalaziorako hizkuntza bat. Hizkuntza hori erabiliko da gunearen hizkuntza lehenetsi gisa, baina aurrerago alda daiteke.';
$string['clialreadyconfigured'] = 'Dagoeneko badago config.php konfigurazio-fitxategia. Mesedez erabili admin/cli/install_database.php Moodle gune honetan instalatu nahi baduzu.';
$string['clialreadyinstalled'] = 'Dagoeneko badago config.php konfigurazio-fitxategia. Mesedez erabili admin/cli/upgrade.php zure Moodle gunea eguneratu nahi baduzu.';
$string['cliinstallheader'] = 'Moodle {$a} komando-lerro bidezko instalaziorako programa';
@@ -71,7 +71,7 @@ $string['pathserrcreatedataroot'] = 'Instalatzaileak ezin du datu-karpeta ({$a->
$string['pathshead'] = 'Egiaztatu bideak';
$string['pathsrodataroot'] = 'Dataroot direktorioa ez da idazteko modukoa.';
$string['pathsroparentdataroot'] = 'Goragoko karpeta ({$a->parent}) ez da idazteko modukoa. Instalatzaileak ezin du datu-karpeta ({$a->dataroot}) sortu.';
$string['pathssubadmindir'] = 'Web ostalari gutxi batzuk /admin URL berezi gisa erabiltzen dute kontrol-panel edo antzekora sarbidea emateko. Zoritxarrez, honek Moodle-ren kudeatze-orrien berezko kokapenarekin gatazka sortzen du. Hau konpondu dezakezu zure instalazioko admin karpeta berrizendatuz, eta izen berria hemen sartuta. Adibidez <em>moodleadmin</em>. Honek Moodle-ko admin estekak konponduko du.';
$string['pathssubadmindir'] = 'Web ostalari gutxi batzuk /admin URL berezi gisa erabiltzen dute kontrol-panel edo antzekora sarbidea emateko. Zoritxarrez, honek Moodle-ren kudeatze-orrien lehenetsitako kokapenarekin gatazka sortzen du. Hau konpondu dezakezu zure instalazioko admin karpeta berrizendatuz, eta izen berria hemen sartuta. Adibidez <em>moodleadmin</em>. Honek Moodle-ko admin estekak konponduko du.';
$string['pathssubdataroot'] = '<p>Moodle-k erabiltzaileek igotako fitxategien edukiak bilduko dituen direktorio bat.</p>
<p>Direktorio honetan web-zerbitzariaren erabiltzaileak irakurtzeko eta idazteko baimena izan beharko ditu (normalean \'www-data\', \'nobody\', edo \'apache\').</p>
<p>Ez litzateke web bidez eskuragarri egon beharko.</p>
@@ -90,14 +90,13 @@ $string['phpversionhelp'] = '<p>Moodle-k PHP 5.6.5 edo 7.1 bertsioetako bat beha
<p>PHP eguneratu edo PHP bertsio berriagoa duen zerbitzari batera jo.</p>';
$string['welcomep10'] = '{$a->installername} ({$a->installerversion})';
$string['welcomep20'] = 'Orri hau ikusten baduzu <strong>{$a->packname} {$a->packversion}</strong> paketea zure ordenagailuan instalatu ahal izan duzu. Zorionak!';
$string['welcomep30'] = '<strong>{$a->installername}</strong>ren bertsio honek <strong>Moodle</strong>k
zure ordenadorean funtzionatzeko behar diren aplikazioak dauzka,
bereziki:';
$string['welcomep30'] = '<strong>{$a->installername}</strong>ren bertsio honek <strong>Moodle</strong>-k
zure ordenagailuan funtzionatzeko behar diren aplikazioak dauzka, zehazki hurrengoak:';
$string['welcomep40'] = 'Paketeak ere zera dauka: <strong>Moodle {$a->moodlerelease} ({$a->moodleversion})</strong>.';
$string['welcomep50'] = 'Paketeko aplikazio guztien erabilpena dagozkien lizentziek
arautzen dute. <strong>{$a->installername}</strong> aplikazioak
<a href="http://www.opensource.org/docs/definition_plain.html">kode irekia</a> dauka eta
<a href="http://www.gnu.org/copyleft/gpl.html">GPL</a> lizentziapean banatzen da.';
$string['welcomep60'] = 'Datozen orriek urrats erraz batzuen bidez gidatuko zaituzte <strong>Moodle</strong> zure ordenagailuan instalatu eta konfiguratzeko. Aholkatzen diren lehentsitako baloreak mantendu edo, nahi izanez gero, alda ditzakezu zure beharrei erantzun diezaieten.';
$string['welcomep70'] = '"Hurrengoa" botoia sakatu <strong>Moodle</strong>ren konfigurazioarekin jarraitzeko.';
$string['welcomep60'] = 'Datozen orriek urrats erraz batzuen bidez gidatuko zaituzte <strong>Moodle</strong> zure ordenagailuan instalatu eta konfiguratzeko. Aholkatzen diren lehenetsitako balioak mantendu edo, nahi izanez gero, alda ditzakezu zure beharrei erantzun diezaieten.';
$string['welcomep70'] = 'Egin klik "Hurrengoa" botoian <strong>Moodle</strong>ren konfigurazioarekin jarraitzeko.';
$string['wwwroot'] = 'Web helbidea';
+1
View File
@@ -32,3 +32,4 @@ defined('MOODLE_INTERNAL') || die();
$string['language'] = 'भाषा';
$string['next'] = 'अगला';
$string['reload'] = 'सीमा से अधिक लादना';
+2 -1
View File
@@ -31,7 +31,7 @@
defined('MOODLE_INTERNAL') || die();
$string['clianswerno'] = 'n';
$string['cliansweryes'] = 'y';
$string['cliansweryes'] = 'd';
$string['cliincorrectvalueerror'] = 'Pogreška, netočna vrijednost "{$a->value}" za "{$a->option}"';
$string['cliincorrectvalueretry'] = 'Netočna vrijednost, pokušajte ponovno';
$string['clitypevalue'] = 'unesite vrijednost';
@@ -40,3 +40,4 @@ $string['cliunknowoption'] = 'Nepoznate opcije: {$a} Molimo koristite --help opc
$string['cliyesnoprompt'] = 'unesite y (znači da) ili n (znači ne)';
$string['environmentrequireinstall'] = 'je neophodno instalirati/omogućiti';
$string['environmentrequireversion'] = 'neophodna inačica je {$a->needed}, a vi trenutačno koristite inačicu {$a->current}';
$string['upgradekeyset'] = 'Ključ za ažuriranje (ostavite prazno kako ga ne bi zadali)';
+4
View File
@@ -30,6 +30,9 @@
defined('MOODLE_INTERNAL') || die();
$string['cannotcreatedboninstall'] = '<p>Ne mogu stvoriti bazu podataka.</p>
<p>Navedena baza podataka ne postoji i korisnik nema prava stvaranja baze podataka.</p>
<p>Administrator treba provjeriti postavke baze podataka.</p>';
$string['cannotcreatelangdir'] = 'Nije moguće stvoriti lang mapu';
$string['cannotcreatetempdir'] = 'Nije moguće stvoriti privremenu (TEMP) mapu';
$string['cannotdownloadcomponents'] = 'Nije moguće preuzimanje komponenti';
@@ -39,6 +42,7 @@ $string['cannotsavemd5file'] = 'Nije moguće pohraniti md5 datoteku';
$string['cannotsavezipfile'] = 'Nije moguće pohraniti ZIP datoteku';
$string['cannotunzipfile'] = 'Nije moguće otpakirati datoteku';
$string['componentisuptodate'] = 'Komponenta je dostupna u svojoj najnovijoj inačici.';
$string['dmlexceptiononinstall'] = '<p>Dogodila se pogreška baze podataka [{$a->errorcode}].<br />{$a->debuginfo}';
$string['downloadedfilecheckfailed'] = 'Došlo je do pogreške pri provjeri preuzete datoteke';
$string['invalidmd5'] = 'Neispravna md5 datoteka';
$string['missingrequiredfield'] = 'Nedostaje neko obvezatno polje';
+2 -2
View File
@@ -31,7 +31,7 @@
defined('MOODLE_INTERNAL') || die();
$string['admindirname'] = 'Admin mapa';
$string['availablelangs'] = 'Popis dostupnih jezika';
$string['availablelangs'] = 'Popis dostupnih jezičnih paketa';
$string['chooselanguagehead'] = 'Odaberite jezik';
$string['chooselanguagesub'] = 'Molimo odaberite jezik instalacije. Ovaj jezik će biti korišten kao zadani jezik sustava, a kasnije to možete lagano promijeniti.';
$string['clialreadyconfigured'] = 'Datoteka config.php već postoji. Upotrijebite naredbu admin/cli/install_database.php ako želite nastaviti instalaciju.';
@@ -68,7 +68,7 @@ $string['pathsroparentdataroot'] = 'Nije moguće zapisivati podatke u nadređenu
$string['pathssubadmindir'] = 'Manji broj webhosting tvrtki koristi /admin kao posebni URL za Vaš pristup upravljanju vašim hosting paketom. Nažalost, to rezultira konfliktom sa standardnom lokacijom za Moodle administratorsku stranicu. Navedenu lokaciju unutar Moodle sustava možete preimenovati. Na primjer: <br /> <br /><em>moodleadmin</em><br /> <br />
Ovo će promijeniti administratorsku poveznicu na Moodle sustavu u novu vrijednost.';
$string['pathssubdataroot'] = 'Mora postojati mapa u koju Moodle može pohraniti prenešene datoteke. Korisnik pod kojim je pokrenut web server (obično \'nobody\' ili \'apache\') bi morao imati mogućnost čitanja/pisanja podataka u toj mapi, ali oni ne bi trebali biti dostupni direktno preko weba. Instalacijska skripta će pokušati stvoriti navedenu mapu ako ista ne postoji.';
$string['pathssubdirroot'] = 'Puna putanja (PATH) do Moodle instalacije.';
$string['pathssubdirroot'] = '<p>Puna putanja (PATH) do Moodle instalacije.</p>';
$string['pathssubwwwroot'] = 'Unesite punu web adresu putem koje će se pristupati vašem Moodle sustavu.
Moodle sustavu NIJE MOGUĆE pristupiti preko više URL-ova, odaberite onaj koji vam najviše odgovara.
Ako vaš poslužitelj ima višestruke javne adrese, onda morate postaviti tzv. permanent redirect na sve osim ove adrese.
+1
View File
@@ -31,6 +31,7 @@
defined('MOODLE_INTERNAL') || die();
$string['language'] = 'Tungumál';
$string['moodlelogo'] = 'Moodle merkið';
$string['next'] = 'Áfram';
$string['previous'] = 'Til baka';
$string['reload'] = 'Endurhlaða';
+2 -2
View File
@@ -73,9 +73,9 @@ $string['pathssubdataroot'] = '<p>ユーザによってアップロードされ
<p>ウェブからは直接アクセスできないようにしてください。</p>
<p>現在ディレクトリが存在しない場合、インストレーションプロセスは作成を試みます。</p';
$string['pathssubdirroot'] = '<p>Moodleコードを含むディレクトリに関するフルパスです。</p>';
$string['pathssubwwwroot'] = '<p>Moodleにアクセスすることのできるフルウェブアドレスです。例えばユーザがブラウザのアドレスバーに入力してMoodleにアクセスするためのアドレスです。</p>
$string['pathssubwwwroot'] = '<p>Moodleにアクセスできるフルウェブアドレスです。例えばユーザがブラウザのアドレスバーに入力してMoodleにアクセスするためのアドレスです。</p>
<p>複数アドレスを使用しMoodleアクセスすることはできません。あなたのサイトに複数アドレスからアクセスできる場合、最も簡単なアドレスを選択して、すべてのアドレスにパーマネントリダイレクトを設定してください。</p>
<p>複数アドレスを使用しMoodleへのアクセスはできません。あなたのサイトに複数アドレスからアクセスできる場合、最も簡単なアドレスを選択して、すべてのアドレスにパーマネントリダイレクトを設定してください。</p>
<p>あなたのサイトにインターネットおよび内部ネットワーク (イントラネットと呼ばれます) からアクセスできる場合、ここではパブリックアドレスを使用してください。</p>
+2 -2
View File
@@ -33,10 +33,10 @@ defined('MOODLE_INTERNAL') || die();
$string['clianswerno'] = 'n';
$string['cliansweryes'] = 's';
$string['cliincorrectvalueerror'] = 'Erro: o valor "{$a->value}" não é permitido para a opção "{$a->option}"';
$string['cliincorrectvalueretry'] = 'Valor incorreto, por favor tente novamente';
$string['cliincorrectvalueretry'] = 'Valor incorreto, por favor, tente novamente';
$string['clitypevalue'] = 'introduza valor';
$string['clitypevaluedefault'] = 'introduza valor, pressione a tecla Enter para usar o valor predefinido ({$a})';
$string['cliunknowoption'] = 'Opções desconhecidas: {$a}. Por favor use a opção --help';
$string['cliunknowoption'] = 'Opções desconhecidas: {$a}. Por favor, use a opção --help';
$string['cliyesnoprompt'] = 'digite s (para sim) ou n (para não)';
$string['environmentrequireinstall'] = 'deve estar instalada e ativa';
$string['environmentrequireversion'] = 'é requerida a versão {$a->needed} e está a correr a versão {$a->current}';
+1
View File
@@ -54,6 +54,7 @@ $string['paths'] = 'Căi';
$string['pathserrcreatedataroot'] = 'Data directory ({$a->dataroot}) nu poate fi creat de către installer.';
$string['pathshead'] = 'Confirmare căi';
$string['pathsrodataroot'] = 'Directorul dataroot nu poate fi scris.';
$string['pathsroparentdataroot'] = 'Directorul parent ({$a->parent}) nu poate fi scris. Directorul data ({$a->dataroot}) nu poate fi creat de persoana care îl instalează.';
$string['pathssubdataroot'] = '<p>Un director unde Moodle va stoca tot conținutul unui fișier încărcat de către utilizatori.</p>
<p>Acest director trebuie poată fi citit și scris de către utilizatrii serverului web (de obicei \'www-data\', \'nobody\', or \'apache\').</p>
<p>Nu trebuie fie direct accesibil de pe web.</p>
+6
View File
@@ -30,9 +30,15 @@
defined('MOODLE_INTERNAL') || die();
$string['cannotcreatedboninstall'] = '<p> డేటాబేస్ను సృష్టించలేరు. </ p>
<p> పేర్కొన్న డేటాబేస్ ఉనికిలో లేదు మరియు ఇచ్చిన యూజర్కు డాటాబేస్ను సృష్టించడానికి అనుమతి లేదు. </ p>
<p> సైట్ నిర్వాహకుడు డేటాబేస్ కాన్ఫిగరేషన్ను ధృవీకరించాలి. </ p>';
$string['cannotcreatelangdir'] = 'భాష డైరెక్టరీని సృష్టించలేం';
$string['cannotcreatetempdir'] = 'టెంప్ డైరెక్టరీని సృష్టించలేం';
$string['cannotdownloadcomponents'] = 'కాంపొనెంట్లను దించుకోలేరు';
$string['cannotdownloadzipfile'] = 'ZIP దస్త్రాలను దించుకోలేరు';
$string['cannotfindcomponent'] = 'కాంపొనెంటు కనబడలేదు';
$string['cannotsavemd5file'] = 'Md5 ఫైల్ను సేవ్ చేయలేరు';
$string['cannotsavezipfile'] = 'జిప్ ఫైల్ను సేవ్ చేయడం సాధ్యపడదు';
$string['cannotunzipfile'] = 'ఫైల్ అన్జిప్ చేయలేరు';
$string['componentisuptodate'] = 'కాంపొనెంటు తాజాగా ఉంది';
+1
View File
@@ -39,3 +39,4 @@ $string['reseteveryonesprofile'] = 'Reset profile for all users';
$string['resetpage'] = 'Reset page to default';
$string['reseterror'] = 'There was an error resetting your page';
$string['privacy:metadata:core_my:preference:user_home_page_preference'] = 'The user home page preference configured for the Dashboard page.';
$string['unabletoaccess'] = 'As a security precaution, you may not access another user\'s dashboard';
+43 -6
View File
@@ -330,6 +330,7 @@ function get_role_definitions_uncached(array $roleids) {
$sql = "SELECT ctx.path, rc.roleid, rc.capability, rc.permission
FROM {role_capabilities} rc
JOIN {context} ctx ON rc.contextid = ctx.id
JOIN {capabilities} cap ON rc.capability = cap.name
WHERE rc.roleid $sql
ORDER BY ctx.path, rc.roleid, rc.capability";
$rs = $DB->get_recordset_sql($sql, $params);
@@ -1144,7 +1145,17 @@ function is_safe_capability($capability) {
*/
function get_local_override($roleid, $contextid, $capability) {
global $DB;
return $DB->get_record('role_capabilities', array('roleid'=>$roleid, 'capability'=>$capability, 'contextid'=>$contextid));
return $DB->get_record_sql("
SELECT rc.*
FROM {role_capabilities} rc
JOIN {capability} cap ON rc.capability = cap.name
WHERE rc.roleid = :roleid AND rc.capability = :capability AND rc.contextid = :contextid", [
'roleid' => $roleid,
'contextid' => $contextid,
'capability' => $capability,
]);
}
/**
@@ -1290,6 +1301,11 @@ function assign_capability($capability, $permission, $roleid, $contextid, $overw
$context = context::instance_by_id($contextid);
}
// Capability must exist.
if (!$capinfo = get_capability_info($capability)) {
throw new coding_exception("Capability '{$capability}' was not found! This has to be fixed in code.");
}
if (empty($permission) || $permission == CAP_INHERIT) { // if permission is not set
unassign_capability($capability, $roleid, $context->id);
return true;
@@ -1337,6 +1353,11 @@ function assign_capability($capability, $permission, $roleid, $contextid, $overw
function unassign_capability($capability, $roleid, $contextid = null) {
global $DB;
// Capability must exist.
if (!$capinfo = get_capability_info($capability)) {
throw new coding_exception("Capability '{$capability}' was not found! This has to be fixed in code.");
}
if (!empty($contextid)) {
if ($contextid instanceof context) {
$context = $contextid;
@@ -1391,6 +1412,7 @@ function get_roles_with_capability($capability, $permission = null, $context = n
FROM {role} r
WHERE r.id IN (SELECT rc.roleid
FROM {role_capabilities} rc
JOIN {capabilities} cap ON rc.capability = cap.name
WHERE rc.capability = :capname
$contextsql
$permissionsql)";
@@ -2177,6 +2199,9 @@ function update_capabilities($component = 'moodle') {
$DB->insert_record('capabilities', $capability, false);
// Flush the cached, as we have changed DB.
cache::make('core', 'capabilities')->delete('core_capabilities');
if (isset($capdef['clonepermissionsfrom']) && in_array($capdef['clonepermissionsfrom'], $existingcaps)){
if ($rolecapabilities = $DB->get_records('role_capabilities', array('capability'=>$capdef['clonepermissionsfrom']))){
foreach ($rolecapabilities as $rolecapability){
@@ -2230,9 +2255,6 @@ function capabilities_cleanup($component, $newcapdef = null) {
if (empty($newcapdef) ||
array_key_exists($cachedcap->name, $newcapdef) === false) {
// Remove from capabilities cache.
$DB->delete_records('capabilities', array('name'=>$cachedcap->name));
$removedcount++;
// Delete from roles.
if ($roles = get_roles_with_capability($cachedcap->name)) {
foreach($roles as $role) {
@@ -2241,6 +2263,13 @@ function capabilities_cleanup($component, $newcapdef = null) {
}
}
}
// Remove from role_capabilities for any old ones.
$DB->delete_records('role_capabilities', array('capability' => $cachedcap->name));
// Remove from capabilities cache.
$DB->delete_records('capabilities', array('name' => $cachedcap->name));
$removedcount++;
} // End if.
}
}
@@ -2305,10 +2334,12 @@ function role_context_capabilities($roleid, context $context, $cap = '') {
}
$sql = "SELECT rc.*
FROM {role_capabilities} rc, {context} c
FROM {role_capabilities} rc
JOIN {context} c ON rc.contextid = c.id
JOIN {capabilities} cap ON rc.capability = cap.name
WHERE rc.contextid in $contexts
AND rc.roleid = ?
AND rc.contextid = c.id $search
$search
ORDER BY c.contextlevel DESC, rc.capability DESC";
$capabilities = array();
@@ -3024,6 +3055,7 @@ function get_switchable_roles(context $context) {
SELECT r.id, r.name, r.shortname, rn.name AS coursealias
FROM (SELECT DISTINCT rc.roleid
FROM {role_capabilities} rc
$extrajoins
$extrawhere) idlist
JOIN {role} r ON r.id = idlist.roleid
@@ -3286,6 +3318,7 @@ function get_users_by_capability(context $context, $capability, $fields = '', $s
$params = array_merge($params, $params2);
$sql = "SELECT rc.id, rc.roleid, rc.permission, rc.capability, ctx.path
FROM {role_capabilities} rc
JOIN {capabilities} cap ON rc.capability = cap.name
JOIN {context} ctx on rc.contextid = ctx.id
WHERE rc.contextid $incontexts AND rc.capability $incaps";
@@ -4381,6 +4414,7 @@ function get_roles_with_cap_in_context($context, $capability) {
$sql = "SELECT rc.id, rc.roleid, rc.permission, ctx.depth
FROM {role_capabilities} rc
JOIN {context} ctx ON ctx.id = rc.contextid
JOIN {capabilities} cap ON rc.capability = cap.name
WHERE rc.capability = :cap AND ctx.id IN ($ctxids)
ORDER BY rc.roleid ASC, ctx.depth DESC";
$params = array('cap'=>$capability);
@@ -4500,6 +4534,7 @@ function prohibit_is_removable($roleid, context $context, $capability) {
$sql = "SELECT ctx.id
FROM {role_capabilities} rc
JOIN {context} ctx ON ctx.id = rc.contextid
JOIN {capabilities} cap ON rc.capability = cap.name
WHERE rc.roleid = :roleid AND rc.permission = :prohibit AND rc.capability = :cap AND ctx.id IN ($ctxids)
ORDER BY ctx.depth DESC";
@@ -4543,6 +4578,7 @@ function role_change_permission($roleid, $context, $capname, $permission) {
$sql = "SELECT ctx.id, rc.permission, ctx.depth
FROM {role_capabilities} rc
JOIN {context} ctx ON ctx.id = rc.contextid
JOIN {capabilities} cap ON rc.capability = cap.name
WHERE rc.roleid = :roleid AND rc.capability = :cap AND ctx.id IN ($ctxids)
ORDER BY ctx.depth DESC";
@@ -7224,6 +7260,7 @@ function get_with_capability_join(context $context, $capability, $useridcolumn)
$defs = array();
$sql = "SELECT rc.id, rc.roleid, rc.permission, ctx.path
FROM {role_capabilities} rc
JOIN {capabilities} cap ON rc.capability = cap.name
JOIN {context} ctx on rc.contextid = ctx.id
WHERE rc.contextid $incontexts AND rc.capability $incaps";
$rcs = $DB->get_records_sql($sql, array_merge($cparams, $capsparams));
+1 -1
View File
@@ -1 +1 @@
define(["core/config"],function(a){var b=function(b){this.storage=b,this.supported=this.detectSupport(),this.hashSource=a.wwwroot+"/"+a.jsrev,this.hash=this.hashString(this.hashSource),this.prefix=this.hash+"/",this.jsrevPrefix=this.hash+"/jsrev"};return b.prototype.detectSupport=function(){if(a.jsrev==-1)return!1;if("undefined"==typeof this.storage)return!1;var b="test";try{return null!==this.storage&&(this.storage.setItem(b,"1"),this.storage.removeItem(b),!0)}catch(c){return!1}},b.prototype.prefixKey=function(a){return this.prefix+a},b.prototype.validateCache=function(){var b=this.storage.getItem(this.jsrevPrefix);if(null===b)return void this.storage.setItem(this.jsrevPrefix,a.jsrev);var c=a.jsrev;c!=b&&(this.storage.clear(),this.storage.setItem(this.jsrevPrefix,a.jsrev))},b.prototype.hashString=function(a){var b,c,d,e=0;if(0===a.length)return e;for(b=0,d=a.length;b<d;b++)c=a.charCodeAt(b),e=(e<<5)-e+c,e|=0;return e},b.prototype.get=function(a){return!!this.supported&&(this.validateCache(),a=this.prefixKey(a),this.storage.getItem(a))},b.prototype.set=function(a,b){if(!this.supported)return!1;this.validateCache(),a=this.prefixKey(a);try{this.storage.setItem(a,b)}catch(c){return!1}return!0},b});
define(["core/config"],function(a){var b=function(b){this.storage=b,this.supported=this.detectSupport(),this.hashSource=a.wwwroot+"/"+a.jsrev,this.hash=this.hashString(this.hashSource),this.prefix=this.hash+"/",this.jsrevPrefix=this.hashString(a.wwwroot)+"/jsrev",this.validateCache()};return b.prototype.detectSupport=function(){if(a.jsrev==-1)return!1;if("undefined"==typeof this.storage)return!1;var b="test";try{return null!==this.storage&&(this.storage.setItem(b,"1"),this.storage.removeItem(b),!0)}catch(c){return!1}},b.prototype.prefixKey=function(a){return this.prefix+a},b.prototype.validateCache=function(){var b=this.storage.getItem(this.jsrevPrefix);if(null===b)return void this.storage.setItem(this.jsrevPrefix,a.jsrev);var c=a.jsrev;c!=b&&(this.storage.clear(),this.storage.setItem(this.jsrevPrefix,a.jsrev))},b.prototype.hashString=function(a){var b,c,d,e=0;if(0===a.length)return e;for(b=0,d=a.length;b<d;b++)c=a.charCodeAt(b),e=(e<<5)-e+c,e|=0;return e},b.prototype.get=function(a){return!!this.supported&&(a=this.prefixKey(a),this.storage.getItem(a))},b.prototype.set=function(a,b){if(!this.supported)return!1;a=this.prefixKey(a);try{this.storage.setItem(a,b)}catch(c){return!1}return!0},b});
+1 -1
View File
File diff suppressed because one or more lines are too long
+3 -4
View File
@@ -35,7 +35,8 @@ define(['core/config'], function(config) {
this.hashSource = config.wwwroot + '/' + config.jsrev;
this.hash = this.hashString(this.hashSource);
this.prefix = this.hash + '/';
this.jsrevPrefix = this.hash + '/jsrev';
this.jsrevPrefix = this.hashString(config.wwwroot) + '/jsrev';
this.validateCache();
};
/**
@@ -89,8 +90,8 @@ define(['core/config'], function(config) {
this.storage.setItem(this.jsrevPrefix, config.jsrev);
return;
}
var moodleVersion = config.jsrev;
var moodleVersion = config.jsrev;
if (moodleVersion != cacheVersion) {
this.storage.clear();
this.storage.setItem(this.jsrevPrefix, config.jsrev);
@@ -132,7 +133,6 @@ define(['core/config'], function(config) {
if (!this.supported) {
return false;
}
this.validateCache();
key = this.prefixKey(key);
return this.storage.getItem(key);
@@ -150,7 +150,6 @@ define(['core/config'], function(config) {
if (!this.supported) {
return false;
}
this.validateCache();
key = this.prefixKey(key);
// This can throw exceptions when the storage limit is reached.
try {
+9 -4
View File
@@ -656,9 +656,10 @@ define([
* @param {string} templateName - should consist of the component and the name of the template like this:
* core/menu (lib/templates/menu.mustache) or
* tool_bananas/yellow (admin/tool/bananas/templates/yellow.mustache)
* @param {Array} parentage - A list of requested partials in this render chain.
* @return {Promise} JQuery promise object resolved when all partials are in the cache.
*/
Renderer.prototype.cachePartials = function(templateName) {
Renderer.prototype.cachePartials = function(templateName, parentage) {
var searchKey = this.currentThemeName + '/' + templateName;
if (searchKey in cachePartialPromises) {
@@ -667,6 +668,9 @@ define([
// This promise will not be resolved until all child partials are also resolved and ready.
// We create it here to allow us to check for recursive inclusion of templates.
// Keep track of the requested partials in this chain.
parentage = parentage || [searchKey];
cachePartialPromises[searchKey] = $.Deferred();
this.getTemplate(templateName)
@@ -675,8 +679,8 @@ define([
var uniquePartials = partials.filter(function(partialName) {
// Check for recursion.
if (typeof cachePartialPromises[this.currentThemeName + '/' + partialName] !== 'undefined') {
// Ignore templates which include their parent.
if (parentage.indexOf(this.currentThemeName + '/' + partialName) >= 0) {
// Ignore templates which include a parent template already requested in the current chain.
return false;
}
@@ -686,7 +690,8 @@ define([
// Fetch any partial which has not already been fetched.
var fetchThemAll = uniquePartials.map(function(partialName) {
return this.cachePartials(partialName);
parentage.push(this.currentThemeName + '/' + partialName);
return this.cachePartials(partialName, parentage);
}.bind(this));
// Resolve the templateName promise when all of the children are resolved.
@@ -54,9 +54,8 @@ class delete_unconfirmed_users_task extends scheduled_task {
WHERE confirmed = 0 AND timecreated > 0
AND timecreated < ? AND deleted = 0", array($cuttime));
foreach ($rs as $user) {
delete_user($user); // We MUST delete user properly first.
$DB->delete_records('user', array('id' => $user->id)); // This is a bloody hack, but it might work.
mtrace(" Deleted unconfirmed user for ".fullname($user, true)." ($user->id)");
delete_user($user);
mtrace(" Deleted unconfirmed user ".fullname($user, true)." ($user->id)");
}
$rs->close();
}
+1
View File
@@ -1103,6 +1103,7 @@ $capabilities = array(
),
'moodle/course:managegroups' => array(
'riskbitmask' => RISK_XSS,
'captype' => 'write',
'contextlevel' => CONTEXT_COURSE,
+14 -7
View File
@@ -118,23 +118,30 @@ class provider implements
$user = $contextlist->get_user();
// Firstly export all autosave records from all contexts in the list owned by the given user.
list($contextsql, $contextparams) = $DB->get_in_or_equal($contextlist->get_contextids(), SQL_PARAMS_NAMED);
$contextparams['userid'] = $user->id;
$sql = "SELECT *
FROM {editor_atto_autosave}
WHERE userid = :userid AND contextid {$contextsql}";
list($contextsql, $contextparams) = $DB->get_in_or_equal($contextlist->get_contextids(), SQL_PARAMS_NAMED);
$contextparams['userid'] = $contextlist->get_user()->id;
$autosaves = $DB->get_recordset_sql($sql, $contextparams);
self::export_autosaves($user, $autosaves);
$sql = "SELECT *
FROM {editor_atto_autosave}
JOIN {context} c ON c.id = eas.contextid
WHERE c.id {$contextsql} AND contextlevel = :contextuser AND c.instanceid = :userid";
// Additionally export all eventual records in the given user's context regardless the actual owner.
// We still consider them to be the user's personal data even when edited by someone else.
list($contextsql, $contextparams) = $DB->get_in_or_equal($contextlist->get_contextids(), SQL_PARAMS_NAMED);
$contextparams['userid'] = $contextlist->get_user()->id;
$contextparams['userid'] = $user->id;
$contextparams['contextuser'] = CONTEXT_USER;
$sql = "SELECT eas.*
FROM {editor_atto_autosave} eas
JOIN {context} c ON c.id = eas.contextid
WHERE c.id {$contextsql} AND c.contextlevel = :contextuser AND c.instanceid = :userid";
$autosaves = $DB->get_recordset_sql($sql, $contextparams);
self::export_autosaves($user, $autosaves);
}
+9 -2
View File
@@ -1278,7 +1278,7 @@ function filter_phrases($text, $linkarray, $ignoretagsopen = null, $ignoretagscl
// for things that have already been matched on this page.
static $usedphrases = [];
$ignoretags = array(); // To store all the enclosig tags to be completely ignored.
$ignoretags = array(); // To store all the enclosing tags to be completely ignored.
$tags = array(); // To store all the simple tags to be ignored.
if (!$linkarrayalreadyprepared) {
@@ -1418,8 +1418,15 @@ function filter_prepare_phrases_for_filtering(array $linkarray) {
// Quote any regular expression characters and the delimiter in the work phrase to be searched.
$linkobject->workregexp = preg_quote($linkobject->workregexp, '/');
// If we ony want to match entire words then add \b assertions. However, only
// do this if the first or last thing in the phrase to match is a word character.
if ($linkobject->fullmatch) {
$linkobject->workregexp = '\b' . $linkobject->workregexp . '\b';
if (preg_match('~^\w~', $linkobject->workregexp)) {
$linkobject->workregexp = '\b' . $linkobject->workregexp;
}
if (preg_match('~\w$~', $linkobject->workregexp)) {
$linkobject->workregexp = $linkobject->workregexp . '\b';
}
}
$linkobject->workregexp = '/(' . $linkobject->workregexp . ')/s';
+6 -4
View File
@@ -6,10 +6,12 @@ Some changes from the upstream version have been made:
* Added context type property for Context class
* Set context type if 'context_type' parameter was submitted through POST
* Do not require tool_consumer_instance_guid
These changes can be reverted once the following pull requests/issues have been integrated upstream:
* https://github.com/IMSGlobal/LTI-Tool-Provider-Library-PHP/pull/10/commits/a9a1641f1a593eba4638133245c21d9ad47d8680
* https://github.com/IMSGlobal/LTI-Tool-Provider-Library-PHP/pull/11/commits/0bae60389bd020a02be5554516b86336e651e237
* https://github.com/IMSGlobal/LTI-Tool-Provider-Library-PHP/issues/19
* Prevent modification of the request to the provider
These changes can be reverted once the following pull requests have been integrated upstream:
* https://github.com/IMSGlobal/LTI-Tool-Provider-Library-PHP/pull/10
* https://github.com/IMSGlobal/LTI-Tool-Provider-Library-PHP/pull/11
* https://github.com/IMSGlobal/LTI-Tool-Provider-Library-PHP/pull/47
* https://github.com/IMSGlobal/LTI-Tool-Provider-Library-PHP/pull/48
It is recommended by upstream to install depdencies via composer - but the composer installation is bundled
with an autoloader so it's better to do it manually.
+4 -12
View File
@@ -17,7 +17,6 @@ class OAuthRequest {
// for debug purposes
public $base_string;
public static $version = '1.0';
public static $POST_INPUT = 'php://input';
function __construct($http_method, $http_url, $parameters = null) {
@@ -60,22 +59,15 @@ class OAuthRequest {
$parameters = array();
}
// It's a POST request of the proper content-type, so parse POST
// parameters and add those overriding any duplicates from GET
if ($http_method == "POST"
&& isset($request_headers['Content-Type'])
&& strstr($request_headers['Content-Type'], 'application/x-www-form-urlencoded')) {
$post_data = OAuthUtil::parse_parameters(file_get_contents(self::$POST_INPUT));
$parameters = array_merge($parameters, $post_data);
}
// We have a Authorization-header with OAuth data. Parse the header
// and add those overriding any duplicates from GET or POST
// We have a Authorization-header with OAuth data. Parse the header and add those.
if (isset($request_headers['Authorization']) && substr($request_headers['Authorization'], 0, 6) == 'OAuth ') {
$header_parameters = OAuthUtil::split_header($request_headers['Authorization']);
$parameters = array_merge($parameters, $header_parameters);
}
// If there are parameters in $_POST, these are likely what will be used. Therefore, they should be considered
// the final value in the case of any duplicates from sources parsed above.
$parameters = array_merge($parameters, $_POST);
}
return new OAuthRequest($http_method, $http_url, $parameters);
+1 -1
View File
@@ -2485,7 +2485,7 @@ class core_renderer extends renderer_base {
global $CFG, $DB;
$user = $userpicture->user;
$canviewfullnames = has_capability('moodle/site:viewfullnames', context_system::instance());
$canviewfullnames = has_capability('moodle/site:viewfullnames', $this->page->context);
if ($userpicture->alttext) {
if (!empty($user->imagealt)) {
+2 -2
View File
@@ -48,8 +48,8 @@ defined('MOODLE_INTERNAL') || die();
/**
* The reCAPTCHA URL's
*/
define('RECAPTCHA_API_URL', 'https://www.google.com/recaptcha/api.js');
define('RECAPTCHA_VERIFY_URL', 'https://www.google.com/recaptcha/api/siteverify');
define('RECAPTCHA_API_URL', 'https://www.recaptcha.net/recaptcha/api.js');
define('RECAPTCHA_VERIFY_URL', 'https://www.recaptcha.net/recaptcha/api/siteverify');
/**
* Returns the language code the reCAPTCHA element should use.
+2 -2
View File
@@ -1212,7 +1212,7 @@ class flexible_table {
* This function is not part of the public api.
*/
function print_headers() {
global $CFG, $OUTPUT;
global $CFG, $OUTPUT, $PAGE;
echo html_writer::start_tag('thead');
echo html_writer::start_tag('tr');
@@ -1234,7 +1234,7 @@ class flexible_table {
case 'fullname':
// Check the full name display for sortable fields.
if (has_capability('moodle/site:viewfullnames', context_system::instance())) {
if (has_capability('moodle/site:viewfullnames', $PAGE->context)) {
$nameformat = $CFG->alternativefullnameformat;
} else {
$nameformat = $CFG->fullnamedisplay;
+99 -4
View File
@@ -1696,6 +1696,101 @@ class core_accesslib_testcase extends advanced_testcase {
$this->assertFalse(has_all_capabilities($sca, $coursecontext, 0));
}
/**
* Test that assigning a fake cap does not return.
*/
public function test_fake_capability() {
global $DB;
$this->resetAfterTest();
$course = $this->getDataGenerator()->create_course();
$coursecontext = context_course::instance($course->id);
$teacherrole = $DB->get_record('role', array('shortname' => 'editingteacher'), '*', MUST_EXIST);
$teacher = $this->getDataGenerator()->create_user();
$fakecapname = 'moodle/fake:capability';
role_assign($teacherrole->id, $teacher->id, $coursecontext);
$admin = $DB->get_record('user', array('username' => 'admin'));
// Test a capability which does not exist.
// Note: Do not use assign_capability because it will not allow fake caps.
$DB->insert_record('role_capabilities', (object) [
'contextid' => $coursecontext->id,
'roleid' => $teacherrole->id,
'capability' => $fakecapname,
'permission' => CAP_ALLOW,
'timemodified' => time(),
'modifierid' => 0,
]);
// Check `has_capability`.
$this->assertFalse(has_capability($fakecapname, $coursecontext, $teacher));
$this->assertDebuggingCalled("Capability \"{$fakecapname}\" was not found! This has to be fixed in code.");
$this->assertFalse(has_capability($fakecapname, $coursecontext, $admin));
$this->assertDebuggingCalled("Capability \"{$fakecapname}\" was not found! This has to be fixed in code.");
// Check `get_with_capability_sql` (with uses `get_with_capability_join`).
list($sql, $params) = get_with_capability_sql($coursecontext, $fakecapname);
$users = $DB->get_records_sql($sql, $params);
$this->assertFalse(array_key_exists($teacher->id, $users));
$this->assertFalse(array_key_exists($admin->id, $users));
// Check `get_users_by_capability`.
$users = get_users_by_capability($coursecontext, $fakecapname);
$this->assertFalse(array_key_exists($teacher->id, $users));
$this->assertFalse(array_key_exists($admin->id, $users));
}
/**
* Test that assigning a fake cap does not return.
*/
public function test_fake_capability_assign() {
global $DB;
$this->resetAfterTest();
$course = $this->getDataGenerator()->create_course();
$coursecontext = context_course::instance($course->id);
$teacherrole = $DB->get_record('role', array('shortname' => 'editingteacher'), '*', MUST_EXIST);
$teacher = $this->getDataGenerator()->create_user();
$capability = 'moodle/fake:capability';
role_assign($teacherrole->id, $teacher->id, $coursecontext);
$admin = $DB->get_record('user', array('username' => 'admin'));
$this->expectException('coding_exception');
$this->expectExceptionMessage("Capability '{$capability}' was not found! This has to be fixed in code.");
assign_capability($capability, CAP_ALLOW, $teacherrole->id, $coursecontext);
}
/**
* Test that assigning a fake cap does not return.
*/
public function test_fake_capability_unassign() {
global $DB;
$this->resetAfterTest();
$course = $this->getDataGenerator()->create_course();
$coursecontext = context_course::instance($course->id);
$teacherrole = $DB->get_record('role', array('shortname' => 'editingteacher'), '*', MUST_EXIST);
$teacher = $this->getDataGenerator()->create_user();
$capability = 'moodle/fake:capability';
role_assign($teacherrole->id, $teacher->id, $coursecontext);
$admin = $DB->get_record('user', array('username' => 'admin'));
$this->expectException('coding_exception');
$this->expectExceptionMessage("Capability '{$capability}' was not found! This has to be fixed in code.");
unassign_capability($capability, CAP_ALLOW, $teacherrole->id, $coursecontext);
}
/**
* Test that the caching in get_role_definitions() and get_role_definitions_uncached()
* works as intended.
@@ -2815,7 +2910,7 @@ class core_accesslib_testcase extends advanced_testcase {
assign_capability('moodle/site:accessallgroups', CAP_ALLOW, $CFG->defaultfrontpageroleid, $frontpagepagecontext, true);
assign_capability('mod/page:view', CAP_PREVENT, $allroles['guest'], $frontpagepagecontext, true);
assign_capability('mod/page:view', CAP_ALLOW, $allroles['user'], $frontpagepagecontext, true);
assign_capability('moodle/page:view', CAP_ALLOW, $allroles['student'], $frontpagepagecontext, true);
assign_capability('mod/page:view', CAP_ALLOW, $allroles['student'], $frontpagepagecontext, true);
assign_capability('moodle/site:accessallgroups', CAP_ALLOW, $CFG->defaultuserroleid, $frontpagecontext, true);
assign_capability('moodle/site:accessallgroups', CAP_ALLOW, $CFG->defaultfrontpageroleid, $frontpagecontext, true);
@@ -3443,10 +3538,10 @@ class core_accesslib_testcase extends advanced_testcase {
$this->assertFalse(array_key_exists($guest->id, $users));
// Test role override.
assign_capability('moodle/site:backupcourse', CAP_PROHIBIT, $teacherrole->id, $coursecontext, true);
assign_capability('moodle/site:backupcourse', CAP_ALLOW, $studentrole->id, $coursecontext, true);
assign_capability('moodle/backup:backupcourse', CAP_PROHIBIT, $teacherrole->id, $coursecontext, true);
assign_capability('moodle/backup:backupcourse', CAP_ALLOW, $studentrole->id, $coursecontext, true);
list($sql, $params) = get_with_capability_sql($coursecontext, 'moodle/site:backupcourse');
list($sql, $params) = get_with_capability_sql($coursecontext, 'moodle/backup:backupcourse');
$users = $DB->get_records_sql($sql, $params);
$this->assertFalse(array_key_exists($teacher->id, $users));
+48 -23
View File
@@ -32,36 +32,61 @@ defined('MOODLE_INTERNAL') || die();
class core_environment_testcase extends advanced_testcase {
/**
* Test the environment.
* Test the environment check status.
*/
public function test_environment() {
public function test_environment_check_status() {
global $CFG;
require_once($CFG->libdir.'/environmentlib.php');
list($envstatus, $environment_results) = check_moodle_environment(normalize_version($CFG->release), ENV_SELECT_RELEASE);
$results = check_moodle_environment(normalize_version($CFG->release), ENV_SELECT_RELEASE);
// The first element of the results array contains the environment check status.
$status = reset($results);
$this->assertTrue($status);
}
/**
* Data provider for Moodle environment check tests.
*
* @return array
*/
public function environment_provider() {
global $CFG;
require_once($CFG->libdir.'/environmentlib.php');
$results = check_moodle_environment(normalize_version($CFG->release), ENV_SELECT_RELEASE);
// The second element of the results array contains the list of environment results.
$environmentresults = end($results);
return array_map(function($result) {
return [$result];
}, $environmentresults);
}
/**
* Test the environment.
*
* @dataProvider environment_provider
* @param environment_results $result
*/
public function test_environment($result) {
$sslmessages = ['ssl/tls configuration not supported', 'invalid ssl/tls configuration'];
$this->assertNotEmpty($envstatus);
foreach ($environment_results as $environment_result) {
if ($environment_result->part === 'php_setting'
and $environment_result->info === 'opcache.enable'
and $environment_result->getLevel() === 'optional'
and $environment_result->getStatus() === false
) {
$this->markTestSkipped('OPCache extension is not necessary for unit testing.');
continue;
}
if ($environment_result->part === 'custom_check'
and in_array($environment_result->info, $sslmessages)
and $environment_result->getLevel() === 'optional'
and $environment_result->getStatus() === false
) {
$this->markTestSkipped('Up-to-date TLS libraries are not necessary for unit testing.');
continue;
}
$this->assertTrue($environment_result->getStatus(), "Problem detected in environment ($environment_result->part:$environment_result->info), fix all warnings and errors!");
if ($result->part === 'php_setting'
&& $result->info === 'opcache.enable'
&& $result->getLevel() === 'optional'
&& $result->getStatus() === false) {
$this->markTestSkipped('OPCache extension is not necessary for unit testing.');
}
if ($result->part === 'custom_check'
&& $result->getLevel() === 'optional'
&& $result->getStatus() === false) {
if (in_array($result->info, $sslmessages)) {
$this->markTestSkipped('Up-to-date TLS libraries are not necessary for unit testing.');
}
}
$info = "{$result->part}:{$result->info}";
$this->assertTrue($result->getStatus(), "Problem detected in environment ($info), fix all warnings and errors!");
}
/**
+7
View File
@@ -1,6 +1,13 @@
This files describes API changes in core libraries and APIs,
information provided here is intended especially for developers.
=== 3.4.6 ===
* The \core_rating provider's get_sql_join function now accepts an optional $innerjoin parameter.
It is recommended that privacy providers using this function call rewrite any long query into a number of separate
calls to add_from_sql for improved performance, and that the new argument is used.
This will allow queries to remain backwards-compatible with older versions of Moodle but will have significantly better performance in version supporting the innerjoin parameter.
=== 3.4.5 ===
* Custom AJAX handlers for the form autocomplete fields can now optionally return string in their processResults()
+3 -3
View File
@@ -31,7 +31,6 @@ redirect_if_major_upgrade_required();
$testsession = optional_param('testsession', 0, PARAM_INT); // test session works properly
$anchor = optional_param('anchor', '', PARAM_RAW); // Used to restore hash anchor to wantsurl.
$logintoken = optional_param('logintoken', '', PARAM_RAW); // Used to validate the request.
$context = context_system::instance();
$PAGE->set_url("$CFG->wwwroot/login/index.php");
@@ -67,13 +66,13 @@ if (!empty($SESSION->has_timed_out)) {
$session_has_timed_out = false;
}
/// auth plugins may override these - SSO anyone?
$frm = false;
$user = false;
$authsequence = get_enabled_auth_plugins(true); // auths, in sequence
foreach($authsequence as $authname) {
$authplugin = get_auth_plugin($authname);
// The auth plugin's loginpage_hook() can eventually set $frm and/or $user.
$authplugin->loginpage_hook();
}
@@ -132,12 +131,13 @@ if ($frm and isset($frm->username)) { // Login WITH
}
if ($user) {
//user already supplied by aut plugin prelogin hook
// The auth plugin has already provided the user via the loginpage_hook() called above.
} else if (($frm->username == 'guest') and empty($CFG->guestloginbutton)) {
$user = false; /// Can't log in as guest if guest button is disabled
$frm = false;
} else {
if (empty($errormsg)) {
$logintoken = isset($frm->logintoken) ? $frm->logintoken : '';
$user = authenticate_user_login($frm->username, $frm->password, false, $errorcode, $logintoken);
}
}
+2 -1
View File
@@ -179,7 +179,8 @@ class helper {
}
}
$data->isonline = null;
if (self::show_online_status($userfields)) {
$user = \core_user::get_user($data->userid);
if (self::show_online_status($user)) {
$data->isonline = self::is_online($userfields->lastaccess);
}
$data->isblocked = isset($contact->blocked) ? (bool) $contact->blocked : false;
+44 -16
View File
@@ -103,29 +103,57 @@ class provider implements
* @return contextlist the list of contexts containing user info for the user.
*/
public static function get_contexts_for_userid(int $userid) : contextlist {
// Fetch all data records.
$contextlist = new contextlist();
// Fetch all data records that the user rote.
$sql = "SELECT c.id
FROM {context} c
INNER JOIN {course_modules} cm ON cm.id = c.instanceid AND c.contextlevel = :contextlevel
INNER JOIN {modules} m ON m.id = cm.module AND m.name = :modname
INNER JOIN {data} d ON d.id = cm.instance
INNER JOIN {data_records} dr ON dr.dataid = d.id
LEFT JOIN {comments} com ON com.commentarea=:commentarea and com.itemid = dr.id AND com.userid = :userid1
LEFT JOIN {rating} r ON r.contextid = c.id AND r.itemid = dr.id AND r.component = :moddata
AND r.ratingarea = :ratingarea AND r.userid = :userid2
WHERE dr.userid = :userid OR com.id IS NOT NULL OR r.id IS NOT NULL";
JOIN {course_modules} cm ON cm.id = c.instanceid AND c.contextlevel = :contextlevel
JOIN {modules} m ON m.id = cm.module AND m.name = :modname
JOIN {data} d ON d.id = cm.instance
JOIN {data_records} dr ON dr.dataid = d.id
WHERE dr.userid = :userid";
$params = [
'modname' => 'data',
'contextlevel' => CONTEXT_MODULE,
'modname' => 'data',
'userid' => $userid,
'userid1' => $userid,
'userid2' => $userid,
'commentarea' => 'database_entry',
'moddata' => 'mod_data',
'ratingarea' => 'entry',
];
$contextlist = new contextlist();
$contextlist->add_from_sql($sql, $params);
// Fetch contexts where the user commented.
$sql = "SELECT c.id
FROM {context} c
JOIN {course_modules} cm ON cm.id = c.instanceid AND c.contextlevel = :contextlevel
JOIN {modules} m ON m.id = cm.module AND m.name = :modname
JOIN {data} d ON d.id = cm.instance
JOIN {data_records} dr ON dr.dataid = d.id
JOIN {comments} com ON com.commentarea = :commentarea and com.itemid = dr.id
WHERE com.userid = :userid";
$params = [
'contextlevel' => CONTEXT_MODULE,
'modname' => 'data',
'commentarea' => 'database_entry',
'userid' => $userid,
];
$contextlist->add_from_sql($sql, $params);
// Fetch all data records.
$ratingquery = \core_rating\privacy\provider::get_sql_join('r', 'mod_data', 'entry', 'dr.id', $userid, true);
$sql = "SELECT c.id
FROM {context} c
JOIN {course_modules} cm ON cm.id = c.instanceid AND c.contextlevel = :contextlevel
JOIN {modules} m ON m.id = cm.module AND m.name = :modname
JOIN {data} d ON d.id = cm.instance
JOIN {data_records} dr ON dr.dataid = d.id
{$ratingquery->join}
WHERE {$ratingquery->userwhere}";
$params = [
'contextlevel' => CONTEXT_MODULE,
'modname' => 'data',
] + $ratingquery->params;
$contextlist->add_from_sql($sql, $params);
return $contextlist;
+1 -1
View File
@@ -274,7 +274,7 @@ class mod_data_external_testcase extends externallib_advanced_testcase {
public function test_view_database_no_capabilities() {
// Test user with no capabilities.
// We need a explicit prohibit since this capability is allowed for students by default.
assign_capability('mod/data:viewpage', CAP_PROHIBIT, $this->studentrole->id, $this->context->id);
assign_capability('mod/data:view', CAP_PROHIBIT, $this->studentrole->id, $this->context->id);
accesslib_clear_all_caches_for_unit_testing();
$this->expectException('moodle_exception');
+162 -56
View File
@@ -152,46 +152,105 @@ class provider implements
* @return contextlist $contextlist The contextlist containing the list of contexts used in this plugin.
*/
public static function get_contexts_for_userid(int $userid) : \core_privacy\local\request\contextlist {
$ratingsql = \core_rating\privacy\provider::get_sql_join('rat', 'mod_forum', 'post', 'p.id', $userid);
// Fetch all forum discussions, and forum posts.
$contextlist = new \core_privacy\local\request\contextlist();
$params = [
'modname' => 'forum',
'contextlevel' => CONTEXT_MODULE,
'userid' => $userid,
];
// Discussion creators.
$sql = "SELECT c.id
FROM {context} c
JOIN {course_modules} cm ON cm.id = c.instanceid AND c.contextlevel = :contextlevel
JOIN {modules} m ON m.id = cm.module AND m.name = :modname
JOIN {forum} f ON f.id = cm.instance
LEFT JOIN {forum_discussions} d ON d.forum = f.id
LEFT JOIN {forum_posts} p ON p.discussion = d.id
LEFT JOIN {forum_digests} dig ON dig.forum = f.id AND dig.userid = :digestuserid
LEFT JOIN {forum_subscriptions} sub ON sub.forum = f.id AND sub.userid = :subuserid
LEFT JOIN {forum_track_prefs} pref ON pref.forumid = f.id AND pref.userid = :prefuserid
LEFT JOIN {forum_read} hasread ON hasread.forumid = f.id AND hasread.userid = :hasreaduserid
LEFT JOIN {forum_discussion_subs} dsub ON dsub.forum = f.id AND dsub.userid = :dsubuserid
{$ratingsql->join}
WHERE (
p.userid = :postuserid OR
d.userid = :discussionuserid OR
dig.id IS NOT NULL OR
sub.id IS NOT NULL OR
pref.id IS NOT NULL OR
hasread.id IS NOT NULL OR
dsub.id IS NOT NULL OR
{$ratingsql->userwhere}
)
JOIN {forum_discussions} d ON d.forum = f.id
WHERE d.userid = :userid
";
$params = [
'modname' => 'forum',
'contextlevel' => CONTEXT_MODULE,
'postuserid' => $userid,
'discussionuserid' => $userid,
'digestuserid' => $userid,
'subuserid' => $userid,
'prefuserid' => $userid,
'hasreaduserid' => $userid,
'dsubuserid' => $userid,
];
$params += $ratingsql->params;
$contextlist->add_from_sql($sql, $params);
$contextlist = new \core_privacy\local\request\contextlist();
// Post authors.
$sql = "SELECT c.id
FROM {context} c
JOIN {course_modules} cm ON cm.id = c.instanceid AND c.contextlevel = :contextlevel
JOIN {modules} m ON m.id = cm.module AND m.name = :modname
JOIN {forum} f ON f.id = cm.instance
JOIN {forum_discussions} d ON d.forum = f.id
JOIN {forum_posts} p ON p.discussion = d.id
WHERE p.userid = :userid
";
$contextlist->add_from_sql($sql, $params);
// Forum digest records.
$sql = "SELECT c.id
FROM {context} c
JOIN {course_modules} cm ON cm.id = c.instanceid AND c.contextlevel = :contextlevel
JOIN {modules} m ON m.id = cm.module AND m.name = :modname
JOIN {forum} f ON f.id = cm.instance
JOIN {forum_digests} dig ON dig.forum = f.id
WHERE dig.userid = :userid
";
$contextlist->add_from_sql($sql, $params);
// Forum subscriptions.
$sql = "SELECT c.id
FROM {context} c
JOIN {course_modules} cm ON cm.id = c.instanceid AND c.contextlevel = :contextlevel
JOIN {modules} m ON m.id = cm.module AND m.name = :modname
JOIN {forum} f ON f.id = cm.instance
JOIN {forum_subscriptions} sub ON sub.forum = f.id
WHERE sub.userid = :userid
";
$contextlist->add_from_sql($sql, $params);
// Discussion subscriptions.
$sql = "SELECT c.id
FROM {context} c
JOIN {course_modules} cm ON cm.id = c.instanceid AND c.contextlevel = :contextlevel
JOIN {modules} m ON m.id = cm.module AND m.name = :modname
JOIN {forum} f ON f.id = cm.instance
JOIN {forum_discussion_subs} dsub ON dsub.forum = f.id
WHERE dsub.userid = :userid
";
$contextlist->add_from_sql($sql, $params);
// Discussion tracking preferences.
$sql = "SELECT c.id
FROM {context} c
JOIN {course_modules} cm ON cm.id = c.instanceid AND c.contextlevel = :contextlevel
JOIN {modules} m ON m.id = cm.module AND m.name = :modname
JOIN {forum} f ON f.id = cm.instance
JOIN {forum_track_prefs} pref ON pref.forumid = f.id
WHERE pref.userid = :userid
";
$contextlist->add_from_sql($sql, $params);
// Discussion read records.
$sql = "SELECT c.id
FROM {context} c
JOIN {course_modules} cm ON cm.id = c.instanceid AND c.contextlevel = :contextlevel
JOIN {modules} m ON m.id = cm.module AND m.name = :modname
JOIN {forum} f ON f.id = cm.instance
JOIN {forum_read} hasread ON hasread.forumid = f.id
WHERE hasread.userid = :userid
";
$contextlist->add_from_sql($sql, $params);
// Rating authors.
$ratingsql = \core_rating\privacy\provider::get_sql_join('rat', 'mod_forum', 'post', 'p.id', $userid, true);
$sql = "SELECT c.id
FROM {context} c
JOIN {course_modules} cm ON cm.id = c.instanceid AND c.contextlevel = :contextlevel
JOIN {modules} m ON m.id = cm.module AND m.name = :modname
JOIN {forum} f ON f.id = cm.instance
JOIN {forum_discussions} d ON d.forum = f.id
JOIN {forum_posts} p ON p.discussion = d.id
{$ratingsql->join}
WHERE {$ratingsql->userwhere}
";
$params += $ratingsql->params;
$contextlist->add_from_sql($sql, $params);
return $contextlist;
@@ -365,30 +424,68 @@ class provider implements
$userid = $user->id;
list($contextsql, $contextparams) = $DB->get_in_or_equal($contextlist->get_contextids(), SQL_PARAMS_NAMED);
$params = $contextparams;
// Digested forums.
$sql = "SELECT
c.id AS contextid,
dig.maildigest AS maildigest
FROM {context} c
JOIN {course_modules} cm ON cm.id = c.instanceid
JOIN {forum} f ON f.id = cm.instance
JOIN {forum_digests} dig ON dig.forum = f.id
WHERE (
dig.userid = :userid AND
c.id {$contextsql}
)
";
$params['userid'] = $userid;
$digests = $DB->get_records_sql_menu($sql, $params);
// Forum subscriptions.
$sql = "SELECT
c.id AS contextid,
sub.userid AS subscribed
FROM {context} c
JOIN {course_modules} cm ON cm.id = c.instanceid
JOIN {forum} f ON f.id = cm.instance
JOIN {forum_subscriptions} sub ON sub.forum = f.id
WHERE (
sub.userid = :userid AND
c.id {$contextsql}
)
";
$params['userid'] = $userid;
$subscriptions = $DB->get_records_sql_menu($sql, $params);
// Tracked forums.
$sql = "SELECT
c.id AS contextid,
f.*,
cm.id AS cmid,
dig.maildigest,
sub.userid AS subscribed,
pref.userid AS tracked
FROM {context} c
JOIN {course_modules} cm ON cm.id = c.instanceid
JOIN {forum} f ON f.id = cm.instance
LEFT JOIN {forum_digests} dig ON dig.forum = f.id AND dig.userid = :digestuserid
LEFT JOIN {forum_subscriptions} sub ON sub.forum = f.id AND sub.userid = :subuserid
LEFT JOIN {forum_track_prefs} pref ON pref.forumid = f.id AND pref.userid = :prefuserid
JOIN {forum_track_prefs} pref ON pref.forumid = f.id
WHERE (
pref.userid = :userid AND
c.id {$contextsql}
)
";
$params['userid'] = $userid;
$tracked = $DB->get_records_sql_menu($sql, $params);
$sql = "SELECT
c.id AS contextid,
f.*,
cm.id AS cmid
FROM {context} c
JOIN {course_modules} cm ON cm.id = c.instanceid
JOIN {forum} f ON f.id = cm.instance
WHERE (
c.id {$contextsql}
)
";
$params = [
'digestuserid' => $userid,
'subuserid' => $userid,
'prefuserid' => $userid,
];
$params += $contextparams;
// Keep a mapping of forumid to contextid.
@@ -407,9 +504,15 @@ class provider implements
request_helper::export_context_files($context, $user);
// Store relevant metadata about this forum instance.
static::export_digest_data($userid, $forum);
static::export_subscription_data($userid, $forum);
static::export_tracking_data($userid, $forum);
if (isset($digests[$forum->contextid])) {
static::export_digest_data($userid, $forum, $digests[$forum->contextid]);
}
if (isset($subscriptions[$forum->contextid])) {
static::export_subscription_data($userid, $forum, $subscriptions[$forum->contextid]);
}
if (isset($tracked[$forum->contextid])) {
static::export_tracking_data($userid, $forum, $tracked[$forum->contextid]);
}
}
$forums->close();
@@ -704,16 +807,17 @@ class provider implements
*
* @param int $userid The userid of the user whose data is to be exported.
* @param \stdClass $forum The forum whose data is being exported.
* @param int $maildigest The mail digest setting for this forum.
* @return bool Whether any data was stored.
*/
protected static function export_digest_data(int $userid, \stdClass $forum) {
if (null !== $forum->maildigest) {
protected static function export_digest_data(int $userid, \stdClass $forum, int $maildigest) {
if (null !== $maildigest) {
// The user has a specific maildigest preference for this forum.
$a = (object) [
'forum' => format_string($forum->name, true),
];
switch ($forum->maildigest) {
switch ($maildigest) {
case 0:
$a->type = get_string('emaildigestoffshort', 'mod_forum');
break;
@@ -726,7 +830,7 @@ class provider implements
}
writer::with_context(\context_module::instance($forum->cmid))
->export_metadata([], 'digestpreference', $forum->maildigest,
->export_metadata([], 'digestpreference', $maildigest,
get_string('privacy:digesttypepreference', 'mod_forum', $a));
return true;
@@ -740,10 +844,11 @@ class provider implements
*
* @param int $userid The userid of the user whose data is to be exported.
* @param \stdClass $forum The forum whose data is being exported.
* @param int $subscribed if the user is subscribed
* @return bool Whether any data was stored.
*/
protected static function export_subscription_data(int $userid, \stdClass $forum) {
if (null !== $forum->subscribed) {
protected static function export_subscription_data(int $userid, \stdClass $forum, int $subscribed) {
if (null !== $subscribed) {
// The user is subscribed to this forum.
writer::with_context(\context_module::instance($forum->cmid))
->export_metadata([], 'subscriptionpreference', 1, get_string('privacy:subscribedtoforum', 'mod_forum'));
@@ -798,10 +903,11 @@ class provider implements
*
* @param int $userid The userid of the user whose data is to be exported.
* @param \stdClass $forum The forum whose data is being exported.
* @param int $tracke if the user is subscribed
* @return bool Whether any data was stored.
*/
protected static function export_tracking_data(int $userid, \stdClass $forum) {
if (null !== $forum->tracked) {
protected static function export_tracking_data(int $userid, \stdClass $forum, int $tracked) {
if (null !== $tracked) {
// The user has a main preference to track all forums, but has opted out of this one.
writer::with_context(\context_module::instance($forum->cmid))
->export_metadata([], 'trackreadpreference', 0, get_string('privacy:readtrackingdisabled', 'mod_forum'));
+8 -5
View File
@@ -658,6 +658,12 @@ function forum_cron() {
}
}
$coursecontext = context_course::instance($course->id);
if (!$course->visible and !has_capability('moodle/course:viewhiddencourses', $coursecontext, $userto->id)) {
// The course is hidden and the user does not have access to it.
continue;
}
// Don't send email if the forum is Q&A and the user has not posted.
// Initial topics are still mailed.
if ($forum->type == 'qanda' && !forum_get_user_posted_time($discussion->id, $userto->id) && $pid != $discussion->firstpost) {
@@ -3919,7 +3925,7 @@ function forum_print_discussion_header(&$post, $forum, $group = -1, $datestring
}
echo '<td class="lastpost">';
$usedate = (empty($post->created)) ? $post->timemodified : $post->created;
$usedate = (empty($post->timemodified)) ? $post->created : $post->timemodified;
$parenturl = '';
$usermodified = new stdClass();
$usermodified->id = $post->usermodified;
@@ -4552,10 +4558,6 @@ function forum_update_post($newpost, $mform, $unused = null) {
}
$post->modified = time();
// Last post modified tracking.
$discussion->timemodified = $post->modified;
$discussion->usermodified = $post->userid;
if (!$post->parent) { // Post is a discussion starter - update discussion title and times too
$discussion->name = $post->subject;
$discussion->timestart = $post->timestart;
@@ -4568,6 +4570,7 @@ function forum_update_post($newpost, $mform, $unused = null) {
$post->message = file_save_draft_area_files($newpost->itemid, $context->id, 'mod_forum', 'post', $post->id,
mod_forum_post_form::editor_options($context, $post->id), $post->message);
$DB->update_record('forum_posts', $post);
// Note: Discussion modified time/user are intentionally not updated, to enable them to track the latest new post.
$DB->update_record('forum_discussions', $discussion);
forum_add_attachment($post, $forum, $cm, $mform);
+1 -1
View File
@@ -574,7 +574,7 @@ file_prepare_draft_area($draftitemid, $modcontext->id, 'mod_forum', 'attachment'
if ($USER->id != $post->userid) { // Not the original author, so add a message to the end
$data = new stdClass();
$data->date = userdate($post->modified);
$data->date = userdate($post->created);
if ($post->messageformat == FORMAT_HTML) {
$data->name = '<a href="'.$CFG->wwwroot.'/user/view.php?id='.$USER->id.'&course='.$post->course.'">'.
fullname($USER).'</a>';
@@ -71,14 +71,14 @@ Feature: New discussions and discussions with recently added replies are display
And I am on "Course 1" course homepage
And I follow "Course general forum"
#
# Make sure the order of the forum posts is as expected (most recently participated first).
# Make sure the order of the forum posts is as expected, with most recent new participation first (ie excluding edits).
#
Then I should see "Forum post 3" in the "//tr[contains(concat(' ', normalize-space(@class), ' '), ' discussion ')][position()=3]" "xpath_element"
And I should see "Edited forum post 2" in the "//tr[contains(concat(' ', normalize-space(@class), ' '), ' discussion ')][position()=2]" "xpath_element"
And I should see "Forum post 1" in the "//tr[contains(concat(' ', normalize-space(@class), ' '), ' discussion ')][position()=1]" "xpath_element"
Then I should see "Forum post 1" in the "//tr[contains(concat(' ', normalize-space(@class), ' '), ' discussion ')][position()=1]" "xpath_element"
And I should see "Forum post 3" in the "//tr[contains(concat(' ', normalize-space(@class), ' '), ' discussion ')][position()=2]" "xpath_element"
And I should see "Edited forum post 2" in the "//tr[contains(concat(' ', normalize-space(@class), ' '), ' discussion ')][position()=3]" "xpath_element"
#
# Make sure the next/prev navigation uses the same order of the posts.
#
And I follow "Edited forum post 2"
And I follow "Forum post 3"
And "//a[@aria-label='Next discussion: Forum post 1']" "xpath_element" should exist
And "//a[@aria-label='Previous discussion: Forum post 3']" "xpath_element" should exist
And "//a[@aria-label='Previous discussion: Edited forum post 2']" "xpath_element" should exist
+42 -1
View File
@@ -3298,6 +3298,17 @@ class mod_forum_lib_testcase extends advanced_testcase {
// On this freshly created discussion, the teacher is the author of the last post.
$this->assertEquals($teacher->id, $DB->get_field('forum_discussions', 'usermodified', ['id' => $discussion->id]));
// Fetch modified timestamp of the discussion.
$discussionmodified = $DB->get_field('forum_discussions', 'timemodified', ['id' => $discussion->id]);
$pasttime = $discussionmodified - 3600;
// Adjust the discussion modified timestamp back an hour, so it's in the past.
$adjustment = (object)[
'id' => $discussion->id,
'timemodified' => $pasttime,
];
$DB->update_record('forum_discussions', $adjustment);
// Let the student reply to the teacher's post.
$reply = $generator->create_post((object)[
'course' => $course->id,
@@ -3310,6 +3321,30 @@ class mod_forum_lib_testcase extends advanced_testcase {
// The student should now be the last post's author.
$this->assertEquals($student->id, $DB->get_field('forum_discussions', 'usermodified', ['id' => $discussion->id]));
// Fetch modified timestamp of the discussion and student's post.
$discussionmodified = $DB->get_field('forum_discussions', 'timemodified', ['id' => $discussion->id]);
$postmodified = $DB->get_field('forum_posts', 'modified', ['id' => $reply->id]);
// Discussion modified time should be updated to be equal to the newly created post's time.
$this->assertEquals($discussionmodified, $postmodified);
// Adjust the discussion and post timestamps, so they are in the past.
$adjustment = (object)[
'id' => $discussion->id,
'timemodified' => $pasttime,
];
$DB->update_record('forum_discussions', $adjustment);
$adjustment = (object)[
'id' => $reply->id,
'modified' => $pasttime,
];
$DB->update_record('forum_posts', $adjustment);
// The discussion and student's post time should now be an hour in the past.
$this->assertEquals($pasttime, $DB->get_field('forum_discussions', 'timemodified', ['id' => $discussion->id]));
$this->assertEquals($pasttime, $DB->get_field('forum_posts', 'modified', ['id' => $reply->id]));
// Let the teacher edit the student's reply.
$this->setUser($teacher->id);
$newpost = (object)[
@@ -3319,8 +3354,14 @@ class mod_forum_lib_testcase extends advanced_testcase {
];
forum_update_post($newpost, null);
// The student should be still the last post's author.
// The student should still be the last post's author.
$this->assertEquals($student->id, $DB->get_field('forum_discussions', 'usermodified', ['id' => $discussion->id]));
// The discussion modified time should not have changed.
$this->assertEquals($pasttime, $DB->get_field('forum_discussions', 'timemodified', ['id' => $discussion->id]));
// The post time should be updated.
$this->assertGreaterThan($pasttime, $DB->get_field('forum_posts', 'modified', ['id' => $reply->id]));
}
public function test_forum_core_calendar_provide_event_action() {
+40 -11
View File
@@ -80,26 +80,55 @@ class provider implements
* @return contextlist the list of contexts containing user info for the user.
*/
public static function get_contexts_for_userid(int $userid) : contextlist {
$ratingquery = \core_rating\privacy\provider::get_sql_join('r', 'mod_glossary', 'entry', 'ge.id', $userid);
$contextlist = new contextlist();
// Glossary entries.
$sql = "SELECT c.id
FROM {context} c
INNER JOIN {course_modules} cm ON cm.id = c.instanceid AND c.contextlevel = :contextlevel
INNER JOIN {modules} m ON m.id = cm.module AND m.name = :modname
INNER JOIN {glossary} g ON g.id = cm.instance
INNER JOIN {glossary_entries} ge ON ge.glossaryid = g.id
LEFT JOIN {comments} com ON com.commentarea =:commentarea AND com.itemid = ge.id
{$ratingquery->join}
WHERE ge.userid = :glossaryentryuserid OR com.userid = :commentuserid OR {$ratingquery->userwhere}";
JOIN {course_modules} cm ON cm.id = c.instanceid AND c.contextlevel = :contextlevel
JOIN {modules} m ON m.id = cm.module AND m.name = :modname
JOIN {glossary} g ON g.id = cm.instance
JOIN {glossary_entries} ge ON ge.glossaryid = g.id
WHERE ge.userid = :glossaryentryuserid";
$params = [
'contextlevel' => CONTEXT_MODULE,
'modname' => 'glossary',
'commentarea' => 'glossary_entry',
'glossaryentryuserid' => $userid,
'commentuserid' => $userid,
] + $ratingquery->params;
];
$contextlist->add_from_sql($sql, $params);
$contextlist = new contextlist();
// Where the user has rated something.
$ratingquery = \core_rating\privacy\provider::get_sql_join('r', 'mod_glossary', 'entry', 'ge.id', $userid, true);
$sql = "SELECT c.id
FROM {context} c
JOIN {course_modules} cm ON cm.id = c.instanceid AND c.contextlevel = :contextlevel
JOIN {modules} m ON m.id = cm.module AND m.name = :modname
JOIN {glossary} g ON g.id = cm.instance
JOIN {glossary_entries} ge ON ge.glossaryid = g.id
{$ratingquery->join}
WHERE {$ratingquery->userwhere}";
$params = [
'contextlevel' => CONTEXT_MODULE,
'modname' => 'glossary',
] + $ratingquery->params;
$contextlist->add_from_sql($sql, $params);
// Comments.
$sql = "SELECT c.id
FROM {context} c
JOIN {course_modules} cm ON cm.id = c.instanceid AND c.contextlevel = :contextlevel
JOIN {modules} m ON m.id = cm.module AND m.name = :modname
JOIN {glossary} g ON g.id = cm.instance
JOIN {glossary_entries} ge ON ge.glossaryid = g.id
JOIN {comments} com ON com.commentarea =:commentarea AND com.itemid = ge.id
WHERE com.userid = :commentuserid";
$params = [
'contextlevel' => CONTEXT_MODULE,
'modname' => 'glossary',
'commentarea' => 'glossary_entry',
'commentuserid' => $userid,
];
$contextlist->add_from_sql($sql, $params);
return $contextlist;
+7
View File
@@ -45,6 +45,13 @@ $reset = optional_param('reset', null, PARAM_BOOL);
require_login();
if (\core\session\manager::is_loggedinas()) {
// Disable access to the user's dashboard for "logged in as" sessions
// to mitigate risks associated with loading other users' JavaScript.
// See MDL-63786 for more information.
redirect(new moodle_url('/', ['redirect' => 0]), get_string('unabletoaccess', 'core_my'));
}
$hassiteconfig = has_capability('moodle/site:config', context_system::instance());
if ($hassiteconfig && moodle_needs_upgrading()) {
redirect(new moodle_url('/admin/index.php'));
@@ -453,7 +453,7 @@ class moodle_content_writer implements content_writer {
$this->write_data($newshortpath, $variablecontent);
} else {
$treekey[$shortpath] = 'No var';
$treekey[clean_param($shortpath, PARAM_PATH)] = 'No var';
}
}
return [$tree, $treekey, $allfiles];
@@ -488,11 +488,11 @@ class moodle_content_writer implements content_writer {
$url = clean_param($url, PARAM_PATH);
$treeleaf->name = $file;
$treeleaf->itemtype = 'item';
$gokey = $url . DIRECTORY_SEPARATOR . $file;
$gokey = clean_param($url . '/' . $file, PARAM_PATH);
if (isset($treekey[$gokey]) && $treekey[$gokey] !== 'No var') {
$treeleaf->datavar = $treekey[$gokey];
} else {
$treeleaf->url = new \moodle_url($url . DIRECTORY_SEPARATOR . $file);
$treeleaf->url = new \moodle_url($url . '/' . $file);
}
};
@@ -635,16 +635,9 @@ class moodle_content_writer implements content_writer {
$this->copy_data($moodleimgpath, ['pix', 'moodlelogo.svg']);
// Additional required css.
// Determine what direction to show the data export page according to the user preference.
$rtl = right_to_left();
if (!$rtl) {
$bootstrapdestination = 'bootstrap.min.css';
$this->write_url_content('https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0/css/bootstrap.min.css',
$bootstrapdestination);
} else {
$rtldestination = 'rtlbootstrap.min.css';
$this->write_url_content('https://cdn.rtlcss.com/bootstrap/v4.0.0/css/bootstrap.min.css', $rtldestination);
}
$csspath = ['theme', 'boost', 'style', 'moodle.css'];
$destination = ['moodle.css'];
$this->copy_data($csspath, $destination);
$csspath = ['privacy', 'export_files', 'general.css'];
$destination = ['general.css'];
@@ -666,6 +659,7 @@ class moodle_content_writer implements content_writer {
$siteurl = $CFG->wwwroot;
// Create custom index.html file.
$rtl = right_to_left();
$htmlpage = new \core_privacy\output\exported_html_page($navigationhtml, $systemname, $fullusername, $rtl, $siteurl);
$outputpage = $output->render_html_page($htmlpage);
$this->write_data('index.html', $outputpage);
@@ -720,22 +714,4 @@ class moodle_content_writer implements content_writer {
}
return $content;
}
/**
* Write url files to the export.
*
* @param string $url Url of the file.
* @param string $path Path to store the file.
*/
protected function write_url_content(string $url, string $path) {
$filepointer = fopen($url, 'r');
$targetpath = $this->path . DIRECTORY_SEPARATOR . $path;
check_dir_exists(dirname($targetpath), true, true);
$status = file_put_contents($targetpath, $filepointer);
if ($status === false) {
// There was an error. Throw an exception to allow the download status to remain as requiring download.
throw new \moodle_exception('Content download was incomplete');
}
$this->files[$path] = $targetpath;
}
}
+3 -8
View File
@@ -49,17 +49,12 @@
<head>
<meta charset="UTF-8">
<title>Data export</title>
{{^righttoleft}}
<link rel="stylesheet" href="bootstrap.min.css">
{{/righttoleft}}
{{#righttoleft}}
<link rel="stylesheet" href="rtlbootstrap.min.css">
{{/righttoleft}}
<link rel="stylesheet" type="text/css" href="moodle.css" />
<link rel="stylesheet" type="text/css" href="general.css" />
</head>
<body>
<nav class="navbar navbar-light bg-light border-bottom">
<a class="navbar-brand" href="https://www.moodle.org" title="Moodle">
<a class="navbar-brand" href="https://moodle.org" title="Moodle">
<img class="moodle-logo" src="pix/moodlelogo.svg" alt="Moodle logo" />
</a>
<span class="navbar-brand">{{#str}}broughtbymoodle, core_privacy{{/str}}</span>
@@ -107,4 +102,4 @@
</footer>
</div>
</body>
</html>
</html>
@@ -0,0 +1,69 @@
<?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/>.
/**
* A scheduled task to remove unneeded random questions.
*
* @package qtype_random
* @category task
* @copyright 2018 Bo Pierce <email.bO.pierce@gmail.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace qtype_random\task;
defined('MOODLE_INTERNAL') || die();
/**
* A scheduled task to remove unneeded random questions.
*
* @copyright 2018 Bo Pierce <email.bO.pierce@gmail.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class remove_unused_questions extends \core\task\scheduled_task {
public function get_name() {
return get_string('taskunusedrandomscleanup', 'qtype_random');
}
public function execute() {
global $DB, $CFG;
require_once($CFG->libdir . '/questionlib.php');
// Find potentially unused random questions (up to 10000).
// Note, because we call question_delete_question below,
// the question will not actually be deleted if something else
// is using them, but nothing else in Moodle core uses qtype_random,
// and not many third-party plugins do.
$unusedrandomids = $DB->get_records_sql("
SELECT q.id, 1
FROM {question} q
LEFT JOIN {quiz_slots} qslots ON q.id = qslots.questionid
WHERE qslots.questionid IS NULL
AND q.qtype = ? AND hidden = ?", ['random', 0], 0, 10000);
$count = 0;
foreach ($unusedrandomids as $unusedrandomid => $notused) {
question_delete_question($unusedrandomid);
// In case the question was not actually deleted (because it was in use somehow
// mark it as hidden so the query above will not return it again.
$DB->set_field('question', 'hidden', 1, ['id' => $unusedrandomid]);
$count += 1;
}
mtrace('Cleaned up ' . $count . ' unused random questions.');
}
}
+38
View File
@@ -0,0 +1,38 @@
<?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/>.
/**
* Definition of question/type/random scheduled tasks.
*
* @package qtype_random
* @category task
* @copyright 2018 Bo Pierce <email.bO.pierce@gmail.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
defined('MOODLE_INTERNAL') || die();
$tasks = array(
array(
'classname' => 'qtype_random\task\remove_unused_questions',
'blocking' => 0,
'minute' => 'R',
'hour' => '*',
'day' => '*',
'month' => '*',
'dayofweek' => '*'
)
);
@@ -33,3 +33,4 @@ $string['randomqname'] = 'Random ({$a})';
$string['randomqplusname'] = 'Random ({$a} and subcategories)';
$string['selectedby'] = '{$a->questionname} selected by {$a->randomname}';
$string['selectmanualquestions'] = 'Random questions can use manually graded questions';
$string['taskunusedrandomscleanup'] = 'Remove unused random questions';
@@ -0,0 +1,70 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* Tests of the scheduled task for cleaning up random questions.
*
* @package qtype_random
* @copyright 2018 The Open University
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
defined('MOODLE_INTERNAL') || die();
global $CFG;
require_once($CFG->dirroot . '/mod/quiz/locallib.php');
/**
* Tests of the scheduled task for cleaning up random questions.
*
* @copyright 2018 The Open University
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class qtype_random_cleanup_task_testcase extends advanced_testcase {
public function test_cleanup_task_removes_unused_question() {
global $DB;
$this->resetAfterTest();
$this->setAdminUser();
$generator = $this->getDataGenerator();
$questiongenerator = $generator->get_plugin_generator('core_question');
$quizgenerator = $generator->get_plugin_generator('mod_quiz');
$cat = $questiongenerator->create_question_category();
$quiz = $quizgenerator->create_instance(['course' => SITEID]);
// Add two random questions.
quiz_add_random_questions($quiz, 0, $cat->id, 2, false);
$quizslots = $DB->get_records('quiz_slots', ['quizid' => $quiz->id],
'slot', 'slot, id, questionid');
// Now remove the second from the quiz. (Do it manually,
// because the API cleans up the random question, but we are trying to
// create an orphaned random question.)
$DB->delete_records('quiz_slots', array('id' => $quizslots[2]->id));
// Run the scheduled task.
$task = new \qtype_random\task\remove_unused_questions();
$this->expectOutputString("Cleaned up 1 unused random questions.\n");
$task->execute();
// Verify.
$this->assertTrue($DB->record_exists('question', ['id' => $quizslots[1]->questionid]));
$this->assertFalse($DB->record_exists('question', ['id' => $quizslots[2]->questionid]));
}
}
+1 -1
View File
@@ -26,7 +26,7 @@
defined('MOODLE_INTERNAL') || die();
$plugin->component = 'qtype_random';
$plugin->version = 2017111300;
$plugin->version = 2017111301;
$plugin->requires = 2017110800;
+24 -9
View File
@@ -129,26 +129,41 @@ class provider implements
/**
* Get the SQL required to find all submission items where this user has had any involvements.
*
* If possible an inner join should be used.
*
* @param string $alias The name of the table alias to use.
* @param string $component The na eof the component to fetch ratings for.
* @param string $ratingarea The rating area to fetch results for.
* @param string $itemidjoin The right-hand-side of the JOIN ON clause.
* @param int $userid The ID of the user being stored.
* @param bool $innerjoin Whether to use an inner join (preferred)
* @return \stdClass
*/
public static function get_sql_join($alias, $component, $ratingarea, $itemidjoin, $userid) {
public static function get_sql_join($alias, $component, $ratingarea, $itemidjoin, $userid, $innerjoin = false) {
static $count = 0;
$count++;
// Join the rating table with the specified alias and the relevant join params.
$join = "LEFT JOIN {rating} {$alias} ON ";
$join .= "{$alias}.userid = :ratinguserid{$count} AND ";
$join .= "{$alias}.component = :ratingcomponent{$count} AND ";
$join .= "{$alias}.ratingarea = :ratingarea{$count} AND ";
$join .= "{$alias}.itemid = {$itemidjoin}";
$userwhere = '';
// Match against the specified user.
$userwhere = "{$alias}.id IS NOT NULL";
if ($innerjoin) {
// Join the rating table with the specified alias and the relevant join params.
$join = "JOIN {rating} {$alias} ON ";
$join .= "{$alias}.itemid = {$itemidjoin}";
$userwhere .= "{$alias}.userid = :ratinguserid{$count} AND ";
$userwhere .= "{$alias}.component = :ratingcomponent{$count} AND ";
$userwhere .= "{$alias}.ratingarea = :ratingarea{$count}";
} else {
// Join the rating table with the specified alias and the relevant join params.
$join = "LEFT JOIN {rating} {$alias} ON ";
$join .= "{$alias}.userid = :ratinguserid{$count} AND ";
$join .= "{$alias}.component = :ratingcomponent{$count} AND ";
$join .= "{$alias}.ratingarea = :ratingarea{$count} AND ";
$join .= "{$alias}.itemid = {$itemidjoin}";
// Match against the specified user.
$userwhere = "{$alias}.id IS NOT NULL";
}
$params = [
'ratingcomponent' . $count => $component,
+88
View File
@@ -153,6 +153,94 @@ class core_rating_privacy_testcase extends \core_privacy\tests\provider_testcase
$this->assertFalse(isset($courses[$course3->id]));
}
/**
* Ensure that the get_sql_join function returns valid SQL which returns the correct list of rated itemids.
* This makes use of the optional inner join argument.
*/
public function test_get_sql_join_inner() {
global $DB;
$this->resetAfterTest();
$course1 = $this->getDataGenerator()->create_course();
$course2 = $this->getDataGenerator()->create_course();
$course3 = $this->getDataGenerator()->create_course();
$u1 = $this->getDataGenerator()->create_user();
$u2 = $this->getDataGenerator()->create_user();
$u3 = $this->getDataGenerator()->create_user();
// Rate the courses.
$rm = new rating_manager();
$ratingoptions = (object) [
'component' => 'core_course',
'ratingarea' => 'course',
'scaleid' => 100,
];
// Rate all courses as u1, and something else in the same context.
$this->rate_as_user($u1->id, 'core_course', 'course', $course1->id, \context_course::instance($course1->id), 25);
$this->rate_as_user($u1->id, 'core_course', 'course', $course2->id, \context_course::instance($course2->id), 50);
$this->rate_as_user($u1->id, 'core_course', 'course', $course3->id, \context_course::instance($course3->id), 75);
$this->rate_as_user($u1->id, 'core_course', 'files', $course3->id, \context_course::instance($course3->id), 99);
// Rate course2 as u2, and something else in a different context/component..
$this->rate_as_user($u2->id, 'core_course', 'course', $course2->id, \context_course::instance($course2->id), 90);
$this->rate_as_user($u2->id, 'user', 'user', $u3->id, \context_user::instance($u3->id), 10);
// Return any course which the u1 has rated.
// u1 rated all three courses.
$ratingquery = provider::get_sql_join('r', 'core_course', 'course', 'c.id', $u1->id, true);
$sql = "SELECT c.id FROM {course} c {$ratingquery->join} WHERE {$ratingquery->userwhere}";
$courses = $DB->get_records_sql($sql, $ratingquery->params);
$this->assertCount(3, $courses);
$this->assertTrue(isset($courses[$course1->id]));
$this->assertTrue(isset($courses[$course2->id]));
$this->assertTrue(isset($courses[$course3->id]));
// User u1 rated files in course 3 only.
$ratingquery = provider::get_sql_join('r', 'core_course', 'files', 'c.id', $u1->id, true);
$sql = "SELECT c.id FROM {course} c {$ratingquery->join} WHERE {$ratingquery->userwhere}";
$courses = $DB->get_records_sql($sql, $ratingquery->params);
$this->assertCount(1, $courses);
$this->assertFalse(isset($courses[$course1->id]));
$this->assertFalse(isset($courses[$course2->id]));
$this->assertTrue(isset($courses[$course3->id]));
// Return any course which the u2 has rated.
// User u2 rated only course 2.
$ratingquery = provider::get_sql_join('r', 'core_course', 'course', 'c.id', $u2->id, true);
$sql = "SELECT c.id FROM {course} c {$ratingquery->join} WHERE {$ratingquery->userwhere}";
$courses = $DB->get_records_sql($sql, $ratingquery->params);
$this->assertCount(1, $courses);
$this->assertFalse(isset($courses[$course1->id]));
$this->assertTrue(isset($courses[$course2->id]));
$this->assertFalse(isset($courses[$course3->id]));
// User u2 rated u3.
$ratingquery = provider::get_sql_join('r', 'user', 'user', 'u.id', $u2->id, true);
$sql = "SELECT u.id FROM {user} u {$ratingquery->join} WHERE {$ratingquery->userwhere}";
$users = $DB->get_records_sql($sql, $ratingquery->params);
$this->assertCount(1, $users);
$this->assertFalse(isset($users[$u1->id]));
$this->assertFalse(isset($users[$u2->id]));
$this->assertTrue(isset($users[$u3->id]));
// Return any course which the u3 has rated.
// User u3 did not rate anything.
$ratingquery = provider::get_sql_join('r', 'core_course', 'course', 'c.id', $u3->id, true);
$sql = "SELECT c.id FROM {course} c {$ratingquery->join} WHERE {$ratingquery->userwhere}";
$courses = $DB->get_records_sql($sql, $ratingquery->params);
$this->assertCount(0, $courses);
$this->assertFalse(isset($courses[$course1->id]));
$this->assertFalse(isset($courses[$course2->id]));
$this->assertFalse(isset($courses[$course3->id]));
}
/**
* Ensure that export_area_ratings exports all ratings that a user has made, and all ratings for a users own content.
*/
File diff suppressed because it is too large Load Diff
-1
View File
@@ -646,7 +646,6 @@ class core_userliblib_testcase extends advanced_testcase {
$this->setUser($user8);
$this->assertFalse(user_can_view_profile($user1));
$allroles = $DB->get_records_menu('role', array(), 'id', 'archetype, id');
// Let us test with guest user.
$this->setGuestUser();
$CFG->forceloginforprofiles = 1;
+1 -1
View File
@@ -25,7 +25,7 @@ echo $OUTPUT->header();
$rs = $DB->get_recordset_select("user", "deleted = 0 AND picture > 0", array(), "lastaccess DESC", user_picture::fields());
foreach ($rs as $user) {
$fullname = fullname($user);
$fullname = s(fullname($user));
echo "<a href=\"$CFG->wwwroot/user/view.php?id=$user->id&amp;course=1\" ".
"title=\"$fullname\">";
echo $OUTPUT->user_picture($user);
+2 -2
View File
@@ -29,11 +29,11 @@
defined('MOODLE_INTERNAL') || die();
$version = 2017111306.00; // 20171113 = branching date YYYYMMDD - do not modify!
$version = 2017111308.00; // 20171113 = branching date YYYYMMDD - do not modify!
// RR = release increments - 00 in DEV branches.
// .XX = incremental changes.
$release = '3.4.6 (Build: 20181112)'; // Human-friendly version name
$release = '3.4.8 (Build: 20190311)'; // Human-friendly version name
$branch = '34'; // This version's branch.
$maturity = MATURITY_STABLE; // This version's maturity level.