diff --git a/mod/forum/classes/search/post.php b/mod/forum/classes/search/post.php index 3efa21e1442..8729aab3275 100644 --- a/mod/forum/classes/search/post.php +++ b/mod/forum/classes/search/post.php @@ -99,6 +99,7 @@ class post extends \core_search\area\base_mod { $doc->set('type', \core_search\manager::TYPE_TEXT); $doc->set('courseid', $record->courseid); $doc->set('userid', $record->userid); + $doc->set('owneruserid', \core_search\manager::NO_OWNER_ID); $doc->set('modified', $record->modified); return $doc; diff --git a/mod/glossary/classes/search/entry.php b/mod/glossary/classes/search/entry.php index 4d1f0821b5c..32397db9e61 100644 --- a/mod/glossary/classes/search/entry.php +++ b/mod/glossary/classes/search/entry.php @@ -95,6 +95,7 @@ class entry extends \core_search\area\base_mod { $doc->set('type', \core_search\manager::TYPE_TEXT); $doc->set('courseid', $entry->course); $doc->set('userid', $entry->userid); + $doc->set('owneruserid', \core_search\manager::NO_OWNER_ID); $doc->set('modified', $entry->timemodified); // Adding keywords as extra info. diff --git a/mod/page/classes/search/activity.php b/mod/page/classes/search/activity.php index 56b8bae4964..d38fe935298 100644 --- a/mod/page/classes/search/activity.php +++ b/mod/page/classes/search/activity.php @@ -68,6 +68,7 @@ class activity extends \core_search\area\base_activity { $doc->set('contextid', $context->id); $doc->set('type', \core_search\manager::TYPE_TEXT); $doc->set('courseid', $record->course); + $doc->set('owneruserid', \core_search\manager::NO_OWNER_ID); $doc->set('modified', $record->timemodified); $doc->set('description1', content_to_text($record->intro, $record->introformat)); diff --git a/search/classes/area/base_activity.php b/search/classes/area/base_activity.php index 0fdc839380f..2b350495774 100644 --- a/search/classes/area/base_activity.php +++ b/search/classes/area/base_activity.php @@ -93,6 +93,7 @@ abstract class base_activity extends base_mod { $doc->set('contextid', $context->id); $doc->set('type', \core_search\manager::TYPE_TEXT); $doc->set('courseid', $record->course); + $doc->set('owneruserid', \core_search\manager::NO_OWNER_ID); $doc->set('modified', $record->{static::MODIFIED_FIELD_NAME}); return $doc; diff --git a/search/classes/document.php b/search/classes/document.php index 7a0451ef4bd..9d47ab9f8b2 100644 --- a/search/classes/document.php +++ b/search/classes/document.php @@ -120,6 +120,11 @@ class document implements \renderable, \templatable { 'stored' => true, 'indexed' => false ), + 'owneruserid' => array( + 'type' => 'int', + 'stored' => true, + 'indexed' => true + ), 'modified' => array( 'type' => 'tdate', 'stored' => true, diff --git a/search/classes/engine.php b/search/classes/engine.php index bd77769f59f..854cec31332 100644 --- a/search/classes/engine.php +++ b/search/classes/engine.php @@ -300,7 +300,7 @@ abstract class engine { * Executes the query on the engine. * * Implementations of this function should check user context array to limit the results to contexts where the - * user have access. + * user have access. They should also limit the owneruserid field to manger::NO_OWNER_ID or the current user's id. * * @param stdClass $filters Query and filters to apply. * @param array $usercontexts Contexts where the user has access. True if the user can access all contexts. diff --git a/search/classes/manager.php b/search/classes/manager.php index 3f8f7da5786..497ffea4758 100644 --- a/search/classes/manager.php +++ b/search/classes/manager.php @@ -67,6 +67,11 @@ class manager { */ const DISPLAY_RESULTS_PER_PAGE = 10; + /** + * @var int The id to be placed in owneruserid when there is no owner. + */ + const NO_OWNER_ID = 0; + /** * @var \core_search\area\base[] Enabled search areas. */ @@ -398,12 +403,13 @@ class manager { * @return \core_search\document[] */ public function search(\stdClass $formdata) { + global $USER; $cache = \cache::make('core', 'search_results'); // Generate a string from all query filters // Not including $areascontext here, being a user cache it is not needed. - $querykey = $this->generate_query_key($formdata); + $querykey = $this->generate_query_key($formdata, $USER->id); // Look for cached results before executing it. if ($results = $cache->get($querykey)) { @@ -434,6 +440,7 @@ class manager { * @return string */ protected function generate_query_key($formdata) { + global $USER; // Empty values by default (although q should always have a value). $fields = array('q', 'title', 'areaid', 'timestart', 'timeend', 'page'); @@ -449,7 +456,7 @@ class manager { // Although it is not likely, we prevent cache hits if available search areas change during the session. $enabledareas = implode('-', array_keys(static::get_search_areas_list(true))); - return md5($params->q . 'title=' . $params->title . 'areaid=' . $params->areaid . + return md5($params->q . 'userid=' . $USER->id . 'title=' . $params->title . 'areaid=' . $params->areaid . 'timestart=' . $params->timestart . 'timeend=' . $params->timeend . 'page=' . $params->page . $enabledareas); } diff --git a/search/engine/solr/classes/engine.php b/search/engine/solr/classes/engine.php index 1e45829519d..17fa080010f 100644 --- a/search/engine/solr/classes/engine.php +++ b/search/engine/solr/classes/engine.php @@ -64,11 +64,12 @@ class engine extends \core_search\engine { * Prepares a Solr query, applies filters and executes it returning its results. * * @throws \core_search\engine_exception - * @param stdClass $filters Containing query and filters. - * @param array $usercontexts Contexts where the user has access. True if the user can access all contexts. + * @param stdClass $filters Containing query and filters. + * @param array $usercontexts Contexts where the user has access. True if the user can access all contexts. * @return \core_search\document[] Results or false if no results */ public function execute_query($filters, $usercontexts) { + global $USER; // Let's keep these changes internal. $data = clone $filters; @@ -111,6 +112,9 @@ class engine extends \core_search\engine { $query->addFilterQuery('{!cache=false}modified:[' . $data->timestart . ' TO ' . $data->timeend . ']'); } + // Restrict to users who are supposed to be able to see a particular result. + $query->addFilterQuery('owneruserid:(' . \core_search\manager::NO_OWNER_ID . ' OR ' . $USER->id . ')'); + // And finally restrict it to the context where the user can access, we want this one cached. // If the user can access all contexts $usercontexts value is just true, we don't need to filter // in that case. @@ -224,6 +228,10 @@ class engine extends \core_search\engine { * @return array $results containing final results to be displayed. */ public function query_response($queryresponse) { + global $USER; + + $userid = $USER->id; + $noownerid = \core_search\manager::NO_OWNER_ID; $response = $queryresponse->getResponse(); $numgranted = 0; @@ -237,6 +245,12 @@ class engine extends \core_search\engine { // Iterate through the results checking its availability and whether they are available for the user or not. foreach ($docs as $key => $docdata) { + if ($docdata['owneruserid'] != $noownerid && $docdata['owneruserid'] != $userid) { + // If owneruserid is set, no other user should be able to access this record. + unset($docs[$key]); + continue; + } + if (!$searcharea = $this->get_search_area($docdata->areaid)) { unset($docs[$key]); continue; diff --git a/search/engine/solr/tests/engine_test.php b/search/engine/solr/tests/engine_test.php index da9234db71e..855541b0c53 100644 --- a/search/engine/solr/tests/engine_test.php +++ b/search/engine/solr/tests/engine_test.php @@ -176,4 +176,80 @@ class search_solr_engine_testcase extends advanced_testcase { cache_helper::purge_by_definition('core', 'search_results'); $this->assertCount(0, $this->search->search($querydata)); } + + public function test_alloweduserid() { + $engine = $this->search->get_engine(); + $area = new core_mocksearch\search\role_capabilities(); + + // Get the first record for the recordset. + $recordset = $area->get_recordset_by_timestamp(); + foreach ($recordset as $r) { + $record = $r; + break; + } + $recordset->close(); + + // Get the doc and insert the default doc. + $doc = $area->get_document($record); + $engine->add_document($doc->export_for_engine()); + + $users = array(); + $users[] = $this->getDataGenerator()->create_user(); + $users[] = $this->getDataGenerator()->create_user(); + $users[] = $this->getDataGenerator()->create_user(); + + // Add a record that only user 100 can see. + $originalid = $doc->get('id'); + + // Now add a custom doc for each user. + foreach ($users as $user) { + $doc->set('id', $originalid.'-'.$user->id); + $doc->set('owneruserid', $user->id); + $engine->add_document($doc->export_for_engine()); + } + + $engine->commit(); + + $querydata = new stdClass(); + $querydata->q = 'message'; + $querydata->title = $doc->get('title'); + + // We are going to go through each user and see if they get the original and the owned doc. + foreach ($users as $user) { + $this->setUser($user); + + $results = $this->search->search($querydata); + $this->assertCount(2, $results); + + $owned = 0; + $notowned = 0; + + // We don't know what order we will get the results in, so we are doing this. + foreach ($results as $result) { + $owneruserid = $result->get('owneruserid'); + if (empty($owneruserid)) { + $notowned++; + $this->assertEquals(0, $owneruserid); + $this->assertEquals($originalid, $result->get('id')); + } else { + $owned++; + $this->assertEquals($user->id, $owneruserid); + $this->assertEquals($originalid.'-'.$user->id, $result->get('id')); + } + } + + $this->assertEquals(1, $owned); + $this->assertEquals(1, $notowned); + } + + // Now test a user with no owned results. + $otheruser = $this->getDataGenerator()->create_user(); + $this->setUser($otheruser); + + $results = $this->search->search($querydata); + $this->assertCount(1, $results); + + $this->assertEquals(0, $results[0]->get('owneruserid')); + $this->assertEquals($originalid, $results[0]->get('id')); + } } diff --git a/search/tests/fixtures/mock_search_area.php b/search/tests/fixtures/mock_search_area.php index d90ccc59694..de7febbd7e4 100644 --- a/search/tests/fixtures/mock_search_area.php +++ b/search/tests/fixtures/mock_search_area.php @@ -55,6 +55,7 @@ class role_capabilities extends \core_search\area\base { $doc->set('type', \core_search\manager::TYPE_TEXT); $doc->set('courseid', SITEID); $doc->set('userid', $USER->id); + $doc->set('owneruserid', \core_search\manager::NO_OWNER_ID); $doc->set('modified', time()); return $doc;