diff --git a/lib/db/services.php b/lib/db/services.php index 31eb43e1d1e..1cc553d8509 100644 --- a/lib/db/services.php +++ b/lib/db/services.php @@ -1706,27 +1706,34 @@ $functions = array( 'type' => 'read', 'ajax' => true ), - 'core_search_get_results' => array( + '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' => array( + ], + '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' => array( + ], + '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/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 index 215a85d7209..ef013061fc0 100644 --- a/search/classes/external/get_results.php +++ b/search/classes/external/get_results.php @@ -150,39 +150,8 @@ class get_results extends external_api { [ 'totalcount' => new external_value(PARAM_INT, 'Total number of results'), 'results' => new external_multiple_structure( - new external_single_structure( - [ - 'itemid' => new external_value(PARAM_INT, 'unique id in the search area scope'), - 'componentname' => new external_value(PARAM_ALPHANUMEXT, 'component name'), - 'areaname' => new external_value(PARAM_ALPHANUMEXT, 'search area name'), - 'courseurl' => new external_value(PARAM_URL, 'result course url'), - 'coursefullname' => new external_value(PARAM_RAW, 'result course fullname'), - 'timemodified' => new external_value(PARAM_INT, 'result modified time'), - 'title' => new external_value(PARAM_RAW, 'result title'), - 'docurl' => new external_value(PARAM_URL, 'result url'), - 'iconurl' => new external_value(PARAM_URL, 'icon url', VALUE_OPTIONAL), - 'content' => new external_value(PARAM_RAW, 'result contents', VALUE_OPTIONAL), - 'contextid' => new external_value(PARAM_INT, 'result context id'), - 'contexturl' => new external_value(PARAM_URL, 'result context url'), - 'description1' => new external_value(PARAM_RAW, 'extra result contents, depends on the search area', - VALUE_OPTIONAL), - 'description2' => new external_value(PARAM_RAW, 'extra result contents, depends on the search area', - VALUE_OPTIONAL), - 'multiplefiles' => new external_value(PARAM_INT, 'whether multiple files are returned or not', - VALUE_OPTIONAL), - 'filenames' => new external_multiple_structure( - new external_value(PARAM_RAW, 'result file name', VALUE_OPTIONAL) - , 'result file names if present', - VALUE_OPTIONAL - ), - 'filename' => new external_value(PARAM_RAW, 'result file name if present', VALUE_OPTIONAL), - 'userid' => new external_value(PARAM_INT, 'user id', VALUE_OPTIONAL), - 'userurl' => new external_value(PARAM_URL, 'user url', VALUE_OPTIONAL), - 'userfullname' => new external_value(PARAM_RAW, 'user fullname', VALUE_OPTIONAL), - 'textformat' => new external_value(PARAM_INT, 'text fields format, it is the same for all of them') - ], 'Search result' - ), 'Search results', VALUE_OPTIONAL - ) + \core_search\external\document_exporter::get_read_structure() + ), ] ); } 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/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']); + } +}