diff --git a/admin/tool/mobile/classes/api.php b/admin/tool/mobile/classes/api.php index fcd1a1ec025..bb9686e8b72 100644 --- a/admin/tool/mobile/classes/api.php +++ b/admin/tool/mobile/classes/api.php @@ -373,6 +373,16 @@ class api { $settings->forcetimezone = $CFG->forcetimezone; } + if (empty($section) || $section === 'manageglobalsearch') { + $settings->searchengine = $CFG->searchengine; + $settings->searchenablecategories = $CFG->searchenablecategories; + $settings->searchdefaultcategory = $CFG->searchdefaultcategory; + $settings->searchhideallcategory = $CFG->searchhideallcategory; + $settings->searchmaxtopresults = $CFG->searchmaxtopresults; + $settings->searchbannerenable = $CFG->searchbannerenable; + $settings->searchbanner = $CFG->searchbanner; + } + return $settings; } @@ -535,6 +545,7 @@ class api { 'CoreFilterDelegate' => new lang_string('type_filter_plural', 'plugin'), 'CoreReportBuilderDelegate' => new lang_string('reportbuilder', 'core_reportbuilder'), 'NoDelegate_CoreUserSupport' => new lang_string('contactsitesupport', 'admin'), + 'NoDelegate_GlobalSearch' => new lang_string('globalsearch', 'search'), ), "$mainmenu" => array( '$mmSideMenuDelegate_mmaFrontpage' => new lang_string('sitehome'), diff --git a/admin/tool/mobile/tests/externallib_test.php b/admin/tool/mobile/tests/externallib_test.php index b722042f9ba..9cd85a08770 100644 --- a/admin/tool/mobile/tests/externallib_test.php +++ b/admin/tool/mobile/tests/externallib_test.php @@ -259,6 +259,14 @@ class externallib_test extends externallib_advanced_testcase { $expected[] = ['name' => 'timezone', 'value' => $CFG->timezone]; $expected[] = ['name' => 'forcetimezone', 'value' => $CFG->forcetimezone]; + $expected[] = ['name' => 'searchengine', 'value' => $CFG->searchengine]; + $expected[] = ['name' => 'searchenablecategories', 'value' => $CFG->searchenablecategories]; + $expected[] = ['name' => 'searchdefaultcategory', 'value' => $CFG->searchdefaultcategory]; + $expected[] = ['name' => 'searchhideallcategory', 'value' => $CFG->searchhideallcategory]; + $expected[] = ['name' => 'searchmaxtopresults', 'value' => $CFG->searchmaxtopresults]; + $expected[] = ['name' => 'searchbannerenable', 'value' => $CFG->searchbannerenable]; + $expected[] = ['name' => 'searchbanner', 'value' => $CFG->searchbanner]; + $this->assertCount(0, $result['warnings']); $this->assertEquals($expected, $result['settings']); diff --git a/lib/db/services.php b/lib/db/services.php index 202db54d220..503c4743877 100644 --- a/lib/db/services.php +++ b/lib/db/services.php @@ -1706,6 +1706,34 @@ $functions = array( 'type' => 'read', 'ajax' => true ), + 'core_search_get_results' => [ + 'classname' => '\core_search\external\get_results', + 'description' => 'Get search results.', + 'type' => 'read', + 'capabilities' => 'moodle/search:query', + 'services' => [MOODLE_OFFICIAL_MOBILE_SERVICE], + ], + 'core_search_get_search_areas_list' => [ + 'classname' => '\core_search\external\get_search_areas_list', + 'description' => 'Get search areas.', + 'type' => 'read', + 'capabilities' => 'moodle/search:query', + 'services' => [MOODLE_OFFICIAL_MOBILE_SERVICE], + ], + 'core_search_view_results' => [ + 'classname' => '\core_search\external\view_results', + 'description' => 'Trigger view search results event.', + 'type' => 'write', + 'capabilities' => 'moodle/search:query', + 'services' => [MOODLE_OFFICIAL_MOBILE_SERVICE], + ], + 'core_search_get_top_results' => [ + 'classname' => '\core_search\external\get_top_results', + 'description' => 'Get top search results.', + 'type' => 'read', + 'capabilities' => 'moodle/search:query', + 'services' => [MOODLE_OFFICIAL_MOBILE_SERVICE], + ], 'core_tag_get_tagindex' => array( 'classname' => 'core_tag_external', 'methodname' => 'get_tagindex', diff --git a/search/classes/document.php b/search/classes/document.php index d7cf6709d1e..85e116f6872 100644 --- a/search/classes/document.php +++ b/search/classes/document.php @@ -603,19 +603,34 @@ class document implements \renderable, \templatable { /** * Export the document data to be used as a template context. * + * Just delegates all the processing to export_doc_info, also used by external functions. * Adding more info than the required one as people might be interested in extending the template. * + * @param \renderer_base $output The renderer. + * @return array + */ + public function export_for_template(\renderer_base $output): array { + $docdata = $this->export_doc($output); + return $docdata; + } + + /** + * Returns the current docuement information. + * + * Adding more info than the required one as themers and ws clients might be interested in showing more stuff. + * * Although content is a required field when setting up the document, it accepts '' (empty) values * as they may be the result of striping out HTML. * * SECURITY NOTE: It is the responsibility of the document to properly escape any text to be displayed. * The renderer will output the content without any further cleaning. * - * @param renderer_base $output The renderer. + * @param \renderer_base $output The renderer. * @return array */ - public function export_for_template(\renderer_base $output) { - global $USER; + public function export_doc(\renderer_base $output): array { + global $USER, $CFG; + require_once($CFG->dirroot . '/course/lib.php'); list($componentname, $areaname) = \core_search\manager::extract_areaid_parts($this->get('areaid')); $context = context::instance_by_id($this->get('contextid')); @@ -623,15 +638,18 @@ class document implements \renderable, \templatable { $searcharea = \core_search\manager::get_search_area($this->data['areaid']); $title = $this->is_set('title') ? $this->format_text($searcharea->get_document_display_title($this)) : ''; $data = [ + 'itemid' => $this->get('itemid'), 'componentname' => $componentname, 'areaname' => $areaname, - 'courseurl' => course_get_url($this->get('courseid')), + 'courseurl' => (course_get_url($this->get('courseid')))->out(false), 'coursefullname' => format_string($this->get('coursefullname'), true, ['context' => $context->id]), 'modified' => userdate($this->get('modified')), + 'timemodified' => $this->get('modified'), 'title' => ($title !== '') ? $title : get_string('notitle', 'search'), - 'docurl' => $this->get_doc_url(), + 'docurl' => ($this->get_doc_url())->out(false), 'content' => $this->is_set('content') ? $this->format_text($this->get('content')) : null, - 'contexturl' => $this->get_context_url(), + 'contextid' => $this->get('contextid'), + 'contexturl' => ($this->get_context_url())->out(false), 'description1' => $this->is_set('description1') ? $this->format_text($this->get('description1')) : null, 'description2' => $this->is_set('description2') ? $this->format_text($this->get('description2')) : null, ]; @@ -656,17 +674,20 @@ class document implements \renderable, \templatable { if ($this->get('userid') == $USER->id || (has_capability('moodle/user:viewdetails', $context) && has_capability('moodle/course:viewparticipants', $context))) { - $data['userurl'] = new \moodle_url( + $data['userurl'] = (new \moodle_url( '/user/view.php', ['id' => $this->get('userid'), 'course' => $this->get('courseid')] - ); + ))->out(false); $data['userfullname'] = format_string($this->get('userfullname'), true, ['context' => $context->id]); + $data['userid'] = $this->get('userid'); } } if ($docicon = $this->get_doc_icon()) { $data['icon'] = $output->image_url($docicon->get_name(), $docicon->get_component()); + $data['iconurl'] = $data['icon']->out(false); } + $data['textformat'] = $this->get_text_format(); return $data; } diff --git a/search/classes/external/document_exporter.php b/search/classes/external/document_exporter.php new file mode 100644 index 00000000000..062fc34b7ef --- /dev/null +++ b/search/classes/external/document_exporter.php @@ -0,0 +1,141 @@ +. + +namespace core_search\external; + +use core\external\exporter; + +/** + * Contains related class for displaying information of a search result. + * + * @package core_search + * @since Moodle 4.3 + */ +class document_exporter extends exporter { + + /** + * Return the list of properties. + * + * @return array + */ + protected static function define_properties() { + return [ + 'itemid' => [ + 'type' => PARAM_INT, + 'description' => 'unique id in the search area scope', + ], + 'componentname' => [ + 'type' => PARAM_ALPHANUMEXT, + 'description' => 'component name', + ], + 'areaname' => [ + 'type' => PARAM_ALPHANUMEXT, + 'description' => 'search area name', + ], + 'courseurl' => [ + 'type' => PARAM_URL, + 'description' => 'result course url', + ], + 'coursefullname' => [ + 'type' => PARAM_RAW, + 'description' => 'result course fullname', + ], + 'timemodified' => [ + 'type' => PARAM_INT, + 'description' => 'result modified time', + ], + 'title' => [ + 'type' => PARAM_RAW, + 'description' => 'result title', + ], + 'docurl' => [ + 'type' => PARAM_URL, + 'description' => 'result url', + ], + 'iconurl' => [ + 'type' => PARAM_URL, + 'description' => 'icon url', + 'optional' => true, + 'default' => '', + 'null' => NULL_ALLOWED, + ], + 'content' => [ + 'type' => PARAM_RAW, + 'description' => 'result contents', + 'optional' => true, + 'default' => '', + 'null' => NULL_ALLOWED, + ], + 'contextid' => [ + 'type' => PARAM_INT, + 'description' => 'result context id', + ], + 'contexturl' => [ + 'type' => PARAM_URL, + 'description' => 'result context url', + ], + 'description1' => [ + 'type' => PARAM_RAW, + 'description' => 'extra result contents, depends on the search area', + 'optional' => true, + 'default' => '', + 'null' => NULL_ALLOWED, + ], + 'description2' => [ + 'type' => PARAM_RAW, + 'description' => 'extra result contents, depends on the search area', + 'optional' => true, + 'default' => '', + 'null' => NULL_ALLOWED, + ], + 'multiplefiles' => [ + 'type' => PARAM_INT, + 'description' => 'whether multiple files are returned or not', + 'optional' => true, + ], + 'filenames' => [ + 'type' => PARAM_RAW, + 'description' => 'result file names if present', + 'muultiple' => true, + 'optional' => true, + ], + 'filename' => [ + 'type' => PARAM_RAW, + 'description' => 'result file name if present', + 'optional' => true, + ], + 'userid' => [ + 'type' => PARAM_INT, + 'description' => 'user id', + 'optional' => true, + ], + 'userurl' => [ + 'type' => PARAM_URL, + 'description' => 'user url', + 'optional' => true, + ], + 'userfullname' => [ + 'type' => PARAM_RAW, + 'description' => 'user fullname', + 'optional' => true, + ], + 'textformat' => [ + 'type' => PARAM_INT, + 'description' => 'text fields format, it is the same for all of them', + ] + ]; + } +} diff --git a/search/classes/external/get_results.php b/search/classes/external/get_results.php new file mode 100644 index 00000000000..ef013061fc0 --- /dev/null +++ b/search/classes/external/get_results.php @@ -0,0 +1,158 @@ +. + +namespace core_search\external; + +use core_external\external_api; +use core_external\external_function_parameters; +use core_external\external_single_structure; +use core_external\external_multiple_structure; +use core_external\external_value; +use moodle_exception; + +/** + * External function for retrieving search results. + * + * @package core_search + * @copyright 2023 David Monllao & Juan Leyva + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * @since Moodle 4.3 + */ +class get_results extends external_api { + + /** + * Webservice parameters. + * + * @return external_function_parameters + */ + public static function execute_parameters(): external_function_parameters { + return new external_function_parameters( + [ + 'query' => new external_value(PARAM_NOTAGS, 'the search query'), + 'filters' => new external_single_structure( + [ + 'title' => new external_value(PARAM_NOTAGS, 'result title', VALUE_OPTIONAL), + 'areaids' => new external_multiple_structure( + new external_value(PARAM_ALPHANUMEXT, 'areaid'), 'restrict results to these areas', VALUE_DEFAULT, [] + ), + 'courseids' => new external_multiple_structure( + new external_value(PARAM_INT, 'courseid'), 'restrict results to these courses', VALUE_DEFAULT, [] + ), + 'contextids' => new external_multiple_structure( + new external_value(PARAM_INT, 'contextid'), 'restrict results to these contexts', VALUE_DEFAULT, [] + ), + 'cat' => new external_value(PARAM_NOTAGS, 'category to filter areas', VALUE_DEFAULT, ''), + 'userids' => new external_multiple_structure( + new external_value(PARAM_INT, 'userid'), 'restrict results to these users', VALUE_DEFAULT, [] + ), + 'groupids' => new external_multiple_structure( + new external_value(PARAM_INT, 'groupid'), 'restrict results to these groups', VALUE_DEFAULT, [] + ), + 'mycoursesonly' => new external_value(PARAM_BOOL, 'only results from my courses', VALUE_DEFAULT, false), + 'order' => new external_value(PARAM_ALPHA, 'how to order', VALUE_DEFAULT, ''), + 'timestart' => new external_value(PARAM_INT, 'docs modified after this date', VALUE_DEFAULT, 0), + 'timeend' => new external_value(PARAM_INT, 'docs modified before this date', VALUE_DEFAULT, 0) + ], 'filters to apply', VALUE_DEFAULT, [] + ), + 'page' => new external_value(PARAM_INT, 'results page number starting from 0, defaults to the first page', + VALUE_DEFAULT, 0) + ] + ); + } + + /** + * Gets global search results based on the provided query and filters. + * + * @param string $query the search query + * @param array $filters filters to apply + * @param int $page results page + * @return array search results + */ + public static function execute(string $query, array $filters = [], int $page = 0): array { + global $PAGE; + + $params = self::validate_parameters(self::execute_parameters(), + [ + 'query' => $query, + 'filters' => $filters, + 'page' => $page, + ] + ); + + $system = \context_system::instance(); + external_api::validate_context($system); + + require_capability('moodle/search:query', $system); + + if (\core_search\manager::is_global_search_enabled() === false) { + throw new moodle_exception('globalsearchdisabled', 'search'); + } + + $search = \core_search\manager::instance(); + + $data = new \stdClass(); + // First, mandatory parameters for consistency with web. + $data->q = $params['query']; + $data->title = $params['filters']['title'] ?? ''; + $data->timestart = $params['filters']['timestart'] ?? 0; + $data->timeend = $params['filters']['timeend'] ?? 0; + $data->areaids = $params['filters']['areaids'] ?? []; + $data->courseids = $params['filters']['courseids'] ?? []; + $data->contextids = $params['filters']['contextids'] ?? []; + $data->userids = $params['filters']['userids'] ?? []; + $data->groupids = $params['filters']['groupids'] ?? []; + + $cat = $params['filters']['cat'] ?? ''; + if (\core_search\manager::is_search_area_categories_enabled()) { + $cat = \core_search\manager::get_search_area_category_by_name($cat); + } + if ($cat instanceof \core_search\area_category) { + $data->cat = $cat->get_name(); + } + + $docs = $search->paged_search($data, $page); + + $return = [ + 'totalcount' => $docs->totalcount, + 'warnings' => [], + 'results' => [] + ]; + + // Convert results to simple data structures. + if ($docs) { + foreach ($docs->results as $doc) { + $return['results'][] = $doc->export_doc($PAGE->get_renderer('core')); + } + } + return $return; + } + + /** + * Webservice returns. + * + * @return external_single_structure + */ + public static function execute_returns(): external_single_structure { + return new external_single_structure( + [ + 'totalcount' => new external_value(PARAM_INT, 'Total number of results'), + 'results' => new external_multiple_structure( + \core_search\external\document_exporter::get_read_structure() + ), + ] + ); + } +} diff --git a/search/classes/external/get_search_areas_list.php b/search/classes/external/get_search_areas_list.php new file mode 100644 index 00000000000..cda6e38c15d --- /dev/null +++ b/search/classes/external/get_search_areas_list.php @@ -0,0 +1,117 @@ +. + +namespace core_search\external; + +use core_external\external_api; +use core_external\external_function_parameters; +use core_external\external_single_structure; +use core_external\external_multiple_structure; +use core_external\external_value; +use core_external\external_warnings; +use \core_search\manager; +use moodle_exception; + +/** + * External function for return the list of search areas. + * + * @package core_search + * @copyright 2023 Juan Leyva + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * @since Moodle 4.3 + */ +class get_search_areas_list extends external_api { + + /** + * Webservice parameters. + * + * @return external_function_parameters + */ + public static function execute_parameters(): external_function_parameters { + return new external_function_parameters( + [ + 'cat' => new external_value(PARAM_NOTAGS, 'category to filter areas', VALUE_DEFAULT, ''), + ] + ); + } + + + /** + * Return list of search areas. + * + * @param string $cat category to filter areas + * @return array search areas and warnings + */ + public static function execute(string $cat = ''): array { + + $params = self::validate_parameters(self::execute_parameters(), ['cat' => $cat]); + + $system = \context_system::instance(); + external_api::validate_context($system); + + require_capability('moodle/search:query', $system); + + if (manager::is_global_search_enabled() === false) { + throw new moodle_exception('globalsearchdisabled', 'search'); + } + + $areas = []; + $allsearchareas = manager::get_search_area_categories(); + $enabledsearchareas = manager::get_search_areas_list(true); + + foreach ($allsearchareas as $categoryid => $searchareacategory) { + if (!empty($params['cat']) && $params['cat'] != $categoryid) { + continue; + } + + $searchareas = $searchareacategory->get_areas(); + $catname = $searchareacategory->get_visiblename(); + foreach ($searchareas as $areaid => $searcharea) { + if (key_exists($areaid, $enabledsearchareas)) { + $name = $searcharea->get_visible_name(); + $areas[$name] = ['id' => $areaid, 'name' => $name, 'categoryid' => $categoryid, 'categoryname' => $catname]; + } + } + } + + ksort($areas); + + return ['areas' => $areas, 'warnings' => []]; + } + + /** + * Webservice returns. + * + * @return external_single_structure + */ + public static function execute_returns(): external_single_structure { + return new external_single_structure( + [ + 'areas' => new external_multiple_structure( + new external_single_structure( + [ + 'id' => new external_value(PARAM_ALPHANUMEXT, 'search area id'), + 'categoryid' => new external_value(PARAM_NOTAGS, 'category id'), + 'categoryname' => new external_value(PARAM_NOTAGS, 'category name'), + 'name' => new external_value(PARAM_TEXT, 'search area name'), + ], 'Search area' + ), 'Search areas' + ), + 'warnings' => new external_warnings() + ] + ); + } +} diff --git a/search/classes/external/get_top_results.php b/search/classes/external/get_top_results.php new file mode 100644 index 00000000000..ffdf87771a1 --- /dev/null +++ b/search/classes/external/get_top_results.php @@ -0,0 +1,131 @@ +. + +namespace core_search\external; + +use core_external\external_api; +use core_external\external_function_parameters; +use core_external\external_single_structure; +use core_external\external_multiple_structure; +use moodle_exception; + +/** + * External function for retrieving top search results. + * + * @package core_search + * @copyright 2023 Juan Leyva + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * @since Moodle 4.3 + */ +class get_top_results extends external_api { + + /** + * Webservice parameters. + * + * @return external_function_parameters + */ + public static function execute_parameters(): external_function_parameters { + + $baseparameters = get_results::execute_parameters(); + + return new external_function_parameters( + [ + 'query' => $baseparameters->keys['query'], + 'filters' => $baseparameters->keys['filters'], + ] + ); + } + + /** + * Gets top search results based on the provided query and filters. + * + * @param string $query the search query + * @param array $filters filters to apply + * @return array search results + */ + public static function execute(string $query, array $filters = []): array { + global $PAGE; + + $params = self::validate_parameters(self::execute_parameters(), + [ + 'query' => $query, + 'filters' => $filters, + ] + ); + + $system = \context_system::instance(); + external_api::validate_context($system); + + require_capability('moodle/search:query', $system); + + if (\core_search\manager::is_global_search_enabled() === false) { + throw new moodle_exception('globalsearchdisabled', 'search'); + } + + $search = \core_search\manager::instance(); + + $data = new \stdClass(); + // First, mandatory parameters for consistency with web. + $data->q = $params['query']; + $data->title = $params['filters']['title'] ?? ''; + $data->timestart = $params['filters']['timestart'] ?? 0; + $data->timeend = $params['filters']['timeend'] ?? 0; + $data->areaids = $params['filters']['areaids'] ?? []; + $data->courseids = $params['filters']['courseids'] ?? []; + $data->contextids = $params['filters']['contextids'] ?? []; + $data->userids = $params['filters']['userids'] ?? []; + $data->groupids = $params['filters']['groupids'] ?? []; + + $cat = $params['filters']['cat'] ?? ''; + if (\core_search\manager::is_search_area_categories_enabled()) { + $cat = \core_search\manager::get_search_area_category_by_name($cat); + } + if ($cat instanceof \core_search\area_category) { + $data->cat = $cat->get_name(); + } + + $docs = $search->search_top($data); + + $return = [ + 'warnings' => [], + 'results' => [] + ]; + + // Convert results to simple data structures. + if ($docs) { + foreach ($docs as $doc) { + $return['results'][] = $doc->export_doc($PAGE->get_renderer('core')); + } + } + return $return; + } + + /** + * Webservice returns. + * + * @return external_single_structure + */ + public static function execute_returns(): external_single_structure { + + return new external_single_structure( + [ + 'results' => new external_multiple_structure( + \core_search\external\document_exporter::get_read_structure() + ), + ] + ); + } +} diff --git a/search/classes/external/view_results.php b/search/classes/external/view_results.php new file mode 100644 index 00000000000..ea0077cc5f5 --- /dev/null +++ b/search/classes/external/view_results.php @@ -0,0 +1,125 @@ +. + +namespace core_search\external; + +use core_external\external_api; +use core_external\external_function_parameters; +use core_external\external_single_structure; +use core_external\external_multiple_structure; +use core_external\external_value; +use core_external\external_warnings; +use moodle_exception; + +/** + * External function for trigger view search results event. + * + * @package core_search + * @copyright 2023 Juan Leyva + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * @since Moodle 4.3 + */ +class view_results extends external_api { + + /** + * Webservice parameters. + * + * @return external_function_parameters + */ + public static function execute_parameters(): external_function_parameters { + return new external_function_parameters( + [ + 'query' => new external_value(PARAM_NOTAGS, 'the search query'), + 'filters' => new external_single_structure( + [ + 'title' => new external_value(PARAM_NOTAGS, 'result title', VALUE_OPTIONAL), + 'areaids' => new external_multiple_structure( + new external_value(PARAM_RAW, 'areaid'), 'restrict results to these areas', VALUE_DEFAULT, [] + ), + 'courseids' => new external_multiple_structure( + new external_value(PARAM_INT, 'courseid'), 'restrict results to these courses', VALUE_DEFAULT, [] + ), + 'timestart' => new external_value(PARAM_INT, 'docs modified after this date', VALUE_DEFAULT, 0), + 'timeend' => new external_value(PARAM_INT, 'docs modified before this date', VALUE_DEFAULT, 0) + ], 'filters to apply', VALUE_DEFAULT, [] + ), + 'page' => new external_value(PARAM_INT, 'results page number starting from 0, defaults to the first page', + VALUE_DEFAULT, 0) + ] + ); + } + + /** + * Trigger view results event. + * + * @param string $query the search query + * @param array $filters filters to apply + * @param int $page results page + * @return array status and warnings + */ + public static function execute(string $query, array $filters = [], int $page = 0): array { + + $params = self::validate_parameters(self::execute_parameters(), + [ + 'query' => $query, + 'filters' => $filters, + 'page' => $page, + ] + ); + + $system = \context_system::instance(); + external_api::validate_context($system); + + require_capability('moodle/search:query', $system); + + if (\core_search\manager::is_global_search_enabled() === false) { + throw new moodle_exception('globalsearchdisabled', 'search'); + } + + $filters = new \stdClass(); + $filters->title = $params['filters']['title'] ?? ''; + $filters->timestart = $params['filters']['timestart'] ?? 0; + $filters->timeend = $params['filters']['timeend'] ?? 0; + $filters->areaids = $params['filters']['areaids'] ?? []; + $filters->courseids = $params['filters']['courseids'] ?? []; + + \core_search\manager::trigger_search_results_viewed([ + 'q' => $params['query'], + 'page' => $params['page'], + 'title' => !empty($filters->title) ? $filters->title : '', + 'areaids' => !empty($filters->areaids) ? $filters->areaids : [], + 'courseids' => !empty($filters->courseids) ? $filters->courseids : [], + 'timestart' => isset($filters->timestart) ? $filters->timestart : 0, + 'timeend' => isset($filters->timeend) ? $filters->timeend : 0 + ]); + + return ['status' => true, 'warnings' => []]; + } + + /** + * Webservice returns. + * + * @return external_single_structure + */ + public static function execute_returns(): external_single_structure { + return new external_single_structure( + [ + 'status' => new external_value(PARAM_BOOL, 'status: true if success'), + 'warnings' => new external_warnings() + ] + ); + } +} diff --git a/search/engine/simpledb/classes/engine.php b/search/engine/simpledb/classes/engine.php index 2531c613581..ff39920ad63 100644 --- a/search/engine/simpledb/classes/engine.php +++ b/search/engine/simpledb/classes/engine.php @@ -323,7 +323,7 @@ class engine extends \core_search\engine { * @return int */ public function get_query_total_count() { - if (!is_null($this->totalresults)) { + if (is_null($this->totalresults)) { // This is a just in case as we count total results in execute_query. return \core_search\manager::MAX_RESULTS; } diff --git a/search/engine/solr/classes/document.php b/search/engine/solr/classes/document.php index 4cdc8024776..6bf42778548 100644 --- a/search/engine/solr/classes/document.php +++ b/search/engine/solr/classes/document.php @@ -118,7 +118,7 @@ class document extends \core_search\document { } /** - * Overwritten to use markdown format as we use markdown for solr highlighting. + * Overwritten to use HTML (highlighting). * * @return int */ @@ -129,6 +129,11 @@ class document extends \core_search\document { /** * Formats a text string coming from the search engine. * + * Even if this is called through an external function it is fine to return HTML as + * HTML is considered solr's search engine text format. An external function can ask + * for raw text, but this just means that it will not pass through format_text, no that + * we can not add HTML. + * * @param string $text Text to format * @return string HTML text to be renderer */ diff --git a/search/templates/result.mustache b/search/templates/result.mustache index 2cb71983b74..4b5c7d8f433 100644 --- a/search/templates/result.mustache +++ b/search/templates/result.mustache @@ -41,6 +41,10 @@ * filename * multiplefiles * filenames + * itemid + * contextid + * userid + * timemodified Example context (json): { diff --git a/search/tests/external/get_results_test.php b/search/tests/external/get_results_test.php new file mode 100644 index 00000000000..414c212e359 --- /dev/null +++ b/search/tests/external/get_results_test.php @@ -0,0 +1,115 @@ +. + +namespace core_search\external; + +use core_external\external_api; + +defined('MOODLE_INTERNAL') || die(); + +global $CFG; +require_once($CFG->dirroot . '/webservice/tests/helpers.php'); + +/** + * Tests for the get_results external function. + * + * @package core_search + * @category test + * @copyright 2023 Juan Leyva (juan@moodle.com) + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * @coversDefaultClass \core_search\external\get_results + */ +class get_results_test extends \externallib_advanced_testcase { + + public function setUp(): void { + $this->resetAfterTest(); + } + + /** + * test external api + * @covers ::execute + * @return void + */ + public function test_external_get_results(): void { + + set_config('enableglobalsearch', true); + set_config('searchengine', 'simpledb'); + + $this->setAdminUser(); + + // Test search not returning anything (nothing in the index yet). + $return = external_api::clean_returnvalue(get_results::execute_returns(), get_results::execute('one')); + $this->assertEquals(0, $return['totalcount']); + + // Create an index of searchable things. + $generator = $this->getDataGenerator(); + $course = $generator->create_course(['fullname' => 'SearchTest course']); + $anothercourse = $generator->create_course(['fullname' => 'Another']); + $student = $this->getDataGenerator()->create_and_enrol($course, 'student'); + $studentothercourse = $this->getDataGenerator()->create_and_enrol($anothercourse, 'student'); + $page = $generator->create_module('page', ['course' => $course->id, 'name' => 'SearchTest page']); + $forum = $generator->create_module('forum', ['course' => $course->id]); + + $fgenerator = $generator->get_plugin_generator('mod_forum'); + + for ($i = 0; $i < 15; $i++) { + $fgenerator->create_discussion( + [ + 'course' => $course->id, + 'forum' => $forum->id, + 'userid' => $student->id, + ] + ); + } + + $search = \core_search\manager::instance(); + $search->index(); + + // Basic search, by text. + $return = external_api::clean_returnvalue(get_results::execute_returns(), get_results::execute('page')); + $this->assertEquals(1, $return['totalcount']); + $this->assertEquals('activity', $return['results'][0]['areaname']); + $this->assertEquals($page->name, $return['results'][0]['title']); + + // Basic search, by name containing text. + $return = external_api::clean_returnvalue(get_results::execute_returns(), get_results::execute('SearchTest')); + $this->assertEquals(2, $return['totalcount']); + + // Test pagination. + $return = external_api::clean_returnvalue(get_results::execute_returns(), get_results::execute('discussion', [], 0)); + $this->assertCount(10, $return['results']); // The first 10 posts of a total of 15 for the second page. + $this->assertEquals(15, $return['totalcount']); + + $return = external_api::clean_returnvalue(get_results::execute_returns(), get_results::execute('discussion', [], 1)); + $this->assertCount(5, $return['results']); // The last 5 posts of a total of 15 for the second page. + $this->assertEquals(15, $return['totalcount']); + + // Test some filters. + $return = external_api::clean_returnvalue(get_results::execute_returns(), + get_results::execute('discussion', ['title' => 'Discussion 11'])); + $this->assertEquals(1, $return['totalcount']); + + // No discussions created in the future. + $return = external_api::clean_returnvalue(get_results::execute_returns(), + get_results::execute('discussion', ['timestart' => time() + DAYSECS])); + $this->assertEquals(0, $return['totalcount']); + + // Basic permissions check. + $this->setUser($studentothercourse); + $return = external_api::clean_returnvalue(get_results::execute_returns(), get_results::execute('discussion', [], 1)); + $this->assertCount(0, $return['results']); // I should not see other courses discussions. + } +} diff --git a/search/tests/external/get_search_areas_list_test.php b/search/tests/external/get_search_areas_list_test.php new file mode 100644 index 00000000000..8ed8bb7492c --- /dev/null +++ b/search/tests/external/get_search_areas_list_test.php @@ -0,0 +1,68 @@ +. + +namespace core_search\external; + +use core_external\external_api; + +defined('MOODLE_INTERNAL') || die(); + +global $CFG; +require_once($CFG->dirroot . '/webservice/tests/helpers.php'); + +/** + * Tests for the get_search_areas_list external function. + * + * @package core_search + * @category test + * @copyright 2023 Juan Leyva (juan@moodle.com) + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * @coversDefaultClass \core_search\external\get_search_areas_list + */ +class get_search_areas_list_test extends \externallib_advanced_testcase { + + public function setUp(): void { + $this->resetAfterTest(); + } + + /** + * test external api + * + * @covers ::execute + * @return void + */ + public function test_external_get_search_areas_list(): void { + + set_config('enableglobalsearch', true); + set_config('searchenablecategories', true); + + $this->setAdminUser(); + + $result = get_search_areas_list::execute(); + $result = external_api::clean_returnvalue(get_search_areas_list::execute_returns(), $result); + + $this->assertNotEmpty($result['areas']); + $totalareas = count($result['areas']); + + // Filter. + $result = get_search_areas_list::execute('core-users'); + $result = external_api::clean_returnvalue(get_search_areas_list::execute_returns(), $result); + $totalfilterareas = count($result['areas']); + + // Just count numbers, plugins can inject areas. + $this->assertLessThan($totalareas, $totalfilterareas); + } +} diff --git a/search/tests/external/get_top_results_test.php b/search/tests/external/get_top_results_test.php new file mode 100644 index 00000000000..8e034d2f3c7 --- /dev/null +++ b/search/tests/external/get_top_results_test.php @@ -0,0 +1,92 @@ +. + +namespace core_search\external; + +use core_external\external_api; + +defined('MOODLE_INTERNAL') || die(); + +global $CFG; +require_once($CFG->dirroot . '/webservice/tests/helpers.php'); + +/** + * Tests for the get_top_results external function. + * + * @package core_search + * @category test + * @copyright 2023 Juan Leyva (juan@moodle.com) + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * @coversDefaultClass \core_search\external\get_top_results + */ +class get_top_results_test extends \externallib_advanced_testcase { + + public function setUp(): void { + $this->resetAfterTest(); + } + + /** + * test external api + * @covers ::execute + * @return void + */ + public function test_external_get_top_results(): void { + + set_config('enableglobalsearch', true); + set_config('searchenablecategories', true); // Required for top search. + set_config('searchmaxtopresults', 5); // Change default. + set_config('searchengine', 'simpledb'); + + $this->setAdminUser(); + + // Create an index of searchable things. + $generator = $this->getDataGenerator(); + $course = $generator->create_course(['fullname' => 'SearchTest course']); + $student = $this->getDataGenerator()->create_and_enrol($course, 'student'); + $forum = $generator->create_module('forum', ['course' => $course->id]); + $fgenerator = $generator->get_plugin_generator('mod_forum'); + + for ($i = 0; $i < 15; $i++) { + $fgenerator->create_discussion( + [ + 'course' => $course->id, + 'forum' => $forum->id, + 'userid' => $student->id, + ] + ); + } + + $search = \core_search\manager::instance(); + $search->index(); + + // Test top results. + $return = external_api::clean_returnvalue(get_top_results::execute_returns(), get_top_results::execute('discussion', [])); + $this->assertCount(5, $return['results']); // We get the 5 top results according to searchmaxtopresults setting value. + + set_config('searchmaxtopresults', 3); // Change to 3 top. + $return = external_api::clean_returnvalue(get_top_results::execute_returns(), get_top_results::execute('discussion', [])); + $this->assertCount(3, $return['results']); + + // Test some filters. + $return = external_api::clean_returnvalue(get_top_results::execute_returns(), + get_top_results::execute('discussion', ['title' => 'Discussion 11'])); + $this->assertCount(1, $return['results']); + + set_config('searchenablecategories', false); // Disable top search. + $return = external_api::clean_returnvalue(get_top_results::execute_returns(), get_top_results::execute('discussion', [])); + $this->assertCount(0, $return['results']); + } +} diff --git a/search/tests/external/view_results_test.php b/search/tests/external/view_results_test.php new file mode 100644 index 00000000000..76ba009833d --- /dev/null +++ b/search/tests/external/view_results_test.php @@ -0,0 +1,72 @@ +. + +namespace core_search\external; + +use core_external\external_api; + +defined('MOODLE_INTERNAL') || die(); + +global $CFG; +require_once($CFG->dirroot . '/webservice/tests/helpers.php'); + +/** + * Tests for the view_results external function. + * + * @package core_search + * @category test + * @copyright 2023 Juan Leyva (juan@moodle.com) + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * @coversDefaultClass \core_search\external\view_results + */ +class view_results_test extends \externallib_advanced_testcase { + + public function setUp(): void { + $this->resetAfterTest(); + } + + /** + * test external api + * @covers ::execute + * @return void + */ + public function test_external_view_results(): void { + + set_config('enableglobalsearch', true); + + $this->setAdminUser(); + // Trigger and capture the event. + $sink = $this->redirectEvents(); + + $result = view_results::execute('forum post', ['title' => 'My progress'], 1); + $result = external_api::clean_returnvalue(view_results::execute_returns(), $result); + $this->assertEmpty($result['warnings']); + $this->assertTrue($result['status']); + + $events = $sink->get_events(); + $this->assertCount(1, $events); + $event = array_shift($events); + $sink->close(); + + // Checking that the event contains the expected values. + $this->assertInstanceOf('core\event\search_results_viewed', $event); + $this->assertEventContextNotUsed($event); + $this->assertNotEmpty($event->get_name()); + $this->assertEquals('forum post', $event->get_data()['other']['q']); + $this->assertEquals('My progress', $event->get_data()['other']['title']); + $this->assertEquals(1, $event->get_data()['other']['page']); + } +} diff --git a/search/tests/fixtures/mock_search_engine.php b/search/tests/fixtures/mock_search_engine.php index 6e51b6997ad..0b8b3e12ffc 100644 --- a/search/tests/fixtures/mock_search_engine.php +++ b/search/tests/fixtures/mock_search_engine.php @@ -61,6 +61,7 @@ class engine extends \core_search\engine { public function execute_query($data, $usercontexts, $limit = 0) { // No need to implement. + return []; } public function delete($areaid = null) { diff --git a/version.php b/version.php index 33774f30a15..aa04faf6a3e 100644 --- a/version.php +++ b/version.php @@ -29,7 +29,7 @@ defined('MOODLE_INTERNAL') || die(); -$version = 2023082600.03; // YYYYMMDD = weekly release date of this DEV branch. +$version = 2023082600.04; // YYYYMMDD = weekly release date of this DEV branch. // RR = release increments - 00 in DEV branches. // .XX = incremental changes. $release = '4.3dev+ (Build: 20230826)'; // Human-friendly version name diff --git a/webservice/externallib.php b/webservice/externallib.php index 95ec4cbb8d0..42731b1e5de 100644 --- a/webservice/externallib.php +++ b/webservice/externallib.php @@ -156,8 +156,9 @@ class core_webservice_external extends \core_external\external_api { $siteinfo['mobilecssurl'] = !empty($CFG->mobilecssurl) ? $CFG->mobilecssurl : ''; // Retrieve some advanced features. Only enable/disable ones (bool). - $advancedfeatures = array("usecomments", "usetags", "enablenotes", "messaging", "enableblogs", - "enablecompletion", "enablebadges", "messagingallusers", "enablecustomreports"); + $advancedfeatures = ["usecomments", "usetags", "enablenotes", "messaging", "enableblogs", + "enablecompletion", "enablebadges", "messagingallusers", "enablecustomreports", "enableglobalsearch"]; + foreach ($advancedfeatures as $feature) { if (isset($CFG->{$feature})) { $siteinfo['advancedfeatures'][] = array(