Merge branch 'MDL-67587-master-1' of git://github.com/mihailges/moodle

Changed PARAM_TEXT to PARAM_NOTAGS to "search" param
because it's the same but WITHOUT lang support and we
don't need lang support there.

Of course, both require to verify that the output is always
escaped. In this case (mustache) it is. Or also p() or s().

Without that XSS on form values are relatively easy!
This commit is contained in:
Eloy Lafuente (stronk7) 2020-04-10 12:09:55 +02:00
commit 594b4b98b7
7 changed files with 158 additions and 30 deletions

View File

@ -163,14 +163,44 @@ class content_item_service {
* @return array the array of exported content items.
*/
public function get_all_content_items(\stdClass $user): array {
global $PAGE;
$allcontentitems = $this->repository->find_all();
return $this->export_content_items($user, $allcontentitems);
}
/**
* Get content items which name matches a certain pattern and may be added to courses,
* irrespective of course caps, for site admin views, etc.
*
* @param \stdClass $user The user object.
* @param string $pattern The search pattern.
* @return array The array of exported content items.
*/
public function get_content_items_by_name_pattern(\stdClass $user, string $pattern): array {
$allcontentitems = $this->repository->find_all();
$filteredcontentitems = array_filter($allcontentitems, function($contentitem) use ($pattern) {
return preg_match("/$pattern/i", $contentitem->get_title()->get_value());
});
return $this->export_content_items($user, $filteredcontentitems);
}
/**
* Export content items.
*
* @param \stdClass $user The user object.
* @param array $contentitems The content items array.
* @return array The array of exported content items.
*/
private function export_content_items(\stdClass $user, $contentitems) {
global $PAGE;
// Export the objects to get the formatted objects for transfer/display.
$favourites = $this->get_favourite_content_items_for_user($user);
$recommendations = $this->get_recommendations();
$ciexporter = new course_content_items_exporter(
$allcontentitems,
$contentitems,
[
'context' => \context_system::instance(),
'favouriteitems' => $favourites,

View File

@ -36,13 +36,18 @@ class activity_list implements \renderable, \templatable {
/** @var array $modules activities to display in the recommendations page. */
protected $modules;
/** @var string $searchquery The search query. */
protected $searchquery;
/**
* Constructor method.
*
* @param array $modules Activities to display
* @param string $searchquery The search query if present
*/
public function __construct(array $modules) {
public function __construct(array $modules, string $searchquery) {
$this->modules = $modules;
$this->searchquery = $searchquery;
}
/**
@ -63,6 +68,18 @@ class activity_list implements \renderable, \templatable {
];
}, $this->modules);
return ['categories' => ['categoryname' => get_string('activities'), 'categorydata' => $info]];
return [
'categories' => [
[
'categoryname' => get_string('activities'),
'hascategorydata' => !empty($info),
'categorydata' => $info
]
],
'search' => [
'query' => $this->searchquery,
'searchresultsnumber' => count($this->modules)
]
];
}
}

View File

@ -24,6 +24,8 @@
require_once("../config.php");
$search = optional_param('search', '', PARAM_TEXT);
$context = context_system::instance();
$url = new moodle_url('/course/recommendations.php');
@ -45,9 +47,13 @@ echo $renderer->header();
echo $renderer->heading(get_string('activitychooserrecommendations', 'course'));
$manager = \core_course\local\factory\content_item_service_factory::get_content_item_service();
$modules = $manager->get_all_content_items($USER);
if (!empty($search)) {
$modules = $manager->get_content_items_by_name_pattern($USER, $search);
} else {
$modules = $manager->get_all_content_items($USER);
}
$activitylist = new \core_course\output\recommendations\activity_list($modules);
$activitylist = new \core_course\output\recommendations\activity_list($modules, $search);
echo $renderer->render_activity_list($activitylist);

View File

@ -21,31 +21,56 @@
No example given as the js will fire and create records from the template library page.
}}
{{#search}}
<form class="row">
<div class="input-group pt-4 pb-1 col-md-6">
<label for="search">
<span class="sr-only">{{#str}} searchactivitiesbyname, course {{/str}}</span>
</label>
<input type="text" name="search" id="search" class="form-control rounded-left" autocomplete="off"
placeholder="{{#str}}search, core {{/str}}" {{#query}} value="{{query}}" autofocus {{/query}}
>
<div class="input-group-append">
<button type="submit" class="btn btn-outline-secondary rounded-right" type="button">
<i class="icon fa fa-search fa-fw m-0" aria-hidden="true"></i>
<span class="sr-only">{{#str}}submitsearch, course {{/str}}</span>
</button>
</div>
</div>
</form>
{{#query}}
<div class="pt-1 pb-1">
<span role="alert">{{#str}} searchresults, course, {{searchresultsnumber}} {{/str}}</span>
</div>
{{/query}}
{{/search}}
{{#categories}}
<h3>{{categoryname}}</h3>
<table class="table table-striped table-hover">
<thead>
<tr class="d-flex">
<th scope="col" class="col-7 c0">{{#str}}module, course{{/str}}</th>
<th scope="col" class="col-5 c1">{{#str}}recommend, course{{/str}}</th>
</tr>
</thead>
<tbody>
{{#categorydata}}
<tr class="d-flex">
<td class="col-7 c0"><span>{{{icon}}}</span>{{name}}</td>
{{#id}}
<td class="col-5 c1 colselect">
<input class="activity-recommend-checkbox" type="checkbox" aria-label="{{#str}}recommendcheckbox, course, {{name}}{{/str}}" data-area="{{componentname}}" data-id="{{id}}" {{#recommended}}checked="checked"{{/recommended}} />
</td>
{{/id}}
{{^id}}
<td class="col-5"></td>
{{/id}}
</tr>
{{/categorydata}}
</tbody>
</table>
{{#hascategorydata}}
<h3 class="pt-4">{{categoryname}}</h3>
<table class="table table-striped table-hover">
<thead>
<tr class="d-flex">
<th scope="col" class="col-7 c0">{{#str}}module, course{{/str}}</th>
<th scope="col" class="col-5 c1">{{#str}}recommend, course{{/str}}</th>
</tr>
</thead>
<tbody>
{{#categorydata}}
<tr class="d-flex">
<td class="col-7 c0"><span>{{{icon}}}</span>{{name}}</td>
{{#id}}
<td class="col-5 c1 colselect">
<input class="activity-recommend-checkbox" type="checkbox" aria-label="{{#str}}recommendcheckbox, course, {{name}}{{/str}}" data-area="{{componentname}}" data-id="{{id}}" {{#recommended}}checked="checked"{{/recommended}} />
</td>
{{/id}}
{{^id}}
<td class="col-5"></td>
{{/id}}
</tr>
{{/categorydata}}
</tbody>
</table>
{{/hascategorydata}}
{{/categories}}
{{#js}}
require([

View File

@ -0,0 +1,21 @@
@core @core_course
Feature: Search recommended activities
As an admin I am able to search for activities in the "Recommended activities" admin setting page
Scenario: Search results are returned if the search query matches any activity names
Given I log in as "admin"
And I am on site homepage
And I navigate to "Courses > Recommended activities" in site administration
When I set the field "search" to "assign"
And I click on "Submit search" "button"
Then I should see "Search results: 1"
And "Assignment" "table_row" should exist
And "Book" "table_row" should not exist
Scenario: Search results are not returned if the search query does not match with any activity names
Given I log in as "admin"
And I am on site homepage
And I navigate to "Courses > Recommended activities" in site administration
When I set the field "search" to "random query"
And I click on "Submit search" "button"
Then I should see "Search results: 0"

View File

@ -134,6 +134,32 @@ class services_content_item_service_testcase extends \advanced_testcase {
$this->assertContains('lti', array_column($allcontentitems, 'name'));
}
/**
* Test confirming that content items which title match a certain pattern can be fetched irrespective of permissions.
*/
public function test_get_content_items_by_name_pattern() {
$this->resetAfterTest();
// Create a user in a course.
$course = $this->getDataGenerator()->create_course();
$user = $this->getDataGenerator()->create_and_enrol($course, 'editingteacher');
// Pattern that does exist.
$pattern1 = "assign";
// Pattern that does not exist.
$pattern2 = "random string";
$cis = new content_item_service(new content_item_readonly_repository());
$matchingcontentitems1 = $cis->get_content_items_by_name_pattern($user, $pattern1);
$matchingcontentitems2 = $cis->get_content_items_by_name_pattern($user, $pattern2);
// The pattern "assign" should return 1 content item ("Assignment").
$this->assertCount(1, $matchingcontentitems1);
$this->assertEquals("Assignment", $matchingcontentitems1[0]->title);
// The pattern "random string" should not return any content items.
$this->assertEmpty($matchingcontentitems2);
}
/**
* Test confirming that a content item can be added to a user's favourites.
*/

View File

@ -67,6 +67,9 @@ $string['privacy:metadata:completionsummary'] = 'The course contains completion
$string['privacy:metadata:favouritessummary'] = 'The course contains information relating to the course being starred by the user.';
$string['recommend'] = 'Recommend';
$string['recommendcheckbox'] = 'Recommend activity: {$a}';
$string['searchactivitiesbyname'] = 'Search for activities by name';
$string['searchresults'] = 'Search results: {$a}';
$string['submitsearch'] = 'Submit search';
$string['studentsatriskincourse'] = 'Students at risk in {$a} course';
$string['studentsatriskinfomessage'] = 'Hi {$a->userfirstname},
<p>Students in the {$a->coursename} course have been identified as being at risk.</p>';