aboutsummaryrefslogtreecommitdiff
path: root/mod/search
diff options
context:
space:
mode:
Diffstat (limited to 'mod/search')
-rw-r--r--mod/search/README.txt276
-rw-r--r--mod/search/index.php375
-rw-r--r--mod/search/languages/en.php23
-rw-r--r--mod/search/manifest.xml24
-rw-r--r--mod/search/pages/search/index.php275
-rw-r--r--mod/search/search_hooks.php425
-rw-r--r--mod/search/start.php663
-rw-r--r--mod/search/views/default/page_elements/searchbox.php4
-rw-r--r--mod/search/views/default/search/comments/entity.php55
-rw-r--r--mod/search/views/default/search/comments/listing.php62
-rw-r--r--mod/search/views/default/search/css.php158
-rw-r--r--mod/search/views/default/search/entity.php57
-rw-r--r--mod/search/views/default/search/entity_list.php66
-rw-r--r--mod/search/views/default/search/gallery.php55
-rw-r--r--mod/search/views/default/search/gallery_listing.php16
-rw-r--r--mod/search/views/default/search/header.php6
-rw-r--r--mod/search/views/default/search/layout.php8
-rw-r--r--mod/search/views/default/search/list.php115
-rw-r--r--mod/search/views/default/search/listing.php68
-rw-r--r--mod/search/views/default/search/no_results.php6
-rw-r--r--mod/search/views/default/search/search_box.php43
-rw-r--r--mod/search/views/default/search/startblurb.php13
-rw-r--r--mod/search/views/default/search/tags/listing.php55
-rw-r--r--mod/search/views/rss/search/comments/entity.php54
-rw-r--r--mod/search/views/rss/search/entity.php24
-rw-r--r--mod/search/views/rss/search/layout.php6
-rw-r--r--mod/search/views/rss/search/list.php25
27 files changed, 1668 insertions, 1289 deletions
diff --git a/mod/search/README.txt b/mod/search/README.txt
new file mode 100644
index 000000000..ac5930e5f
--- /dev/null
+++ b/mod/search/README.txt
@@ -0,0 +1,276 @@
+= Elgg Search =
+
+Full text search developer's reference.
+
+== Contents ==
+
+1. Overview
+2. Search and Custom Plugins
+3. Controlling Search Results
+ 1. Entities Returned
+ 2. Custom Search
+4. Controlling Search Views
+ 1. Entities
+ 2. Listing
+ 3. Layout
+5. Hints and Quirks
+
+
+== 1. Overview ==
+
+All entities are searched through title and description using
+MySQL's native fulltext search when possible, and `LIKE %...%` when not.
+This can be overridden on a type/subtype basis.
+
+Entities are displayed in a standard list view consisting of a
+title, blurb, and icon of the owning entity. This can be overridden
+on a type/subtype basis.
+
+Search is separated based upon types/subtypes pairs and any
+registered custom search.
+
+**METADATA, ANNOTATIONS, AND PRIVATE DATA ARE NOT SEARCHED BY DEFAULT!**
+
+These are used in a variety of ways by plugin authors and generally
+should not be displayed. There are exceptions (profile fields and
+comments) but if a plugin needs to match against metadata,
+annotations, or private data it must register a search hook itself.
+
+
+== 2. Search and Custom Plugins ==
+
+To appear in search you must register your entity type and subtype
+by saying in your plugin's init function:
+
+ register_entity_type($type, $subtype);
+
+If your plugin uses ElggEntity's standard title and description,
+and you don't need a custom display, there is nothing else you need
+to do for your results to appear in search. If you would like more
+granular control of search, continue below.
+
+
+== 3. Controlling Search Results ==
+
+Search results can be controlled at a object:subtype level.
+
+You can specify your own search types by responding to a hook.
+
+
+=== 3.1 Controlling Search Results - Entities Returned ===
+
+You can override the default search by responding to the search/type
+or search/type:subtype hook. Generally, you will be replying to
+search/object:subtype.
+
+Search will first trigger a hook for search/type:subtype. If no
+results are returned (but not FALSE, see below) a hook for search/type
+will be triggered.
+
+FALSE returned for any search hook will halt results for that
+type/subtype.
+
+Register plugin hooks like this:
+
+ register_plugin_hook('search', 'object:my_subtype',
+ 'my_subtype_search_hook');
+
+The hooked function is provided with details about the search query
+in $param. These include:
+
+* query
+* offset
+* limit
+* search_type
+* type - Entity type. (Not applicable for custom searches)
+* subtype - Entity subtype. (Not applicable for custom searches)
+* owner_guid
+* friends - Should only entities by friends of the logged in
+ user be searched? (@todo)
+* pagination - Show pagination?
+
+The hooked function should respond to search triggers with the
+following:
+
+ array(
+ 'count' => A count of ALL entities found,
+ 'entities' => An array of entities.
+ )
+
+This information is passed directly to the search view, so if you are
+registering your own custom hook, you can provide more
+information to display in your custom view.
+
+For each entity in the returned array, search expects two pieces of
+volatile data: search_matched_title and search_matched_description.
+Set these by saying:
+
+ $entity->setVolatileData('data_name', 'data_value');
+
+Again, if you are customizing your search views, you can add anything
+you need.
+
+
+=== 3.2 Controlling Search Results - Custom Search ===
+
+Non-entities, including information from 3rd party applications,
+can easily be included in search by registering a custom search hook
+that responds to the search_types/get_types trigger:
+
+ register_plugin_hook('search_types', 'get_types',
+ 'my_custom_search_hook_function');
+
+In this function, append to the array sent in $value with the name of
+your custom search:
+
+ function my_custom_search_hook_function($hook, $type,
+ $value, $params) {
+
+ $value[] = 'my_custom_search';
+ return $value;
+ }
+
+Search will trigger a hook for search/my_custom_search, which your
+plugin should respond to as detailed in section 3.1 above.
+
+
+== 4.0 Controlling Search Views ==
+Three types views are used for displaying search: entity, listing,
+and layout.
+
+Each view has a default that standardizes the display of entities
+regardless of type, subtype, or search type.
+
+The entity and listing views can be customized based upon a type,
+subtype, or custom search type of the results.
+
+The layout view can be customized based upon the original search
+type. NB: This can be different to the types for the results.
+
+The entity view controls how each individual result is formatted.
+
+The listing view controls how each group of listings is formatted.
+
+The listing layout controls how each full result set is formatted.
+
+
+=== 4.1 Controlling Search Views - Entities ===
+
+The default view for entities is search/entity.
+
+Search views are separate from the object/entity views because
+view types might not match entity types.
+
+The default search listing view iterates through each entity
+found and passes to the entity view. See 3.3 for more information
+about listing views.
+
+Entity views are discovered in the following order. The first search
+view found is used.
+
+ search/type/subtype/entity (For entity-based searches only)
+ search/type/entity
+ search/entity
+
+The following parameters are passed in $vars to the entity view by
+the default listing view:
+
+ entity => The current returned entity
+ results => The results from the search/type:subtype hook
+ params => The params passed to the search/type:subtype hook
+
+Example: To create an entity view for an ElggObject of subtype blog,
+create a file called:
+
+ views/default/search/object/blog/entity.php
+
+To create an entity view for a custom search mysearch, create a file
+called:
+
+ views/default/search/mysearch/entity.php
+
+
+=== 4.2 Controlling Search Views - Listing
+
+The default search view is search/listing.
+
+For each entity in the returned array, search expects two pieces of
+volatile data: search_matched_title and search_matched_description.
+
+Listing views are discovered in the following order. The first
+search view found is used.
+ search/type/subtype/listing (For entity-based searches only)
+ search/type/listing
+ search/listing
+
+The view is called with the following in $vars:
+ results => The results from the search/type:subtype hook
+ params => The params passed to the search/type:subtype hook
+
+Example: To create a listing view for ElggObjects with the subtype
+of blog, create a file called:
+
+ views/default/search/object/blog/listing.php
+
+To create a listing view for the custom search mysearch, create a file
+called:
+
+ views/default/search/mysearch/listing.php
+
+
+=== 4.3 Controlling Search Views - Layout ===
+
+The default layout view for search is search/layout, which calls
+to `elgg_view_layout(two_column_left_sidebar', '', $entity_results);`
+
+Layouts can be overridden only when not searching all entities.
+
+Layout views are discovered in the following order. The first search
+view found is used.
+
+ search/type/subtype/layout (For entity-based searches only)
+ search/type/layout
+ search/layout
+
+The following parameters are passed in $vars to the layout view:
+
+ body => The HTML formatted list of results.
+ params => The original params for the search.
+
+Example: To create a layout view for ElggObjects with the subtype
+of blog, create a file called:
+
+ views/default/search/object/blog/layout.php
+
+To create a layout view for the custom search mysearch, create a file
+called:
+
+ views/default/search/mysearch/layout.php
+
+
+== 5. Hints and Quirks ==
+
+Use search_get_relevant_substring() to extract and highlight
+relevant substrings for the search_match_title and description.
+
+If searching in 3rd party applications, create a temporary
+ElggObject to hold the results. No need to save it since search
+uses volatile data.
+
+ $entity = new ElggObject();
+ $entity->owner_guid = use_magic_to_match_to_a_real_user();
+ $entity->setVolatileData('search_matched_title',
+ '3rd Party Integration');
+ $entity->setVolatileData('search_matched_description',
+ 'Searching is fun!');
+
+ return array(
+ 'count' => $count,
+ 'entities' => array($entity)
+ );
+
+MySQL's fulltext engine returns *ZERO* rows if more than 50% of
+the rows searched match.
+
+The default search hooks for users and groups ignore subtypes.
+See [GitHub issue 1499](https://github.com/elgg/elgg/issues/1499)
diff --git a/mod/search/index.php b/mod/search/index.php
deleted file mode 100644
index ee6da32a8..000000000
--- a/mod/search/index.php
+++ /dev/null
@@ -1,375 +0,0 @@
-<?php
-
-// $search_type == all || entities || trigger plugin hook
-$search_type = get_input('search_type', 'all');
-
-// @todo there is a bug in get_input that makes variables have slashes sometimes.
-$query = stripslashes(get_input('q', get_input('tag', '', FALSE), FALSE));
-
-// get limit and offset. override if on search dashboard, where only 2
-// of each most recent entity types will be shown.
-$limit = ($search_type == 'all') ? 2 : get_input('limit', 10);
-$offset = ($search_type == 'all') ? 0 : get_input('offset', 0);
-
-$entity_type = get_input('entity_type', NULL);
-$entity_subtype = get_input('entity_subtype', NULL);
-$owner_guid = get_input('owner_guid', NULL);
-$friends = (int)get_input('friends', 0);
-
-// set up search params
-$params = array(
- 'query' => $query,
- 'offset' => $offset,
- 'limit' => $limit,
- 'search_type' => $search_type,
- 'type' => $entity_type,
- 'subtype' => $entity_subtype,
-// 'tag_type' => $tag_type,
- 'owner_guid' => $owner_guid,
-// 'friends' => $friends
-);
-
-$results_html = '';
-//$results_html .= elgg_view_title(elgg_echo('search:results')) . "<input type=\"text\" value=\"$query\" />";
-$results_html .= elgg_view_title(elgg_echo('search:results'));
-$types = get_registered_entity_types();
-$custom_types = trigger_plugin_hook('search_types', 'get_types', $params, array());
-
-// add submenu items for all and native types
-// @todo should these maintain any existing type / subtype filters or reset?
-$data = htmlspecialchars(http_build_query(array(
- 'q' => $query,
- 'entity_subtype' => $subtype,
- 'entity_type' => $type,
- 'owner_guid' => $owner_guid,
- 'search_type' => 'all',
- 'friends' => $friends
-)));
-$url = "{$CONFIG->wwwroot}pg/search/?$data";
-add_submenu_item(elgg_echo('all'), $url);
-
-foreach ($types as $type => $subtypes) {
- // @todo when using index table, can include result counts on each of these.
- if (is_array($subtypes) && count($subtypes)) {
- foreach ($subtypes as $subtype) {
- $label = "item:$type:$subtype";
-
- $data = htmlspecialchars(http_build_query(array(
- 'q' => $query,
- 'entity_subtype' => $subtype,
- 'entity_type' => $type,
- 'owner_guid' => $owner_guid,
- 'search_type' => 'entities',
- 'friends' => $friends
- )));
-
- $url = "{$CONFIG->wwwroot}pg/search/?$data";
-
- add_submenu_item(elgg_echo($label), $url);
- }
- } else {
- $label = "item:$type";
-
- $data = htmlspecialchars(http_build_query(array(
- 'q' => $query,
- 'entity_type' => $type,
- 'owner_guid' => $owner_guid,
- 'search_type' => 'entities',
- 'friends' => $friends
- )));
-
- $url = "{$CONFIG->wwwroot}pg/search/?$data";
-
- add_submenu_item(elgg_echo($label), $url);
- }
-}
-
-// add submenu for custom searches
-foreach ($custom_types as $type) {
- $label = "search_types:$type";
-
- $data = htmlspecialchars(http_build_query(array(
- 'q' => $query,
- 'entity_subtype' => $entity_subtype,
- 'entity_type' => $entity_type,
- 'owner_guid' => $owner_guid,
- 'search_type' => $type,
- 'friends' => $friends
- )));
-
- $url = "{$CONFIG->wwwroot}pg/search/?$data";
-
- add_submenu_item(elgg_echo($label), $url);
-}
-
-
-// check that we have an actual query
-if (!$query) {
- $body .= "No query.";
- $layout = elgg_view_layout('two_column_left_sidebar', '', $body);
- page_draw($title, $layout);
-
- return;
-}
-
-if ($search_type == 'all' || $search_type == 'entities') {
- // to pass the correct search type to the views
- $params['search_type'] = 'entities';
-
- // foreach through types.
- // if a plugin returns FALSE for subtype ignore it.
- // if a plugin returns NULL or '' for subtype, pass to generic type search function.
- // if still NULL or '' or empty(array()) no results found. (== don't show??)
- foreach ($types as $type => $subtypes) {
- if ($search_type != 'all' && $entity_type != $type) {
- continue;
- }
-
- if (is_array($subtypes) && count($subtypes)) {
- foreach ($subtypes as $subtype) {
- // no need to search if we're not interested in these results
- // @todo when using index table, allow search to get full count.
- if ($search_type != 'all' && $entity_subtype != $subtype) {
- continue;
- }
- $params['subtype'] = $subtype;
- $params['type'] = $type;
-
- $entities = trigger_plugin_hook('search', "$type:$subtype", $params, NULL);
- if ($entities === FALSE) {
- // someone is saying not to display these types in searches.
- continue;
- } elseif (is_array($entities) && !count($entities)) {
- // no results, but results searched in hook.
- } elseif (!$entities) {
- // no results and not hooked. use default type search.
- // don't change the params here, since it's really a different subtype.
- // Will be passed to elgg_get_entities().
- $results = trigger_plugin_hook('search', $type, $params, array());
- }
-
- if (is_array($results['entities']) && $results['count']) {
- $results_html .= search_get_listing_html($results['entities'], $results['count'], $params);
- }
- }
- }
-
- // pull in default type entities with no subtypes
- // @todo this might currently means "all entities regardless of subtype"
- $params['type'] = $type;
- $params['subtype'] = 0;
-
- $results = trigger_plugin_hook('search', $type, $params, array());
- if ($results === FALSE) {
- // someone is saying not to display these types in searches.
- continue;
- }
-
- if (is_array($results['entities']) && $results['count']) {
- $results_html .= search_get_listing_html($results['entities'], $results['count'], $params);
- }
- }
-}
-
-// call custom searches
-if ($search_type != 'entities' || $search_type == 'all') {
- // get custom search types
- $types = trigger_plugin_hook('search_types', 'get_types', $params, array());
-
- if (is_array($types)) {
- foreach ($types as $type) {
- if ($search_type != 'all' && $search_type != $type) {
- continue;
- }
-
- $params['search_type'] = $type;
- unset($params['subtype']);
-
- $results = trigger_plugin_hook('search', $type, $params, array());
-
- if ($results === FALSE) {
- // someone is saying not to display these types in searches.
- continue;
- }
-
- if (is_array($results['entities']) && $results['count']) {
- $results_html .= search_get_listing_html($results['entities'], $results['count'], $params);
- }
- }
- }
-}
-
-//if ($search_type !== 'all') {
-// var_dump('here');
-// $entities = trigger_plugin_hook('search', $search_type, '', $return);
-//}
-/*
-
-call search_section_start to display long bar with types and titles
-call search
-
-*/
-
-if (!$results_html) {
- $body = elgg_echo('search:no_results');
-} else {
- $body = $results_html;
-}
-
-$layout = elgg_view_layout('two_column_left_sidebar', '', $body);
-
-page_draw($title, $layout);
-
-
-
-
-
-
-
-return;
-
-
-/** Main search page */
-
-global $CONFIG;
-
-$tag = get_input('tag');
-$offset = get_input('offset', 0);
-$viewtype = get_input('search_viewtype','list');
-if ($viewtype == 'gallery') {
- $limit = get_input('limit', 12); // 10 items in list view
-} else {
- $limit = get_input('limit', 10); // 12 items in gallery view
-}
-$searchtype = get_input('searchtype', 'all');
-$type = get_input('type', '');
-$subtype = get_input('subtype', '');
-$owner_guid = get_input('owner_guid', '');
-$tagtype = get_input('tagtype', '');
-$friends = (int)get_input('friends', 0);
-$title = sprintf(elgg_echo('searchtitle'), $tag);
-
-if (substr_count($owner_guid, ',')) {
- $owner_guid_array = explode(',', $owner_guid);
-} else {
- $owner_guid_array = $owner_guid;
-}
-if ($friends > 0) {
- if ($friends = get_user_friends($friends, '', 9999)) {
- $owner_guid_array = array();
- foreach($friends as $friend) {
- $owner_guid_array[] = $friend->guid;
- }
- } else {
- $owner_guid = -1;
- }
-}
-
-// Set up submenus
-if ($types = get_registered_entity_types()) {
- foreach($types as $ot => $subtype_array) {
- if (is_array($subtype_array) && count($subtype_array)) {
- foreach($subtype_array as $object_subtype) {
- $label = 'item:' . $ot;
- if (!empty($object_subtype)) {
- $label .= ':' . $object_subtype;
- }
-
- $data = http_build_query(array(
- 'tag' => urlencode($tag),
- 'subtype' => $object_subtype,
- 'type' => urlencode($ot),
- //'tagtype' => urlencode($md_type),
- 'owner_guid' => urlencode($owner_guid)
- ));
-
- $url = "{$CONFIG->wwwroot}pg/search/?$data";
-
- add_submenu_item(elgg_echo($label), $url);
- }
- }
- }
-
- $data = http_build_query(array(
- 'tag' => urlencode($tag),
- 'owner_guid' => urlencode($owner_guid)
- ));
-
- add_submenu_item(elgg_echo('all'), "{$CONFIG->wwwroot}pg/search/?$data");
-}
-
-// pull in search types for external or aggregated searches.
-if ($search_types = trigger_plugin_hook('search', 'types', '', NULL, array())) {
-
-}
-
-$body = '';
-if (!empty($tag)) {
- // start with blank results.
- $results = array(
- 'entities' => array(),
- 'total' => 0
- );
-
- // do the actual searchts
- $params = array(
- 'tag' => $tag,
- 'offset' => $offset,
- 'limit' => $limit,
- 'searchtype' => $searchtype,
- 'type' => $type,
- 'subtype' => $subtype,
- 'tagtype' => $tagtype,
- 'owner_guid' => $owner_guid_array
- );
-
- $results = trigger_plugin_hook('search', 'entities', $params, $results);
-
- if (empty($type) && empty($subtype)) {
- $title = sprintf(elgg_echo('searchtitle'),$tag);
- } else {
- if (empty($type)) {
- $type = 'object';
- }
- $itemtitle = 'item:' . $type;
- if (!empty($subtype)) {
- $itemtitle .= ':' . $subtype;
- }
- $itemtitle = elgg_echo($itemtitle);
- $title = sprintf(elgg_echo('advancedsearchtitle'),$itemtitle,$tag);
- }
-
- $body .= elgg_view_title($title); // elgg_view_title(sprintf(elgg_echo('searchtitle'),$tag));
-
- // call the old (now-deprecated) search hook here
- $body .= trigger_plugin_hook('search','',$tag, '');
-
- $body .= elgg_view('search/startblurb', array('query' => $query));
-
- if ($results->total > 0) {
- $body .= elgg_view('search/entity_list', array(
- 'entities' => $results->entities,
- 'count' => $results->total,
- 'offset' => $offset,
- 'limit' => $limit,
- 'baseurl' => $_SERVER['REQUEST_URI'],
- 'fullview' => false,
- 'context' => 'search',
- 'viewtypetoggle' => true,
- 'viewtype' => $viewtype,
- 'pagination' => true
- ));
- } else {
- $body .= elgg_view('page_elements/contentwrapper', array('body' => elgg_echo('search:noresults')));
- }
-
- elgg_view_entity_list($results->entities, count($results->entities), 0, count($results->entities), false);
-} else {
- // if no tag was given, give the user a box to input a search term
- $body .= elgg_view_title(elgg_echo('search:enterterm'));
- $body .= elgg_view('page_elements/contentwrapper', array('body' => '<div>' . elgg_view('page_elements/searchbox') . '</div>'));
-}
-
-$layout = elgg_view_layout('two_column_left_sidebar','',$body);
-
-page_draw($title, $layout); \ No newline at end of file
diff --git a/mod/search/languages/en.php b/mod/search/languages/en.php
index 52c29c7ed..e3899500f 100644
--- a/mod/search/languages/en.php
+++ b/mod/search/languages/en.php
@@ -1,10 +1,21 @@
<?php
- $language_array = array('search:enterterm' => 'Enter a search term:',
- 'search:noresults' => 'No results.',
- 'search:matched' => 'Matched: '
- );
+$english = array(
+ 'search:enter_term' => 'Enter a search term:',
+ 'search:no_results' => 'No results.',
+ 'search:matched' => 'Matched: ',
+ 'search:results' => 'Results for %s',
+ 'search:no_query' => 'Please enter a query to search.',
+ 'search:search_error' => 'Error',
-add_translation('en', $language_array);
+ 'search:more' => '+%s more %s',
-?>
+ 'search_types:tags' => 'Tags',
+
+ 'search_types:comments' => 'Comments',
+ 'search:comment_on' => 'Comment on "%s"',
+ 'search:comment_by' => 'by',
+ 'search:unavailable_entity' => 'Unavailable Entity',
+);
+
+add_translation('en', $english);
diff --git a/mod/search/manifest.xml b/mod/search/manifest.xml
index 1f8415445..513d3a6b9 100644
--- a/mod/search/manifest.xml
+++ b/mod/search/manifest.xml
@@ -1,10 +1,16 @@
<?xml version="1.0" encoding="UTF-8"?>
-<plugin_manifest>
- <field key="author" value="Curverider Ltd, The MITRE Corporation" />
- <field key="version" value="1.7" />
- <field key="description" value="Allow search across entities of the site" />
- <field key="website" value="http://www.elgg.org/" />
- <field key="copyright" value="(C) Curverider 2008-2009, MITRE 2009" />
- <field key="licence" value="GNU Public License version 2" />
- <field key="elgg_version" value="2009030702" />
-</plugin_manifest> \ No newline at end of file
+<plugin_manifest xmlns="http://www.elgg.org/plugin_manifest/1.8">
+ <name>Search</name>
+ <author>The MITRE Corporation, Core developers</author>
+ <version>1.8</version>
+ <category>bundled</category>
+ <description>Allow search across entities of the site</description>
+ <website>http://www.elgg.org/</website>
+ <copyright>See COPYRIGHT.txt</copyright>
+ <license>GNU General Public License version 2</license>
+ <requires>
+ <type>elgg_release</type>
+ <version>1.8</version>
+ </requires>
+ <activate_on_install>true</activate_on_install>
+</plugin_manifest>
diff --git a/mod/search/pages/search/index.php b/mod/search/pages/search/index.php
new file mode 100644
index 000000000..9542e0751
--- /dev/null
+++ b/mod/search/pages/search/index.php
@@ -0,0 +1,275 @@
+<?php
+/**
+ * Elgg search page
+ *
+ * @todo much of this code should be pulled out into a library of functions
+ */
+
+// Search supports RSS
+global $autofeed;
+$autofeed = true;
+
+// $search_type == all || entities || trigger plugin hook
+$search_type = get_input('search_type', 'all');
+
+// @todo there is a bug in get_input that makes variables have slashes sometimes.
+// @todo is there an example query to demonstrate ^
+// XSS protection is more important that searching for HTML.
+$query = stripslashes(get_input('q', get_input('tag', '')));
+
+$display_query = _elgg_get_display_query($query);
+
+// check that we have an actual query
+if (!$query) {
+ $title = sprintf(elgg_echo('search:results'), "\"$display_query\"");
+
+ $body = elgg_view_title(elgg_echo('search:search_error'));
+ $body .= elgg_echo('search:no_query');
+ $layout = elgg_view_layout('one_sidebar', array('content' => $body));
+ echo elgg_view_page($title, $layout);
+
+ return;
+}
+
+// get limit and offset. override if on search dashboard, where only 2
+// of each most recent entity types will be shown.
+$limit = ($search_type == 'all') ? 2 : get_input('limit', 10);
+$offset = ($search_type == 'all') ? 0 : get_input('offset', 0);
+
+$entity_type = get_input('entity_type', ELGG_ENTITIES_ANY_VALUE);
+$entity_subtype = get_input('entity_subtype', ELGG_ENTITIES_ANY_VALUE);
+$owner_guid = get_input('owner_guid', ELGG_ENTITIES_ANY_VALUE);
+$container_guid = get_input('container_guid', ELGG_ENTITIES_ANY_VALUE);
+$friends = get_input('friends', ELGG_ENTITIES_ANY_VALUE);
+$sort = get_input('sort');
+switch ($sort) {
+ case 'relevance':
+ case 'created':
+ case 'updated':
+ case 'action_on':
+ case 'alpha':
+ break;
+
+ default:
+ $sort = 'relevance';
+ break;
+}
+
+$order = get_input('order', 'desc');
+if ($order != 'asc' && $order != 'desc') {
+ $order = 'desc';
+}
+
+// set up search params
+$params = array(
+ 'query' => $query,
+ 'offset' => $offset,
+ 'limit' => $limit,
+ 'sort' => $sort,
+ 'order' => $order,
+ 'search_type' => $search_type,
+ 'type' => $entity_type,
+ 'subtype' => $entity_subtype,
+// 'tag_type' => $tag_type,
+ 'owner_guid' => $owner_guid,
+ 'container_guid' => $container_guid,
+// 'friends' => $friends
+ 'pagination' => ($search_type == 'all') ? FALSE : TRUE
+);
+
+$types = get_registered_entity_types();
+$custom_types = elgg_trigger_plugin_hook('search_types', 'get_types', $params, array());
+
+// add sidebar items for all and native types
+// @todo should these maintain any existing type / subtype filters or reset?
+$data = htmlspecialchars(http_build_query(array(
+ 'q' => $query,
+ 'entity_subtype' => $entity_subtype,
+ 'entity_type' => $entity_type,
+ 'owner_guid' => $owner_guid,
+ 'search_type' => 'all',
+ //'friends' => $friends
+)));
+$url = elgg_get_site_url() . "search?$data";
+$menu_item = new ElggMenuItem('all', elgg_echo('all'), $url);
+elgg_register_menu_item('page', $menu_item);
+
+foreach ($types as $type => $subtypes) {
+ // @todo when using index table, can include result counts on each of these.
+ if (is_array($subtypes) && count($subtypes)) {
+ foreach ($subtypes as $subtype) {
+ $label = "item:$type:$subtype";
+
+ $data = htmlspecialchars(http_build_query(array(
+ 'q' => $query,
+ 'entity_subtype' => $subtype,
+ 'entity_type' => $type,
+ 'owner_guid' => $owner_guid,
+ 'search_type' => 'entities',
+ 'friends' => $friends
+ )));
+
+ $url = elgg_get_site_url()."search?$data";
+ $menu_item = new ElggMenuItem($label, elgg_echo($label), $url);
+ elgg_register_menu_item('page', $menu_item);
+ }
+ } else {
+ $label = "item:$type";
+
+ $data = htmlspecialchars(http_build_query(array(
+ 'q' => $query,
+ 'entity_type' => $type,
+ 'owner_guid' => $owner_guid,
+ 'search_type' => 'entities',
+ 'friends' => $friends
+ )));
+
+ $url = elgg_get_site_url() . "search?$data";
+
+ $menu_item = new ElggMenuItem($label, elgg_echo($label), $url);
+ elgg_register_menu_item('page', $menu_item);
+ }
+}
+
+// add sidebar for custom searches
+foreach ($custom_types as $type) {
+ $label = "search_types:$type";
+
+ $data = htmlspecialchars(http_build_query(array(
+ 'q' => $query,
+ 'search_type' => $type,
+ )));
+
+ $url = elgg_get_site_url()."search?$data";
+
+ $menu_item = new ElggMenuItem($label, elgg_echo($label), $url);
+ elgg_register_menu_item('page', $menu_item);
+}
+
+// start the actual search
+$results_html = '';
+
+if ($search_type == 'all' || $search_type == 'entities') {
+ // to pass the correct current search type to the views
+ $current_params = $params;
+ $current_params['search_type'] = 'entities';
+
+ // foreach through types.
+ // if a plugin returns FALSE for subtype ignore it.
+ // if a plugin returns NULL or '' for subtype, pass to generic type search function.
+ // if still NULL or '' or empty(array()) no results found. (== don't show??)
+ foreach ($types as $type => $subtypes) {
+ if ($search_type != 'all' && $entity_type != $type) {
+ continue;
+ }
+
+ if (is_array($subtypes) && count($subtypes)) {
+ foreach ($subtypes as $subtype) {
+ // no need to search if we're not interested in these results
+ // @todo when using index table, allow search to get full count.
+ if ($search_type != 'all' && $entity_subtype != $subtype) {
+ continue;
+ }
+ $current_params['subtype'] = $subtype;
+ $current_params['type'] = $type;
+
+ $results = elgg_trigger_plugin_hook('search', "$type:$subtype", $current_params, NULL);
+ if ($results === FALSE) {
+ // someone is saying not to display these types in searches.
+ continue;
+ } elseif (is_array($results) && !count($results)) {
+ // no results, but results searched in hook.
+ } elseif (!$results) {
+ // no results and not hooked. use default type search.
+ // don't change the params here, since it's really a different subtype.
+ // Will be passed to elgg_get_entities().
+ $results = elgg_trigger_plugin_hook('search', $type, $current_params, array());
+ }
+
+ if (is_array($results['entities']) && $results['count']) {
+ if ($view = search_get_search_view($current_params, 'list')) {
+ $results_html .= elgg_view($view, array(
+ 'results' => $results,
+ 'params' => $current_params,
+ ));
+ }
+ }
+ }
+ }
+
+ // pull in default type entities with no subtypes
+ $current_params['type'] = $type;
+ $current_params['subtype'] = ELGG_ENTITIES_NO_VALUE;
+
+ $results = elgg_trigger_plugin_hook('search', $type, $current_params, array());
+ if ($results === FALSE) {
+ // someone is saying not to display these types in searches.
+ continue;
+ }
+
+ if (is_array($results['entities']) && $results['count']) {
+ if ($view = search_get_search_view($current_params, 'list')) {
+ $results_html .= elgg_view($view, array(
+ 'results' => $results,
+ 'params' => $current_params,
+ ));
+ }
+ }
+ }
+}
+
+// call custom searches
+if ($search_type != 'entities' || $search_type == 'all') {
+ if (is_array($custom_types)) {
+ foreach ($custom_types as $type) {
+ if ($search_type != 'all' && $search_type != $type) {
+ continue;
+ }
+
+ $current_params = $params;
+ $current_params['search_type'] = $type;
+
+ $results = elgg_trigger_plugin_hook('search', $type, $current_params, array());
+
+ if ($results === FALSE) {
+ // someone is saying not to display these types in searches.
+ continue;
+ }
+
+ if (is_array($results['entities']) && $results['count']) {
+ if ($view = search_get_search_view($current_params, 'list')) {
+ $results_html .= elgg_view($view, array(
+ 'results' => $results,
+ 'params' => $current_params,
+ ));
+ }
+ }
+ }
+ }
+}
+
+// highlight search terms
+if ($search_type == 'tags') {
+ $searched_words = array($display_query);
+} else {
+ $searched_words = search_remove_ignored_words($display_query, 'array');
+}
+$highlighted_query = search_highlight_words($searched_words, $display_query);
+
+$body = elgg_view_title(elgg_echo('search:results', array("\"$highlighted_query\"")));
+
+if (!$results_html) {
+ $body .= elgg_view('search/no_results');
+} else {
+ $body .= $results_html;
+}
+
+// this is passed the original params because we don't care what actually
+// matched (which is out of date now anyway).
+// we want to know what search type it is.
+$layout_view = search_get_search_view($params, 'layout');
+$layout = elgg_view($layout_view, array('params' => $params, 'body' => $body));
+
+$title = elgg_echo('search:results', array("\"$display_query\""));
+
+echo elgg_view_page($title, $layout);
diff --git a/mod/search/search_hooks.php b/mod/search/search_hooks.php
index f7a49400f..923cf0aa8 100644
--- a/mod/search/search_hooks.php
+++ b/mod/search/search_hooks.php
@@ -3,51 +3,46 @@
* Elgg core search.
*
* @package Elgg
- * @subpackage Core
- * @author Curverider Ltd <info@elgg.com>, The MITRE Corporation <http://www.mitre.org>
- * @link http://elgg.org/
+ * @subpackage Search
*/
/**
- * Return default results for searches on objects.
+ * Get objects that match the search parameters.
*
- * @param unknown_type $hook
- * @param unknown_type $type
- * @param unknown_type $value
- * @param unknown_type $params
- * @return unknown_type
+ * @param string $hook Hook name
+ * @param string $type Hook type
+ * @param array $value Empty array
+ * @param array $params Search parameters
+ * @return array
*/
function search_objects_hook($hook, $type, $value, $params) {
- global $CONFIG;
- $join = "JOIN {$CONFIG->dbprefix}objects_entity oe ON e.guid = oe.guid";
+ $db_prefix = elgg_get_config('dbprefix');
+
+ $join = "JOIN {$db_prefix}objects_entity oe ON e.guid = oe.guid";
$params['joins'] = array($join);
$fields = array('title', 'description');
$where = search_get_where_sql('oe', $fields, $params);
$params['wheres'] = array($where);
-
- //@todo allow sorting by recent time
- $params['order_by'] = NULL;
-
- $entities = elgg_get_entities($params);
$params['count'] = TRUE;
$count = elgg_get_entities($params);
-
+
// no need to continue if nothing here.
if (!$count) {
return array('entities' => array(), 'count' => $count);
}
+
+ $params['count'] = FALSE;
+ $params['order_by'] = search_get_order_by_sql('e', 'oe', $params['sort'], $params['order']);
+ $entities = elgg_get_entities($params);
// add the volatile data for why these entities have been returned.
foreach ($entities as $entity) {
- //$title = search_get_highlighted_relevant_substrings($entity->title, $params['query']);
- //$title = search_get_relevant_substring($entity->title, $params['query'], '<strong class="searchMatch">', '</strong>');
$title = search_get_highlighted_relevant_substrings($entity->title, $params['query']);
$entity->setVolatileData('search_matched_title', $title);
- //$desc = search_get_relevant_substring($entity->description, $params['query'], '<strong class="searchMatch">', '</strong>');
$desc = search_get_highlighted_relevant_substrings($entity->description, $params['query']);
$entity->setVolatileData('search_matched_description', $desc);
}
@@ -59,45 +54,49 @@ function search_objects_hook($hook, $type, $value, $params) {
}
/**
- * Return default results for searches on groups.
+ * Get groups that match the search parameters.
*
- * @param unknown_type $hook
- * @param unknown_type $type
- * @param unknown_type $value
- * @param unknown_type $params
- * @return unknown_type
+ * @param string $hook Hook name
+ * @param string $type Hook type
+ * @param array $value Empty array
+ * @param array $params Search parameters
+ * @return array
*/
function search_groups_hook($hook, $type, $value, $params) {
- global $CONFIG;
+ $db_prefix = elgg_get_config('dbprefix');
$query = sanitise_string($params['query']);
- $join = "JOIN {$CONFIG->dbprefix}groups_entity ge ON e.guid = ge.guid";
+ $join = "JOIN {$db_prefix}groups_entity ge ON e.guid = ge.guid";
$params['joins'] = array($join);
+ $fields = array('name', 'description');
+
+ $where = search_get_where_sql('ge', $fields, $params);
- $where = "(ge.guid = e.guid
- AND (ge.name LIKE '%$query%'
- OR ge.description LIKE '%$query%'
- )
- )";
$params['wheres'] = array($where);
- $entities = elgg_get_entities($params);
+ // override subtype -- All groups should be returned regardless of subtype.
+ $params['subtype'] = ELGG_ENTITIES_ANY_VALUE;
+
$params['count'] = TRUE;
$count = elgg_get_entities($params);
-
+
// no need to continue if nothing here.
if (!$count) {
return array('entities' => array(), 'count' => $count);
}
+
+ $params['count'] = FALSE;
+ $params['order_by'] = search_get_order_by_sql('e', 'ge', $params['sort'], $params['order']);
+ $entities = elgg_get_entities($params);
// add the volatile data for why these entities have been returned.
foreach ($entities as $entity) {
- $description = search_get_highlighted_relevant_substrings($entity->description, $query);
- $entity->setVolatileData('search_matched_title', $description);
-
$name = search_get_highlighted_relevant_substrings($entity->name, $query);
- $entity->setVolatileData('search_matched_description', $name);
+ $entity->setVolatileData('search_matched_title', $name);
+
+ $description = search_get_highlighted_relevant_substrings($entity->description, $query);
+ $entity->setVolatileData('search_matched_description', $description);
}
return array(
@@ -107,45 +106,93 @@ function search_groups_hook($hook, $type, $value, $params) {
}
/**
- * Return default results for searches on users.
+ * Get users that match the search parameters.
*
- * @param unknown_type $hook
- * @param unknown_type $type
- * @param unknown_type $value
- * @param unknown_type $params
- * @return unknown_type
+ * Searches on username, display name, and profile fields
+ *
+ * @param string $hook Hook name
+ * @param string $type Hook type
+ * @param array $value Empty array
+ * @param array $params Search parameters
+ * @return array
*/
function search_users_hook($hook, $type, $value, $params) {
- global $CONFIG;
+ $db_prefix = elgg_get_config('dbprefix');
$query = sanitise_string($params['query']);
- $join = "JOIN {$CONFIG->dbprefix}users_entity ue ON e.guid = ue.guid";
- $params['joins'] = array($join);
-
- $where = "(ue.guid = e.guid
- AND (ue.username LIKE '%$query%'
- OR ue.name LIKE '%$query%'
- )
- )";
- $params['wheres'] = array($where);
-
- $entities = elgg_get_entities($params);
- $params['count'] = TRUE;
+ $params['joins'] = array(
+ "JOIN {$db_prefix}users_entity ue ON e.guid = ue.guid",
+ "JOIN {$db_prefix}metadata md on e.guid = md.entity_guid",
+ "JOIN {$db_prefix}metastrings msv ON n_table.value_id = msv.id"
+ );
+
+ // username and display name
+ $fields = array('username', 'name');
+ $where = search_get_where_sql('ue', $fields, $params, FALSE);
+
+ // profile fields
+ $profile_fields = array_keys(elgg_get_config('profile_fields'));
+
+ // get the where clauses for the md names
+ // can't use egef_metadata() because the n_table join comes too late.
+ $clauses = elgg_entities_get_metastrings_options('metadata', array(
+ 'metadata_names' => $profile_fields,
+ ));
+
+ $params['joins'] = array_merge($clauses['joins'], $params['joins']);
+ // no fulltext index, can't disable fulltext search in this function.
+ // $md_where .= " AND " . search_get_where_sql('msv', array('string'), $params, FALSE);
+ $md_where = "(({$clauses['wheres'][0]}) AND msv.string LIKE '%$query%')";
+
+ $params['wheres'] = array("(($where) OR ($md_where))");
+
+ // override subtype -- All users should be returned regardless of subtype.
+ $params['subtype'] = ELGG_ENTITIES_ANY_VALUE;
+ $params['count'] = true;
$count = elgg_get_entities($params);
// no need to continue if nothing here.
if (!$count) {
return array('entities' => array(), 'count' => $count);
}
+
+ $params['count'] = FALSE;
+ $params['order_by'] = search_get_order_by_sql('e', 'ue', $params['sort'], $params['order']);
+ $entities = elgg_get_entities($params);
// add the volatile data for why these entities have been returned.
foreach ($entities as $entity) {
- $username = search_get_highlighted_relevant_substrings($entity->username, $query);
- $entity->setVolatileData('search_matched_title', $username);
+
+ $title = search_get_highlighted_relevant_substrings($entity->name, $query);
- $name = search_get_highlighted_relevant_substrings($entity->name, $query);
- $entity->setVolatileData('search_matched_description', $name);
+ // include the username if it matches but the display name doesn't.
+ if (false !== strpos($entity->username, $query)) {
+ $username = search_get_highlighted_relevant_substrings($entity->username, $query);
+ $title .= " ($username)";
+ }
+
+ $entity->setVolatileData('search_matched_title', $title);
+
+ $matched = '';
+ foreach ($profile_fields as $md_name) {
+ $metadata = $entity->$md_name;
+ if (is_array($metadata)) {
+ foreach ($metadata as $text) {
+ if (stristr($text, $query)) {
+ $matched .= elgg_echo("profile:{$md_name}") . ': '
+ . search_get_highlighted_relevant_substrings($text, $query);
+ }
+ }
+ } else {
+ if (stristr($metadata, $query)) {
+ $matched .= elgg_echo("profile:{$md_name}") . ': '
+ . search_get_highlighted_relevant_substrings($metadata, $query);
+ }
+ }
+ }
+
+ $entity->setVolatileData('search_matched_description', $matched);
}
return array(
@@ -155,35 +202,130 @@ function search_users_hook($hook, $type, $value, $params) {
}
/**
- * Return default results for searches on tags.
+ * Get entities with tags that match the search parameters.
*
- * @param unknown_type $hook
- * @param unknown_type $type
- * @param unknown_type $value
- * @param unknown_type $params
- * @return unknown_type
+ * @param string $hook Hook name
+ * @param string $type Hook type
+ * @param array $value Empty array
+ * @param array $params Search parameters
+ * @return array
*/
function search_tags_hook($hook, $type, $value, $params) {
- global $CONFIG;
+ $db_prefix = elgg_get_config('dbprefix');
+
+ $valid_tag_names = elgg_get_registered_tag_metadata_names();
// @todo will need to split this up to support searching multiple tags at once.
$query = sanitise_string($params['query']);
- $params['metadata_name_value_pair'] = array ('name' => 'tags', 'value' => $query, 'case_sensitive' => FALSE);
- $entities = elgg_get_entities_from_metadata($params);
+ // if passed a tag metadata name, only search on that tag name.
+ // tag_name isn't included in the params because it's specific to
+ // tag searches.
+ if ($tag_names = get_input('tag_names')) {
+ if (is_array($tag_names)) {
+ $search_tag_names = $tag_names;
+ } else {
+ $search_tag_names = array($tag_names);
+ }
+
+ // check these are valid to avoid arbitrary metadata searches.
+ foreach ($search_tag_names as $i => $tag_name) {
+ if (!in_array($tag_name, $valid_tag_names)) {
+ unset($search_tag_names[$i]);
+ }
+ }
+ } else {
+ $search_tag_names = $valid_tag_names;
+ }
+
+ if (!$search_tag_names) {
+ return array('entities' => array(), 'count' => $count);
+ }
+
+ // don't use elgg_get_entities_from_metadata() here because of
+ // performance issues. since we don't care what matches at this point
+ // use an IN clause to grab everything that matches at once and sort
+ // out the matches later.
+ $params['joins'][] = "JOIN {$db_prefix}metadata md on e.guid = md.entity_guid";
+ $params['joins'][] = "JOIN {$db_prefix}metastrings msn on md.name_id = msn.id";
+ $params['joins'][] = "JOIN {$db_prefix}metastrings msv on md.value_id = msv.id";
+
+ $access = get_access_sql_suffix('md');
+ $sanitised_tags = array();
+
+ foreach ($search_tag_names as $tag) {
+ $sanitised_tags[] = '"' . sanitise_string($tag) . '"';
+ }
+
+ $tags_in = implode(',', $sanitised_tags);
+
+ $params['wheres'][] = "(msn.string IN ($tags_in) AND msv.string = '$query' AND $access)";
+
$params['count'] = TRUE;
- $count = elgg_get_entities_from_metadata($params);
+ $count = elgg_get_entities($params);
// no need to continue if nothing here.
if (!$count) {
return array('entities' => array(), 'count' => $count);
}
+
+ $params['count'] = FALSE;
+ $params['order_by'] = search_get_order_by_sql('e', null, $params['sort'], $params['order']);
+ $entities = elgg_get_entities($params);
// add the volatile data for why these entities have been returned.
foreach ($entities as $entity) {
- $tags = implode(',', $entity->tags);
- $tags_str = search_get_highlighted_relevant_substrings($tags, $params['query']);
- $entity->setVolatileData('search_matched_tags', $tags_str);
+ $matched_tags_strs = array();
+
+ // get tags for each tag name requested to find which ones matched.
+ foreach ($search_tag_names as $tag_name) {
+ $tags = $entity->getTags($tag_name);
+
+ // @todo make one long tag string and run this through the highlight
+ // function. This might be confusing as it could chop off
+ // the tag labels.
+ if (in_array(strtolower($query), array_map('strtolower', $tags))) {
+ if (is_array($tags)) {
+ $tag_name_str = elgg_echo("tag_names:$tag_name");
+ $matched_tags_strs[] = "$tag_name_str: " . implode(', ', $tags);
+ }
+ }
+ }
+
+ // different entities have different titles
+ switch($entity->type) {
+ case 'site':
+ case 'user':
+ case 'group':
+ $title_tmp = $entity->name;
+ break;
+
+ case 'object':
+ $title_tmp = $entity->title;
+ break;
+ }
+
+ // Nick told me my idea was dirty, so I'm hard coding the numbers.
+ $title_tmp = strip_tags($title_tmp);
+ if (elgg_strlen($title_tmp) > 297) {
+ $title_str = elgg_substr($title_tmp, 0, 297) . '...';
+ } else {
+ $title_str = $title_tmp;
+ }
+
+ $desc_tmp = strip_tags($entity->description);
+ if (elgg_strlen($desc_tmp) > 297) {
+ $desc_str = elgg_substr($desc_tmp, 0, 297) . '...';
+ } else {
+ $desc_str = $desc_tmp;
+ }
+
+ $tags_str = implode('. ', $matched_tags_strs);
+ $tags_str = search_get_highlighted_relevant_substrings($tags_str, $params['query'], 30, 300, true);
+
+ $entity->setVolatileData('search_matched_title', $title_str);
+ $entity->setVolatileData('search_matched_description', $desc_str);
+ $entity->setVolatileData('search_matched_extra', $tags_str);
}
return array(
@@ -195,11 +337,11 @@ function search_tags_hook($hook, $type, $value, $params) {
/**
* Register tags as a custom search type.
*
- * @param unknown_type $hook
- * @param unknown_type $type
- * @param unknown_type $value
- * @param unknown_type $params
- * @return unknown_type
+ * @param string $hook Hook name
+ * @param string $type Hook type
+ * @param array $value Array of custom search types
+ * @param array $params Search parameters
+ * @return array
*/
function search_custom_types_tags_hook($hook, $type, $value, $params) {
$value[] = 'tags';
@@ -208,76 +350,141 @@ function search_custom_types_tags_hook($hook, $type, $value, $params) {
/**
- * Return default results for searches on comments.
+ * Get comments that match the search parameters.
*
- * @param unknown_type $hook
- * @param unknown_type $type
- * @param unknown_type $value
- * @param unknown_type $params
- * @return unknown_type
+ * @param string $hook Hook name
+ * @param string $type Hook type
+ * @param array $value Empty array
+ * @param array $params Search parameters
+ * @return array
*/
function search_comments_hook($hook, $type, $value, $params) {
- global $CONFIG;
+ $db_prefix = elgg_get_config('dbprefix');
$query = sanitise_string($params['query']);
+ $limit = sanitise_int($params['limit']);
+ $offset = sanitise_int($params['offset']);
$params['annotation_names'] = array('generic_comment', 'group_topic_post');
$params['joins'] = array(
- "JOIN {$CONFIG->dbprefix}annotations a on e.guid = a.entity_guid",
- "JOIN {$CONFIG->dbprefix}metastrings msn on a.name_id = msn.id",
- "JOIN {$CONFIG->dbprefix}metastrings msv on a.value_id = msv.id"
+ "JOIN {$db_prefix}annotations a on e.guid = a.entity_guid",
+ "JOIN {$db_prefix}metastrings msn on a.name_id = msn.id",
+ "JOIN {$db_prefix}metastrings msv on a.value_id = msv.id"
);
$fields = array('string');
- $search_where = search_get_where_sql('msv', $fields, $params);
+
+ // force IN BOOLEAN MODE since fulltext isn't
+ // available on metastrings (and boolean mode doesn't need it)
+ $search_where = search_get_where_sql('msv', $fields, $params, FALSE);
+
+ $container_and = '';
+ if ($params['container_guid'] && $params['container_guid'] !== ELGG_ENTITIES_ANY_VALUE) {
+ $container_and = 'AND e.container_guid = ' . sanitise_int($params['container_guid']);
+ }
$e_access = get_access_sql_suffix('e');
$a_access = get_access_sql_suffix('a');
// @todo this can probably be done through the api..
- $q = "SELECT DISTINCT a.*, msv.string as comment FROM {$CONFIG->dbprefix}annotations a
- JOIN {$CONFIG->dbprefix}metastrings msn ON a.name_id = msn.id
- JOIN {$CONFIG->dbprefix}metastrings msv ON a.value_id = msv.id
- JOIN {$CONFIG->dbprefix}entities e ON a.entity_guid = e.guid
+ $q = "SELECT count(DISTINCT a.id) as total FROM {$db_prefix}annotations a
+ JOIN {$db_prefix}metastrings msn ON a.name_id = msn.id
+ JOIN {$db_prefix}metastrings msv ON a.value_id = msv.id
+ JOIN {$db_prefix}entities e ON a.entity_guid = e.guid
WHERE msn.string IN ('generic_comment', 'group_topic_post')
AND ($search_where)
AND $e_access
AND $a_access
+ $container_and
+ ";
- LIMIT {$params['offset']}, {$params['limit']}
+ if (!$result = get_data($q)) {
+ return FALSE;
+ }
+
+ $count = $result[0]->total;
+
+ // don't continue if nothing there...
+ if (!$count) {
+ return array('entities' => array(), 'count' => 0);
+ }
+
+ // no full text index on metastrings table
+ if ($params['sort'] == 'relevance') {
+ $params['sort'] = 'created';
+ }
+
+ $order_by = search_get_order_by_sql('a', null, $params['sort'], $params['order']);
+ if ($order_by) {
+ $order_by = "ORDER BY $order_by";
+ }
+
+ $q = "SELECT DISTINCT a.*, msv.string as comment FROM {$db_prefix}annotations a
+ JOIN {$db_prefix}metastrings msn ON a.name_id = msn.id
+ JOIN {$db_prefix}metastrings msv ON a.value_id = msv.id
+ JOIN {$db_prefix}entities e ON a.entity_guid = e.guid
+ WHERE msn.string IN ('generic_comment', 'group_topic_post')
+ AND ($search_where)
+ AND $e_access
+ AND $a_access
+ $container_and
+
+ $order_by
+ LIMIT $offset, $limit
";
+
$comments = get_data($q);
+ // @todo if plugins are disabled causing subtypes
+ // to be invalid and there are comments on entities of those subtypes,
+ // the counts will be wrong here and results might not show up correctly,
+ // especially on the search landing page, which only pulls out two results.
+
+ // probably better to check against valid subtypes than to do what I'm doing.
+
// need to return actual entities
// add the volatile data for why these entities have been returned.
$entities = array();
foreach ($comments as $comment) {
- $tags = implode(',', $entity->tags);
- if (!$entity = get_entity($comment->entity_guid)) {
- continue;
+ $entity = get_entity($comment->entity_guid);
+
+ // hic sunt dracones
+ if (!$entity) {
+ //continue;
+ $entity = new ElggObject();
+ $entity->setVolatileData('search_unavailable_entity', TRUE);
}
+
$comment_str = search_get_highlighted_relevant_substrings($comment->comment, $query);
- $entity->setVolatileData('search_matched_comment', $comment_str);
- $entity->setVolatileData('search_matched_comment_owner_guid', $comment->owner_guid);
- $entity->setVolatileData('search_matched_comment_time_created', $comment->time_created);
+ $comments_data = $entity->getVolatileData('search_comments_data');
+ if (!$comments_data) {
+ $comments_data = array();
+ }
+ $comments_data[] = array(
+ 'annotation_id' => $comment->id,
+ 'text' => $comment_str,
+ 'owner_guid' => $comment->owner_guid,
+ 'time_created' => $comment->time_created,
+ );
+ $entity->setVolatileData('search_comments_data', $comments_data);
$entities[] = $entity;
}
return array(
'entities' => $entities,
- 'count' => count($entities),
+ 'count' => $count,
);
}
/**
* Register comments as a custom search type.
*
- * @param unknown_type $hook
- * @param unknown_type $type
- * @param unknown_type $value
- * @param unknown_type $params
- * @return unknown_type
+ * @param string $hook Hook name
+ * @param string $type Hook type
+ * @param array $value Array of custom search types
+ * @param array $params Search parameters
+ * @return array
*/
function search_custom_types_comments_hook($hook, $type, $value, $params) {
$value[] = 'comments';
return $value;
-} \ No newline at end of file
+}
diff --git a/mod/search/start.php b/mod/search/start.php
index 18b743cde..8a112a3a3 100644
--- a/mod/search/start.php
+++ b/mod/search/start.php
@@ -1,46 +1,42 @@
<?php
/**
- * Elgg core search.
- *
- * @package Elgg
- * @subpackage Core
- * @author Curverider Ltd <info@elgg.com>, The MITRE Corporation <http://www.mitre.org>
- * @link http://elgg.org/
- */
+ * Elgg search plugin
+ *
+ */
+
+elgg_register_event_handler('init','system','search_init');
/**
- * Initialise search helper functions.
- *
+ * Initialize search plugin
*/
function search_init() {
global $CONFIG;
require_once 'search_hooks.php';
// page handler for search actions and results
- register_page_handler('search','search_page_handler');
+ elgg_register_page_handler('search', 'search_page_handler');
// register some default search hooks
- register_plugin_hook('search', 'object', 'search_objects_hook');
- register_plugin_hook('search', 'user', 'search_users_hook');
-
- // @todo pull this out into groups
- register_plugin_hook('search', 'group', 'search_groups_hook');
+ elgg_register_plugin_hook_handler('search', 'object', 'search_objects_hook');
+ elgg_register_plugin_hook_handler('search', 'user', 'search_users_hook');
+ elgg_register_plugin_hook_handler('search', 'group', 'search_groups_hook');
// tags and comments are a bit different.
// register a search types and a hooks for them.
- register_plugin_hook('search_types', 'get_types', 'search_custom_types_tags_hook');
- register_plugin_hook('search', 'tags', 'search_tags_hook');
+ elgg_register_plugin_hook_handler('search_types', 'get_types', 'search_custom_types_tags_hook');
+ elgg_register_plugin_hook_handler('search', 'tags', 'search_tags_hook');
- register_plugin_hook('search_types', 'get_types', 'search_custom_types_comments_hook');
- register_plugin_hook('search', 'comments', 'search_comments_hook');
+ elgg_register_plugin_hook_handler('search_types', 'get_types', 'search_custom_types_comments_hook');
+ elgg_register_plugin_hook_handler('search', 'comments', 'search_comments_hook');
// get server min and max allowed chars for ft searching
$CONFIG->search_info = array();
// can't use get_data() here because some servers don't have these globals set,
// which throws a db exception.
- $r = mysql_query('SELECT @@ft_min_word_len as min, @@ft_max_word_len as max');
- if ($word_lens = mysql_fetch_assoc($r)) {
+ $dblink = get_db_link('read');
+ $r = mysql_query('SELECT @@ft_min_word_len as min, @@ft_max_word_len as max', $dblink);
+ if ($r && ($word_lens = mysql_fetch_assoc($r))) {
$CONFIG->search_info['min_chars'] = $word_lens['min'];
$CONFIG->search_info['max_chars'] = $word_lens['max'];
} else {
@@ -50,16 +46,19 @@ function search_init() {
}
// add in CSS for search elements
- elgg_extend_view('css', 'search/css');
+ elgg_extend_view('css/elgg', 'search/css');
+
+ // extend view for elgg topbar search box
+ elgg_extend_view('page/elements/header', 'search/header');
}
/**
* Page handler for search
*
- * @param array $page Page elements from pain page handler
+ * @param array $page Page elements from core page handler
+ * @return bool
*/
function search_page_handler($page) {
- global $CONFIG;
// if there is no q set, we're being called from a legacy installation
// it expects a search by tags.
@@ -70,420 +69,315 @@ function search_page_handler($page) {
//set_input('search_type', 'tags');
}
- include_once('index.php');
+ $base_dir = elgg_get_plugins_path() . 'search/pages/search';
+
+ include_once("$base_dir/index.php");
+ return true;
}
/**
- * Return a string with highlighted matched elements.
- * Checks for "s
- * Provides context for matched elements.
- * Will not return more than $max_length of full context.
- * Only highlights words
+ * Return a string with highlighted matched queries and relevant context
+ * Determines context based upon occurance and distance of words with each other.
*
- * @param unknown_type $haystack
- * @param unknown_type $need
- * @param unknown_type $context
- * @param unknown_type $max_length
- * @return unknown_type
+ * @param string $haystack
+ * @param string $query
+ * @param int $min_match_context = 30
+ * @param int $max_length = 300
+ * @param bool $tag_match Search is for tags. Don't ignore words.
+ * @return string
*/
-function search_get_highlighted_relevant_substrings($haystack, $needle, $min_match_context = 15, $max_length = 500) {
- global $CONFIG;
+function search_get_highlighted_relevant_substrings($haystack, $query, $min_match_context = 30, $max_length = 300, $tag_match = false) {
+
$haystack = strip_tags($haystack);
- $haystack_lc = strtolower($haystack);
-//
-// $haystack = "Like merge sort, quicksort can also be easily parallelized due to its "
-// . "divide-and-conquer nature. Individual in-place partition operations are difficult "
-// . "to parallelize, but once divided, different sections of the list can be sorted in parallel. "
-// . "If we have p processors, we can divide a list of n ele";
-//
-// $needle = 'difficult to sort in parallel';
-
- // for now don't worry about "s or boolean operators
- $needle = str_replace(array('"', '-', '+', '~'), '', stripslashes(strip_tags($needle)));
- $words = explode(' ', $needle);
+ $haystack_length = elgg_strlen($haystack);
+ $haystack_lc = elgg_strtolower($haystack);
- $min_chars = $CONFIG->search_info['min_chars'];
- // if > ft_min_word == not running in literal mode.
- if ($needle >= $min_chars) {
- // clean out any words that are ignored by mysql
- foreach ($words as $i => $word) {
- if (strlen($word) < $min_chars) {
- unset ($words[$i]);
- }
- }
+ if (!$tag_match) {
+ $words = search_remove_ignored_words($query, 'array');
+ } else {
+ $words = array();
}
- /*
-
- $body_len = 250
-
- $context = 5-30, 20-45, 75-100, 150
-
- can pull out context either on:
- one of each matching term
- X # of highest matching terms
+ // if haystack < $max_length return the entire haystack w/formatting immediately
+ if ($haystack_length <= $max_length) {
+ $return = search_highlight_words($words, $haystack);
+ return $return;
+ }
- */
- $substr_counts = array();
- $str_pos = array();
- // matrices for being and end context lengths.
- // defaults to min context. will add additional context later if needed
+ // get the starting positions and lengths for all matching words
$starts = array();
- $stops = array();
-
- // map the words to the starts and stops
- $words_arg = array();
- $context_count = 0;
-
-
- // get the full count of matches.
+ $lengths = array();
foreach ($words as $word) {
- $word = strtolower($word);
- $count = substr_count($haystack, $word);
- $word_len = strlen($word);
+ $word = elgg_strtolower($word);
+ $count = elgg_substr_count($haystack_lc, $word);
+ $word_len = elgg_strlen($word);
+ $haystack_len = elgg_strlen($haystack_lc);
// find the start positions for the words
if ($count > 1) {
- $str_pos[$word] = array();
$offset = 0;
- while (FALSE !== $pos = strpos($haystack, $word, $offset)) {
- $str_pos[$word][] = $pos;
- $starts[] = ($pos - $min_match_context > 0) ? $pos - $min_match_context : 0;
- $stops[] = $pos + $word_len + $min_match_context;
- $words_arg[] = $word;
- $context_count += $min_match_context + $word_len;
+ while (FALSE !== $pos = elgg_strpos($haystack_lc, $word, $offset)) {
+ $start = ($pos - $min_match_context > 0) ? $pos - $min_match_context : 0;
+ $starts[] = $start;
+ $stop = $pos + $word_len + $min_match_context;
+ $lengths[] = $stop - $start;
$offset += $pos + $word_len;
+
+ if ($offset >= $haystack_len) {
+ break;
+ }
}
} else {
- $pos = strpos($haystack, $word);
- $str_pos[$word] = array($pos);
- $starts[] = ($pos - $min_match_context > 0) ? $pos - $min_match_context : 0;
- $stops[] = $pos + $word_len + $min_match_context;
- $context_count += $min_match_context + $word_len;
- $words_arg[] = $word;
- }
- $substr_counts[$word] = $count;
- }
-
- // sort by order of occurence
- //krsort($substr_counts);
- $full_count = array_sum($substr_counts);
-
- // figure out what the context needs to be.
- // take one of each matched phrase
- // if there are any
-
-//
-// var_dump($str_pos);
-// var_dump($substr_counts);
-// var_dump($context_count);
-
-
- // sort to put them in order of occurence
- asort($starts, SORT_NUMERIC);
- asort($stops, SORT_NUMERIC);
-
- // offset them correctly
- $starts[] = 0;
- $new_stops = array(0);
- foreach ($stops as $i => $pos) {
- $new_stops[$i+1] = $pos;
- }
- $stops = $new_stops;
-
- $substrings = array();
- $len = count($starts);
-
- $starts = array_merge($starts);
- $stops = array_merge($stops);
-
- $offsets = array();
- $limits = array();
- $c = 0;
- foreach ($starts as $i => $start) {
- $stop = $stops[$i];
- $offsets[$c] = $start;
- $limits[$c] = $stop;
-
- // never need the last one as it's just a displacing entry
- if ($c+1 == count($starts)) {
- break;
- }
-
- if ($start - $stop < 0) {
- //var_dump("Looking at c=$c & $start - $stop and going to unset {$limits[$c]}");
- unset($offsets[$c]);
- unset($limits[$c]);
+ $pos = elgg_strpos($haystack_lc, $word);
+ $start = ($pos - $min_match_context > 0) ? $pos - $min_match_context : 0;
+ $starts[] = $start;
+ $stop = $pos + $word_len + $min_match_context;
+ $lengths[] = $stop - $start;
}
- $c++;
}
- // reset indexes and remove placeholder elements.
- $limits = array_merge($limits);
- array_shift($limits);
- $offsets = array_merge($offsets);
- array_pop($offsets);
-
- // figure out if we need to adjust the offsets from the base
- // this could result in overlapping summaries.
- // might be nicer to just remove it.
+ $offsets = search_consolidate_substrings($starts, $lengths);
- $total_len = 0;
- foreach ($offsets as $i => $offset) {
- $total_len += $limits[$i] - $offset;
- }
+ // figure out if we can adjust the offsets and lengths
+ // in order to return more context
+ $total_length = array_sum($offsets);
$add_length = 0;
- if ($total_length < $max_length) {
- $add_length = floor((($max_length - $total_len) / count($offsets)) / 2);
- }
-
- $lengths = array();
- foreach ($offsets as $i => $offset) {
- $limit = $limits[$i];
- if ($offset == 0 && $add_length) {
- $limit += $add_length;
- } else {
- $offset = $offset - $add_length;
- }
- $string = substr($haystack, $offset, $limit - $offset);
-
- if ($offset != 0) {
- $string = "...$string";
- }
-
- if ($limit + $offset >= strlen($haystack)) {
- $string .= '...';
+ if ($total_length < $max_length && $offsets) {
+ $add_length = floor((($max_length - $total_length) / count($offsets)) / 2);
+
+ $starts = array();
+ $lengths = array();
+ foreach ($offsets as $offset => $length) {
+ $start = ($offset - $add_length > 0) ? $offset - $add_length : 0;
+ $length = $length + $add_length;
+ $starts[] = $start;
+ $lengths[] = $length;
}
- $substrings[] = $string;
- $lengths[] = strlen($string);
+ $offsets = search_consolidate_substrings($starts, $lengths);
}
- // sort by length of context.
- asort($lengths);
+ // sort by order of string size descending (which is roughly
+ // the proximity of matched terms) so we can keep the
+ // substrings with terms closest together and discard
+ // the others as needed to fit within $max_length.
+ arsort($offsets);
- $matched = '';
- foreach ($lengths as $i => $len) {
- $string = $substrings[$i];
+ $return_strs = array();
+ $total_length = 0;
+ foreach ($offsets as $start => $length) {
+ $string = trim(elgg_substr($haystack, $start, $length));
- if (strlen($matched) + strlen($string) < $max_length) {
- $matched .= $string;
+ // continue past if adding this substring exceeds max length
+ if ($total_length + $length > $max_length) {
+ continue;
}
- }
- $i = 1;
- foreach ($words as $word) {
- $search = "/($word)/i";
- $replace = "<strong class=\"searchMatch searchMatchColor$i\">$1</strong>";
- $matched = preg_replace($search, $replace, $matched);
- $i++;
+ $total_length += $length;
+ $return_strs[$start] = $string;
}
- return $matched;
-
+ // put the strings in order of occurence
+ ksort($return_strs);
- // crap below..
-
-
-
- for ($i=0; $i<$len; $i++) {
- $start = $starts[$i];
- $stop = $stops[$i];
- var_dump("Looking at $i = $start - $stop");
-
- while ($start - $stop <= 0) {
- $stop = $stops[$i++];
- var_dump("New start is $stop");
- }
-
- var_dump("$start-$stop");
- }
-
- // find the intersecting contexts
- foreach ($starts as $i => $start_pos) {
- $words .= "{$words_arg[$i]}\t\t\t";
- echo "$start_pos\t\t\t";
+ // add ...s where needed
+ $return = implode('...', $return_strs);
+ if (!array_key_exists(0, $return_strs)) {
+ $return = "...$return";
}
- echo "\n";
-
- foreach ($stops as $i => $stop_pos) {
- echo "$stop_pos\t\t\t";
+ // add to end of string if last substring doesn't hit the end.
+ $starts = array_keys($return_strs);
+ $last_pos = $starts[count($starts)-1];
+ if ($last_pos + elgg_strlen($return_strs[$last_pos]) < $haystack_length) {
+ $return .= '...';
}
-echo "\n$words\n";
- // get full number of matches against all words to see how many we actually want to look at.
+ $return = search_highlight_words($words, $return);
+ return $return;
+}
+/**
+ * Takes an array of offsets and lengths and consolidates any
+ * overlapping entries, returning an array of new offsets and lengths
+ *
+ * Offsets and lengths are specified in separate arrays because of possible
+ * index collisions with the offsets.
+ *
+ * @param array $offsets
+ * @param array $lengths
+ * @return array
+ */
+function search_consolidate_substrings($offsets, $lengths) {
+ // sort offsets by occurence
+ asort($offsets, SORT_NUMERIC);
-// $desc = search_get_relevant_substring($entity->description, $params['query'], '<strong class="searchMatch">', '</strong>');
-
-
- $params['query'];
- // "this is"just a test "silly person"
-
- // check for "s
- $words_quotes = explode('"', $needle);
-
- $words_orig = explode(' ', $needle);
- $words = array();
-
- foreach ($words_orig as $i => $word) {
- // figure out if we have a special operand
- $operand = substr($word, 0, 1);
- switch($operand) {
- case '"':
- // find the matching " if any. else, remove the "
- if (substr_count($query, '"') < 2) {
- $words[] = substr($word, 1);
- } else {
- $word = substr($word, 1);
- $word_i = $i;
- while ('"' != strpos($words_orig[$word_i], '"')) {
- $word .= " {$words_orig[$word_i]}";
- unset($words_orig[$word_i]);
- }
- }
+ // reset the indexes maintaining association with the original offsets.
+ $offsets = array_merge($offsets);
- break;
+ $new_lengths = array();
+ foreach ($offsets as $i => $offset) {
+ $new_lengths[] = $lengths[$i];
+ }
- case '+':
- // remove +
- $words[] = substr($word, 1);
- break;
+ $lengths = $new_lengths;
- case '~':
- case '-':
- // remove this from highlighted list.
+ $return = array();
+ $count = count($offsets);
+ for ($i=0; $i<$count; $i++) {
+ $offset = $offsets[$i];
+ $length = $lengths[$i];
+ $end_pos = $offset + $length;
+ // find the next entry that doesn't overlap
+ while (array_key_exists($i+1, $offsets) && $end_pos > $offsets[$i+1]) {
+ $i++;
+ if (!array_key_exists($i, $offsets)) {
break;
+ }
+ $end_pos = $lengths[$i] + $offsets[$i];
}
- }
- // pick out " queries
- if (substr_count($query, '"') >= 2) {
+ $length = $end_pos - $offset;
+ // will never have a colliding offset, so can return as a single array
+ $return[$offset] = $length;
}
- // ignore queries starting with -
-
-
- // @todo figure out a way to "center" the matches within the max_length.
- // if only one match, its context is $context + $max_length / 2
- // if 2 matches, its context is $context + $max_length / 4
- // if 3 matches, its context is $context + $max_length / 6
- // $context per match = $min_match_context + ($max_length / $num_count_match)
-
- // if $max_length / ($matched_count * 2) < $context
- // only match against the first X matches where $context >= $context
+ return $return;
}
/**
- * Returns a matching string with $context amount of context, optionally
- * surrounded by $before and $after.
+ * Safely highlights the words in $words found in $string avoiding recursion
*
- * If no match is found, restricts string to $context*2 starting from strpos 0.
- *
- * @param str $haystack
- * @param str $needle
- * @param str $before
- * @param str $after
- * @param int $context
- * @return str
+ * @param array $words
+ * @param string $string
+ * @return string
*/
-function search_get_relevant_substring($haystack, $needle, $before = '', $after = '', $context = 75) {
- $haystack = strip_tags($haystack);
- $needle = strip_tags($needle);
-
- $pos = strpos(strtolower($haystack), strtolower($needle));
+function search_highlight_words($words, $string) {
+ $i = 1;
+ $replace_html = array(
+ 'strong' => rand(10000, 99999),
+ 'class' => rand(10000, 99999),
+ 'search-highlight' => rand(10000, 99999),
+ 'search-highlight-color' => rand(10000, 99999)
+ );
- if ($pos === FALSE) {
- $str = substr($haystack, 0, $context*2);
- if (strlen($haystack) > $context*2) {
- $str .= '...';
- }
+ foreach ($words as $word) {
+ // remove any boolean mode operators
+ $word = preg_replace("/([\-\+~])([\w]+)/i", '$2', $word);
+
+ // escape the delimiter and any other regexp special chars
+ $word = preg_quote($word, '/');
+
+ $search = "/($word)/i";
- return $str;
+ // @todo
+ // must replace with placeholders in case one of the search terms is
+ // in the html string.
+ // later, will replace the placeholders with the actual html.
+ // Yeah this is hacky. I'm tired.
+ $strong = $replace_html['strong'];
+ $class = $replace_html['class'];
+ $highlight = $replace_html['search-highlight'];
+ $color = $replace_html['search-highlight-color'];
+
+ $replace = "<$strong $class=\"$highlight $color{$i}\">$1</$strong>";
+ $string = preg_replace($search, $replace, $string);
+ $i++;
}
- $start_pos = $pos - $context;
-
- if ($start_pos < 0) {
- $start_pos = 0;
+ foreach ($replace_html as $replace => $search) {
+ $string = str_replace($search, $replace, $string);
}
- // get string from -context to +context
- $matched = substr($haystack, $start_pos, $context*2);
+ return $string;
+}
- // add elipses to front.
- if ($start_pos > 0) {
- $matched = "...$matched";
- }
+/**
+ * Returns a query with stop and too short words removed.
+ * (Unless the entire query is < ft_min_word_chars, in which case
+ * it's taken literally.)
+ *
+ * @param array $query
+ * @param str $format Return as an array or a string
+ * @return mixed
+ */
+function search_remove_ignored_words($query, $format = 'array') {
+ global $CONFIG;
- // add elipses to end.
- if ($pos + strlen($needle) + $context*2 < strlen($haystack)) {
- $matched = "$matched...";
+ // don't worry about "s or boolean operators
+ //$query = str_replace(array('"', '-', '+', '~'), '', stripslashes(strip_tags($query)));
+ $query = stripslashes(strip_tags($query));
+
+ $words = explode(' ', $query);
+
+ $min_chars = $CONFIG->search_info['min_chars'];
+ // if > ft_min_word we're not running in literal mode.
+ if (elgg_strlen($query) >= $min_chars) {
+ // clean out any words that are ignored by mysql
+ foreach ($words as $i => $word) {
+ if (elgg_strlen($word) < $min_chars) {
+ unset ($words[$i]);
+ }
+ }
}
- // surround if needed
- // @todo would getting each position of the match then
- // inserting manually based on the position be faster than preg_replace()?
- if ($before || $after) {
- $matched = str_ireplace($needle, $before . $needle . $after, $matched);
- //$matched = mb_ereg_replace("")
- // insert before
+ if ($format == 'string') {
+ return implode(' ', $words);
}
- return $matched;
+ return $words;
}
/**
- * Passes entities, count, and original params to the view functions for
+ * Passes results, and original params to the view functions for
* search type.
*
- * @param array $entities
- * @param int $count
+ * @param array $results
* @param array $params
+ * @param string $view_type = list, entity or layout
* @return string
*/
-function search_get_listing_html($entities, $count, $params) {
- if (!is_array($entities) || !$count) {
- return FALSE;
+function search_get_search_view($params, $view_type) {
+ switch ($view_type) {
+ case 'list':
+ case 'entity':
+ case 'layout':
+ break;
+
+ default:
+ return FALSE;
}
$view_order = array();
- // check if there's a special search view for this type:subtype
+ // check if there's a special search list view for this type:subtype
if (isset($params['type']) && $params['type'] && isset($params['subtype']) && $params['subtype']) {
- $view_order[] = "search/{$params['type']}/{$params['subtype']}/listing";
+ $view_order[] = "search/{$params['type']}/{$params['subtype']}/$view_type";
}
// also check for the default type
if (isset($params['type']) && $params['type']) {
- $view_order[] = "search/{$params['type']}/listing";
+ $view_order[] = "search/{$params['type']}/$view_type";
}
// check search types
if (isset($params['search_type']) && $params['search_type']) {
- $view_order[] = "search/{$params['search_type']}/listing";
+ $view_order[] = "search/{$params['search_type']}/$view_type";
}
- // finally default to a search listing default
- $view_order[] = "search/listing";
-
- $vars = array(
- 'entities' => $entities,
- 'count' => $count,
- 'params' => $params
- );
+ // finally default to a search list default
+ $view_order[] = "search/$view_type";
foreach ($view_order as $view) {
if (elgg_view_exists($view)) {
- return elgg_view($view, $vars);
+ return $view;
}
}
@@ -498,58 +392,111 @@ function search_get_listing_html($entities, $count, $params) {
* @param array $params Original search params
* @return str
*/
-function search_get_where_sql($table, $fields, $params) {
+function search_get_where_sql($table, $fields, $params, $use_fulltext = TRUE) {
global $CONFIG;
$query = $params['query'];
// add the table prefix to the fields
foreach ($fields as $i => $field) {
- $fields[$i] = "$table.$field";
+ if ($table) {
+ $fields[$i] = "$table.$field";
+ }
}
+
+ $where = '';
// if query is shorter than the min for fts words
// it's likely a single acronym or similar
// switch to literal mode
- if (strlen($query) < $CONFIG->search_info['min_chars']) {
+ if (elgg_strlen($query) < $CONFIG->search_info['min_chars']) {
$likes = array();
$query = sanitise_string($query);
foreach ($fields as $field) {
$likes[] = "$field LIKE '%$query%'";
}
$likes_str = implode(' OR ', $likes);
- //$where = "($table.guid = e.guid AND ($likes_str))";
$where = "($likes_str)";
} else {
- // if using advanced or paired "s, switch into boolean mode
- if ((isset($params['advanced_search']) && $params['advanced_search']) || substr_count($query, '"') >= 2 ) {
+ // if we're not using full text, rewrite the query for bool mode.
+ // exploiting a feature(ish) of bool mode where +-word is the same as -word
+ if (!$use_fulltext) {
+ $query = '+' . str_replace(' ', ' +', $query);
+ }
+
+ // if using advanced, boolean operators, or paired "s, switch into boolean mode
+ $booleans_used = preg_match("/([\-\+~])([\w]+)/i", $query);
+ $advanced_search = (isset($params['advanced_search']) && $params['advanced_search']);
+ $quotes_used = (elgg_substr_count($query, '"') >= 2);
+
+ if (!$use_fulltext || $booleans_used || $advanced_search || $quotes_used) {
$options = 'IN BOOLEAN MODE';
} else {
- // natural language mode is default and this keyword isn't supported
- // in < 5.1
+ // natural language mode is default and this keyword isn't supported in < 5.1
//$options = 'IN NATURAL LANGUAGE MODE';
$options = '';
}
-
+
// if short query, use query expansion.
- if (strlen($query) < 6) {
- //$options .= ' WITH QUERY EXPANSION';
- }
+ // @todo doesn't seem to be working well.
+// if (elgg_strlen($query) < 5) {
+// $options .= ' WITH QUERY EXPANSION';
+// }
$query = sanitise_string($query);
- // if query is shorter than the ft_min_word_len switch to literal mode.
$fields_str = implode(',', $fields);
- //$where = "($table.guid = e.guid AND (MATCH ($fields_str) AGAINST ('$query' $options)))";
$where = "(MATCH ($fields_str) AGAINST ('$query' $options))";
}
return $where;
}
-function search_get_query_where_sql($table, $query) {
- // if there are multiple "s or 's it's a literal string.
-}
+/**
+ * Returns ORDER BY sql for insertion into elgg_get_entities().
+ *
+ * @param str $entities_table Prefix for entities table.
+ * @param str $type_table Prefix for the type table.
+ * @param str $sort ORDER BY part
+ * @param str $order ASC or DESC
+ * @return str
+ */
+function search_get_order_by_sql($entities_table, $type_table, $sort, $order) {
-/** Register init system event **/
+ $on = NULL;
-register_elgg_event_handler('init','system','search_init'); \ No newline at end of file
+ switch ($sort) {
+ default:
+ case 'relevance':
+ // default is relevance descending.
+ // ascending relevancy is silly and complicated.
+ $on = '';
+ break;
+ case 'created':
+ $on = "$entities_table.time_created";
+ break;
+ case 'updated':
+ $on = "$entities_table.time_updated";
+ break;
+ case 'action_on':
+ // @todo not supported yet in core
+ $on = '';
+ break;
+ case 'alpha':
+ // @todo not support yet because both title
+ // and name columns are used for this depending
+ // on the entity, which we don't always know. >:O
+ break;
+ }
+ $order = strtolower($order);
+ if ($order != 'asc' && $order != 'desc') {
+ $order = 'DESC';
+ }
+
+ if ($on) {
+ $order_by = "$on $order";
+ } else {
+ $order_by = '';
+ }
+
+ return $order_by;
+}
diff --git a/mod/search/views/default/page_elements/searchbox.php b/mod/search/views/default/page_elements/searchbox.php
deleted file mode 100644
index cfc0b953b..000000000
--- a/mod/search/views/default/page_elements/searchbox.php
+++ /dev/null
@@ -1,4 +0,0 @@
-<form id="searchform" action="<?php echo $vars['url']; ?>pg/search/" method="get">
- <input type="text" size="21" name="q" value="<?php echo elgg_echo('search'); ?>" onclick="if (this.value=='<?php echo elgg_echo('search'); ?>') { this.value='' }" class="search_input" />
- <input type="submit" value="<?php echo elgg_echo('search:go'); ?>" class="search_submit_button" />
-</form>
diff --git a/mod/search/views/default/search/comments/entity.php b/mod/search/views/default/search/comments/entity.php
new file mode 100644
index 000000000..77e950843
--- /dev/null
+++ b/mod/search/views/default/search/comments/entity.php
@@ -0,0 +1,55 @@
+<?php
+/**
+ * Default search view for a comment
+ *
+ * @uses $vars['entity']
+ */
+
+$entity = $vars['entity'];
+$comments_data = $entity->getVolatileData('search_comments_data');
+$comment_data = array_shift($comments_data);
+$entity->setVolatileData('search_comments_data', $comments_data);
+
+$owner = get_entity($comment_data['owner_guid']);
+
+if ($owner instanceof ElggUser) {
+ $icon = elgg_view_entity_icon($owner, 'tiny');
+} else {
+ $icon = '';
+}
+
+// @todo Sometimes we find comments on entities we can't display...
+if ($entity->getVolatileData('search_unavailable_entity')) {
+ $title = elgg_echo('search:comment_on', array(elgg_echo('search:unavailable_entity')));
+ // keep anchor for formatting.
+ $title = "<a>$title</a>";
+} else {
+ if ($entity->getType() == 'object') {
+ $title = $entity->title;
+ } else {
+ $title = $entity->name;
+ }
+
+ if (!$title) {
+ $title = elgg_echo('item:' . $entity->getType() . ':' . $entity->getSubtype());
+ }
+
+ if (!$title) {
+ $title = elgg_echo('item:' . $entity->getType());
+ }
+
+ $title = elgg_echo('search:comment_on', array($title));
+
+ // @todo this should use something like $comment->getURL()
+ $url = $entity->getURL() . '#comment_' . $comment_data['annotation_id'];
+ $title = "<a href=\"$url\">$title</a>";
+}
+
+$description = $comment_data['text'];
+$tc = $comment_data['time_created'];
+$time = elgg_view_friendly_time($tc);
+
+$body = "<p class=\"mbn\">$title</p>$description";
+$body .= "<p class=\"elgg-subtext\">$time</p>";
+
+echo elgg_view_image_block($icon, $body);
diff --git a/mod/search/views/default/search/comments/listing.php b/mod/search/views/default/search/comments/listing.php
deleted file mode 100644
index 58353a110..000000000
--- a/mod/search/views/default/search/comments/listing.php
+++ /dev/null
@@ -1,62 +0,0 @@
-<?php
-/**
- * Elgg comments search listing
- *
- * @package Elgg
- * @subpackage Core
- * @author Curverider Ltd
- * @link http://elgg.org/
- */
-?>
-
-<div class="search_listing">
-<?php
-if (!is_array($vars['entities']) || !count($vars['entities'])) {
- return FALSE;
-}
-
-$title_str = elgg_echo('comments');
-$body = elgg_view_title($title_str);
-
-$query = htmlspecialchars(http_build_query(
- array(
- 'q' => $vars['params']['query'],
- 'entity_type' => $vars['params']['type'],
- 'entity_subtype' => $vars['params']['subtype'],
- 'limit' => get_input('limit', 10),
- 'offset' => get_input('offset', 0),
- 'search_type' => 'comments',
- )
-));
-
-$url = "{$vars['url']}pg/search?$query";
-$more = "<a href=\"$url\">+$count more $title_str</a>";
-
-echo elgg_view('page_elements/contentwrapper', array('body' => $body));
-
-foreach ($vars['entities'] as $entity) {
- if ($owner = $entity->getOwnerEntity()) {
- $owner_icon = $owner->getIcon('tiny');
- $icon = "<img src=\"$owner_icon\" />";
- } else {
- $icon = '';
- }
- $title = "Comment on " . elgg_echo('item:' . $entity->getType() . ':' . $entity->getSubtype());
- $description = $entity->getVolatileData('search_matched_comment');
- $url = $entity->getURL();
- $title = "<a href=\"$url\">$title</a>";
- $tc = $entity->getVolatileData('search_matched_comment_time_created');;
- $time = friendly_time($tc);
-
- echo <<<___END
-<span class="searchListing">
- <h3 class="searchTitle">$title</h3>
- <span class="searchDetails">
- <span class="searchDescription">$description</span><br />
- $icon $time - $more</a>
- </span>
-</span>
-___END;
-}
-?>
-</div>
diff --git a/mod/search/views/default/search/css.php b/mod/search/views/default/search/css.php
index 28dc82a4a..30ff45172 100644
--- a/mod/search/views/default/search/css.php
+++ b/mod/search/views/default/search/css.php
@@ -1,101 +1,67 @@
-.searchtype {
-background: #FFFACD;
-color: black;
-}
-
-.searchtypes {
-border: 1px #EEEEEE solid;
-padding: 4px;
-margin: 6px;
-}
-
-.searchListing {
- display: block;
- margin-bottom: 2em;
-}
-
-.searchMatch {
- background-color: #FFFF66;
-}
-
-.searchMatchColor1 {
- background-color: #FFFF66;
-}
-
-.searchMatchColor2 {
+<?php
+/**
+ * Elgg Search css
+ *
+ */
+?>
+
+/**********************************
+Search plugin
+***********************************/
+.elgg-search-header {
+ bottom: 5px;
+ height: 23px;
+ position: absolute;
+ right: 0;
+}
+.elgg-search input[type=text] {
+ width: 230px;
+}
+.elgg-search input[type=submit] {
+ display: none;
+}
+.elgg-search input[type=text] {
+ -webkit-border-radius: 10px;
+ -moz-border-radius: 10px;
+ border-radius: 10px;
+
+ border: 1px solid #71b9f7;
+ color: white;
+ font-size: 12px;
+ font-weight: bold;
+ padding: 2px 4px 2px 26px;
+ background: transparent url(<?php echo elgg_get_site_url(); ?>_graphics/elgg_sprites.png) no-repeat 2px -934px;
+}
+.elgg-search input[type=text]:focus, .elgg-search input[type=text]:active {
+ background-color: white;
+ background-position: 2px -916px;
+ border: 1px solid white;
+ color: #0054A7;
+}
+
+.search-list li {
+ padding: 5px 0 0;
+}
+.search-heading-category {
+ margin-top: 20px;
+ color: #666666;
+}
+
+.search-highlight {
+ background-color: #bbdaf7;
+}
+.search-highlight-color1 {
+ background-color: #bbdaf7;
+}
+.search-highlight-color2 {
background-color: #A0FFFF;
}
-
-.searchMatchColor3 {
- background-color: #FF9999;
-}
-
-.searchMatchColor4 {
- background-color: #FF66FF;
-}
-
-.searchMatchColor5 {
- background-color: #99FF99;
-}
-
-
-.searchTitle {
- text-decoration: underline;
-}
-
-#searchform input.search_input {
- -webkit-border-radius: 4px;
- -moz-border-radius: 4px;
- background-color:#FFFFFF;
- border:1px solid #BBBBBB;
- color:#999999;
- font-size:12px;
- font-weight:bold;
- margin:0pt;
- padding:2px;
- width:180px;
- height:12px;
-}
-#searchform input.search_submit_button {
- -webkit-border-radius: 4px;
- -moz-border-radius: 4px;
- color:#333333;
- background: #cccccc;
- border:none;
- font-size:12px;
- font-weight:bold;
- margin:0px;
- padding:2px;
- width:auto;
- height:18px;
- cursor:pointer;
+.search-highlight-color3 {
+ background-color: #FDFFC3;
}
-#searchform input.search_submit_button:hover {
- color:#ffffff;
- background: #4690d6;
+.search-highlight-color4 {
+ background-color: #ccc;
}
-
-
-.search_listing {
- display: block;
- -webkit-border-radius: 8px;
- -moz-border-radius: 8px;
- background:white;
- margin:0 10px 5px 10px;
- padding:5px;
-}
-
-.entity_gallery_item .search_listing {
- background: none;
- text-align: center;
+.search-highlight-color5 {
+ background-color: #4690d6;
}
-
-/* override the entity container piece */
-.search_listing .entity_listing {
- -webkit-border-radius: 0px;
- -moz-border-radius: 0px;
- background: transparent;
- margin: 0;
- padding: 0;
-}
-
diff --git a/mod/search/views/default/search/entity.php b/mod/search/views/default/search/entity.php
new file mode 100644
index 000000000..e04d08836
--- /dev/null
+++ b/mod/search/views/default/search/entity.php
@@ -0,0 +1,57 @@
+<?php
+/**
+ * Default view for an entity returned in a search
+ *
+ * Display largely controlled by a set of overrideable volatile data:
+ * - search_icon (defaults to entity icon)
+ * - search_matched_title
+ * - search_matched_description
+ * - search_matched_extra
+ * - search_url (defaults to entity->getURL())
+ * - search_time (defaults to entity->time_updated or entity->time_created)
+ *
+ * @uses $vars['entity'] Entity returned in a search
+ */
+
+$entity = $vars['entity'];
+
+$icon = $entity->getVolatileData('search_icon');
+if (!$icon) {
+ // display the entity's owner by default if available.
+ // @todo allow an option to switch to displaying the entity's icon instead.
+ $type = $entity->getType();
+ if ($type == 'user' || $type == 'group') {
+ $icon = elgg_view_entity_icon($entity, 'tiny');
+ } elseif ($owner = $entity->getOwnerEntity()) {
+ $icon = elgg_view_entity_icon($owner, 'tiny');
+ } else {
+ // display a generic icon if no owner, though there will probably be
+ // other problems if the owner can't be found.
+ $icon = elgg_view_entity_icon($entity, 'tiny');
+ }
+}
+
+$title = $entity->getVolatileData('search_matched_title');
+$description = $entity->getVolatileData('search_matched_description');
+$extra_info = $entity->getVolatileData('search_matched_extra');
+$url = $entity->getVolatileData('search_url');
+
+if (!$url) {
+ $url = $entity->getURL();
+}
+
+$title = "<a href=\"$url\">$title</a>";
+$time = $entity->getVolatileData('search_time');
+if (!$time) {
+ $tc = $entity->time_created;
+ $tu = $entity->time_updated;
+ $time = elgg_view_friendly_time(($tu > $tc) ? $tu : $tc);
+}
+
+$body = "<p class=\"mbn\">$title</p>$description";
+if ($extra_info) {
+ $body .= "<p class=\"elgg-subtext\">$extra_info</p>";
+}
+$body .= "<p class=\"elgg-subtext\">$time</p>";
+
+echo elgg_view_image_block($icon, $body);
diff --git a/mod/search/views/default/search/entity_list.php b/mod/search/views/default/search/entity_list.php
deleted file mode 100644
index d709210df..000000000
--- a/mod/search/views/default/search/entity_list.php
+++ /dev/null
@@ -1,66 +0,0 @@
-<?php
-$context = $vars['context'];
-$offset = $vars['offset'];
-$entities = $vars['entities'];
-$limit = $vars['limit'];
-$count = $vars['count'];
-$baseurl = $vars['baseurl'];
-$context = $vars['context'];
-$viewtype = $vars['viewtype'];
-$pagination = $vars['pagination'];
-$fullview = $vars['fullview'];
-
-$html = "";
-$nav = "";
-if (isset($vars['viewtypetoggle'])) {
- $viewtypetoggle = $vars['viewtypetoggle'];
-} else {
- $viewtypetoggle = true;
-}
-
-if ($context == "search" && $count > 0 && $viewtypetoggle) {
- $nav .= elgg_view("navigation/viewtype",array(
-
- 'baseurl' => $baseurl,
- 'offset' => $offset,
- 'count' => $count,
- 'viewtype' => $viewtype,
-
- ));
-}
-
-if ($pagination)
- $nav .= elgg_view('navigation/pagination',array(
-
- 'baseurl' => $baseurl,
- 'offset' => $offset,
- 'count' => $count,
- 'limit' => $limit,
-
- ));
-
-$html .= $nav;
-
-if ($viewtype == "list") {
- if (is_array($entities) && sizeof($entities) > 0) {
- foreach($entities as $entity) {
- // print out the entity
- $ev = elgg_view_entity($entity, $fullview);
- // then add the search decorations around it
- $html .= elgg_view('search/listing', array('entity_view' => $ev,
- 'search_types' => $entity->getVolatileData('search')));
-
- }
- }
-} else if ($viewtype == "gallery") {
- if (is_array($entities) && sizeof($entities) > 0) {
- $html .= elgg_view("search/gallery",array('entities' => $entities));
- }
-}
-
-if ($count) {
- $html .= $nav;
-}
-echo $html;
-
-?>
diff --git a/mod/search/views/default/search/gallery.php b/mod/search/views/default/search/gallery.php
deleted file mode 100644
index 753a38684..000000000
--- a/mod/search/views/default/search/gallery.php
+++ /dev/null
@@ -1,55 +0,0 @@
-<?php
-
- /**
- * Elgg gallery view
- *
- * @package Elgg
- * @subpackage Core
-
- * @author Curverider Ltd
-
- * @link http://elgg.org/
- */
-
- $entities = $vars['entities'];
- if (is_array($entities) && sizeof($entities) > 0) {
-
-?>
-
- <table class="entity_gallery">
-
-<?php
-
- $col = 0;
- foreach($entities as $entity) {
- if ($col == 0) {
-
- echo "<tr>";
-
- }
- echo "<td class=\"entity_gallery_item\">";
-
- $ev = elgg_view_entity($entity, $fullview);
-
- echo elgg_view('search/listing', array('entity_view' => $ev,
- 'search_types' => $entity->getVolatileData('search')));
-
-
- echo "</td>";
- $col++;
- if ($col > 3) {
- echo "</tr>";
- $col = 0;
- }
- }
- if ($col > 0) echo "</tr>";
-
-?>
-
- </table>
-
-<?php
-
- }
-
-?> \ No newline at end of file
diff --git a/mod/search/views/default/search/gallery_listing.php b/mod/search/views/default/search/gallery_listing.php
deleted file mode 100644
index bbecaf202..000000000
--- a/mod/search/views/default/search/gallery_listing.php
+++ /dev/null
@@ -1,16 +0,0 @@
-<?php
-/**
- * Elgg search listing: gallery view
- *
- * DEPRECATED VIEW: use entities/gallery_listing instead
- *
- * @package Elgg
- * @subpackage Core
- * @author Curverider Ltd
- * @link http://elgg.org/
- */
-
-
- echo elgg_view('entities/gallery_listing', $vars);
-
-?> \ No newline at end of file
diff --git a/mod/search/views/default/search/header.php b/mod/search/views/default/search/header.php
new file mode 100644
index 000000000..6f8654c13
--- /dev/null
+++ b/mod/search/views/default/search/header.php
@@ -0,0 +1,6 @@
+<?php
+/**
+ * Search box in page header
+ */
+
+echo elgg_view('search/search_box', array('class' => 'elgg-search-header')); \ No newline at end of file
diff --git a/mod/search/views/default/search/layout.php b/mod/search/views/default/search/layout.php
new file mode 100644
index 000000000..eb597836e
--- /dev/null
+++ b/mod/search/views/default/search/layout.php
@@ -0,0 +1,8 @@
+<?php
+/**
+ * The default search layout
+ *
+ * @uses $vars['body']
+ */
+
+echo elgg_view_layout('one_sidebar', array('content' => $vars['body'])); \ No newline at end of file
diff --git a/mod/search/views/default/search/list.php b/mod/search/views/default/search/list.php
new file mode 100644
index 000000000..90aa28989
--- /dev/null
+++ b/mod/search/views/default/search/list.php
@@ -0,0 +1,115 @@
+<?php
+/**
+ * List a section of search results corresponding in a particular type/subtype
+ * or search type (comments for example)
+ *
+ * @uses $vars['results'] Array of data related to search results including:
+ * - 'entities' Array of entities to be displayed
+ * - 'count' Total number of results
+ * @uses $vars['params'] Array of parameters including:
+ * - 'type' Entity type
+ * - 'subtype' Entity subtype
+ * - 'search_type' Type of search: 'entities', 'comments', 'tags'
+ * - 'offset' Offset in search results
+ * - 'limit' Number of results per page
+ * - 'pagination' Display pagination?
+ */
+
+$entities = $vars['results']['entities'];
+$count = $vars['results']['count'] - count($entities);
+
+if (!is_array($entities) || !count($entities)) {
+ return FALSE;
+}
+
+$query = http_build_query(
+ array(
+ 'q' => $vars['params']['query'],
+ 'entity_type' => $vars['params']['type'],
+ 'entity_subtype' => $vars['params']['subtype'],
+ 'limit' => $vars['params']['limit'],
+ 'offset' => $vars['params']['offset'],
+ 'search_type' => $vars['params']['search_type'],
+ //@todo include vars for sorting, order, and friend-only.
+ )
+);
+
+$url = elgg_get_site_url() . "search?$query";
+
+$more_items = $vars['results']['count'] - ($vars['params']['offset'] + $vars['params']['limit']);
+
+// get pagination
+if (array_key_exists('pagination', $vars['params']) && $vars['params']['pagination']) {
+ $nav = elgg_view('navigation/pagination', array(
+ 'base_url' => $url,
+ 'offset' => $vars['params']['offset'],
+ 'count' => $vars['results']['count'],
+ 'limit' => $vars['params']['limit'],
+ ));
+ $show_more = false;
+} else {
+ // faceted search page so no pagination
+ $nav = '';
+ $show_more = $more_items > 0;
+}
+
+// figure out what we're dealing with.
+$type_str = NULL;
+
+if (array_key_exists('type', $vars['params']) && array_key_exists('subtype', $vars['params'])) {
+ $type_str_tmp = "item:{$vars['params']['type']}:{$vars['params']['subtype']}";
+ $type_str_echoed = elgg_echo($type_str_tmp);
+ if ($type_str_echoed != $type_str_tmp) {
+ $type_str = $type_str_echoed;
+ }
+}
+
+if (!$type_str && array_key_exists('type', $vars['params'])) {
+ $type_str = elgg_echo("item:{$vars['params']['type']}");
+}
+
+if (!$type_str) {
+ $type_str = elgg_echo('search:unknown_entity');
+}
+
+// allow overrides for titles
+$search_type_str = elgg_echo("search_types:{$vars['params']['search_type']}");
+if (array_key_exists('search_type', $vars['params'])
+ && $search_type_str != "search_types:{$vars['params']['search_type']}") {
+
+ $type_str = $search_type_str;
+}
+
+if ($show_more) {
+ $more_str = elgg_echo('search:more', array($count, $type_str));
+ $more_url = elgg_http_remove_url_query_element($url, 'limit');
+ $more_link = "<li class='elgg-item'><a href=\"$more_url\">$more_str</a></li>";
+} else {
+ $more_link = '';
+}
+
+// @todo once elgg_view_title() supports passing a $vars array use it
+$body = elgg_view('page/elements/title', array(
+ 'title' => $type_str,
+ 'class' => 'search-heading-category',
+));
+
+$view = search_get_search_view($vars['params'], 'entity');
+if ($view) {
+ $body .= '<ul class="elgg-list search-list">';
+ foreach ($entities as $entity) {
+ $id = "elgg-{$entity->getType()}-{$entity->getGUID()}";
+ $body .= "<li id=\"$id\" class=\"elgg-item\">";
+ $body .= elgg_view($view, array(
+ 'entity' => $entity,
+ 'params' => $vars['params'],
+ 'results' => $vars['results']
+ ));
+ $body .= '</li>';
+ }
+ $body .= $more_link;
+ $body .= '</ul>';
+}
+
+echo $body;
+echo $nav;
diff --git a/mod/search/views/default/search/listing.php b/mod/search/views/default/search/listing.php
deleted file mode 100644
index 37850c911..000000000
--- a/mod/search/views/default/search/listing.php
+++ /dev/null
@@ -1,68 +0,0 @@
-<?php
-/**
- * Elgg search listing
- *
- * @package Elgg
- * @subpackage Core
- * @author Curverider Ltd
- * @link http://elgg.org/
- */
-?>
-
-<div class="search_listing">
-
-<?php
-$entities = $vars['entities'];
-$count = $vars['count'] - count($vars['entities']);
-
-if (!is_array($vars['entities']) || !count($vars['entities'])) {
- return FALSE;
-}
-
-$title_str = elgg_echo("item:{$vars['params']['type']}:{$vars['params']['subtype']}");
-$body = elgg_view_title($title_str);
-
-$query = htmlspecialchars(http_build_query(
- array(
- 'q' => $vars['params']['query'],
- 'entity_type' => $vars['params']['type'],
- 'entity_subtype' => $vars['params']['subtype'],
- 'limit' => get_input('limit', 10),
- 'offset' => get_input('offset', 0),
- 'search_type' => 'entities',
- )
-));
-
-$url = "{$vars['url']}pg/search?$query";
-$more = "<a href=\"$url\">+$count more $title_str</a>";
-
-echo elgg_view('page_elements/contentwrapper', array('body' => $body));
-
-foreach ($entities as $entity) {
- if ($owner = $entity->getOwnerEntity()) {
- $owner_icon = $owner->getIcon('tiny');
- $icon = "<img src=\"$owner_icon\" />";
- } else {
- $icon = '';
- }
- $title = $entity->getVolatileData('search_matched_title');
- $description = $entity->getVolatileData('search_matched_description');
- $url = $entity->getURL();
- $title = "<a href=\"$url\">$title</a>";
- $tc = $entity->time_created;
- $tu = $entity->time_updated;
- $time = friendly_time(($tu > $tc) ? $tu : $tc);
-
- echo <<<___END
-<span class="searchListing">
- <h3 class="searchTitle">$title</h3>
- <span class="searchDetails">
- <span class="searchDescription">$description</span><br />
- $icon $time - $more</a>
- </span>
-</span>
-___END;
-}
-
-?>
-</div> \ No newline at end of file
diff --git a/mod/search/views/default/search/no_results.php b/mod/search/views/default/search/no_results.php
new file mode 100644
index 000000000..0e9a5e295
--- /dev/null
+++ b/mod/search/views/default/search/no_results.php
@@ -0,0 +1,6 @@
+<?php
+/**
+ * No results from search
+ */
+
+echo elgg_autop(elgg_echo('search:no_results'));
diff --git a/mod/search/views/default/search/search_box.php b/mod/search/views/default/search/search_box.php
new file mode 100644
index 000000000..7474a280c
--- /dev/null
+++ b/mod/search/views/default/search/search_box.php
@@ -0,0 +1,43 @@
+<?php
+/**
+ * Search box
+ *
+ * @uses $vars['value'] Current search query
+ * @uses $vars['class'] Additional class
+ */
+
+if (array_key_exists('value', $vars)) {
+ $value = $vars['value'];
+} elseif ($value = get_input('q', get_input('tag', NULL))) {
+ $value = $value;
+} else {
+ $value = elgg_echo('search');
+}
+
+$class = "elgg-search";
+if (isset($vars['class'])) {
+ $class = "$class {$vars['class']}";
+}
+
+// @todo - why the strip slashes?
+$value = stripslashes($value);
+
+// @todo - create function for sanitization of strings for display in 1.8
+// encode <,>,&, quotes and characters above 127
+if (function_exists('mb_convert_encoding')) {
+ $display_query = mb_convert_encoding($value, 'HTML-ENTITIES', 'UTF-8');
+} else {
+ // if no mbstring extension, we just strip characters
+ $display_query = preg_replace("/[^\x01-\x7F]/", "", $value);
+}
+$display_query = htmlspecialchars($display_query, ENT_QUOTES, 'UTF-8', false);
+
+?>
+
+<form class="<?php echo $class; ?>" action="<?php echo elgg_get_site_url(); ?>search" method="get">
+ <fieldset>
+ <input type="text" class="search-input" size="21" name="q" value="<?php echo $display_query; ?>" onblur="if (this.value=='') { this.value='<?php echo elgg_echo('search'); ?>' }" onfocus="if (this.value=='<?php echo elgg_echo('search'); ?>') { this.value='' };" />
+ <input type="hidden" name="search_type" value="all" />
+ <input type="submit" value="<?php echo elgg_echo('search:go'); ?>" class="search-submit-button" />
+ </fieldset>
+</form>
diff --git a/mod/search/views/default/search/startblurb.php b/mod/search/views/default/search/startblurb.php
index 82d2d62d1..d6394da0d 100644
--- a/mod/search/views/default/search/startblurb.php
+++ b/mod/search/views/default/search/startblurb.php
@@ -1,13 +1,6 @@
<?php
/**
- * @package Elgg
- * @subpackage Core
- * @author Curverider Ltd
- * @link http://elgg.org/
+ * @todo This doesn't appear to be called by anything. Look into removing.
*/
-?>
-<div class="contentWrapper">
-<?php
- echo sprintf(elgg_echo("tag:search:startblurb"), $vars['query']);
-?>
-</div> \ No newline at end of file
+
+echo elgg_echo("tag:search:startblurb", array($vars['query']));
diff --git a/mod/search/views/default/search/tags/listing.php b/mod/search/views/default/search/tags/listing.php
deleted file mode 100644
index 9b229b349..000000000
--- a/mod/search/views/default/search/tags/listing.php
+++ /dev/null
@@ -1,55 +0,0 @@
-<?php
-/**
- * Elgg tag search listing
- *
- * @package Elgg
- * @subpackage Core
- * @author Curverider Ltd
- * @link http://elgg.org/
- */
-?>
-
-<div class="search_listing">
-
-<?php
-$entities = $vars['entities'];
-$count = $vars['count'];
-
-if (!is_array($vars['entities']) || !count($vars['entities'])) {
- return FALSE;
-}
-
-$title_str = elgg_echo("item:{$vars['params']['type']}:{$vars['params']['subtype']}");
-$body = elgg_view_title(elgg_echo('tags'));
-
-echo elgg_view('page_elements/contentwrapper', array('body' => $body));
-
-foreach ($entities as $entity) {
- if ($owner = $entity->getOwnerEntity()) {
- $owner_icon = $owner->getIcon('tiny');
- $icon = "<img src=\"$owner_icon\" />";
- } else {
- $icon = '';
- }
- $tags = $entity->getVolatileData('search_matched_tags');
-
- $entity_html = elgg_view_entity($entity);
- $url = $entity->getURL();
- $title = "<a href=\"$url\">$title</a>";
- $tc = $entity->time_created;
- $tu = $entity->time_updated;
- $time = friendly_time(($tu > $tc) ? $tu : $tc);
-
- echo <<<___END
-<span class="searchListing">
- <h3 class="searchTitle">$title</h3>
- <span class="searchDetails">
- $entity_html
- $tags
- </span>
-</span>
-___END;
-}
-
-?>
-</div> \ No newline at end of file
diff --git a/mod/search/views/rss/search/comments/entity.php b/mod/search/views/rss/search/comments/entity.php
new file mode 100644
index 000000000..e47afec4a
--- /dev/null
+++ b/mod/search/views/rss/search/comments/entity.php
@@ -0,0 +1,54 @@
+<?php
+/**
+ * Search comment view for RSS feeds.
+ *
+ * @uses $vars['entity']
+ */
+
+$entity = $vars['entity'];
+$comments_data = $entity->getVolatileData('search_comments_data');
+$comment_data = array_shift($comments_data);
+$entity->setVolatileData('search_comments_data', $comments_data);
+
+$author_name = '';
+$comment_author_guid = $comment_data['owner_guid'];
+$author = get_user($comment_author_guid);
+if ($author) {
+ $author_name = $author->name;
+}
+
+// @todo Sometimes we find comments on entities we can't display...
+if ($entity->getVolatileData('search_unavailable_entity')) {
+ $title = elgg_echo('search:comment_on', array(elgg_echo('search:unavailable_entity')));
+} else {
+ if ($entity->getType() == 'object') {
+ $title = $entity->title;
+ } else {
+ $title = $entity->name;
+ }
+
+ if (!$title) {
+ $title = elgg_echo('item:' . $entity->getType() . ':' . $entity->getSubtype());
+ }
+
+ if (!$title) {
+ $title = elgg_echo('item:' . $entity->getType());
+ }
+
+ $title = elgg_echo('search:comment_on', array($title));
+ $title .= ' ' . elgg_echo('search:comment_by') . ' ' . $author_name;
+ $url = $entity->getURL() . '#annotation-' . $comment_data['annotation_id'];
+}
+
+$description = $comment_data['text'];
+$tc = $comment_data['time_created'];
+
+?>
+
+<item>
+ <guid isPermaLink='true'><?php echo htmlspecialchars($url); ?></guid>
+ <pubDate><?php echo date("r", $tc) ?></pubDate>
+ <link><?php echo htmlspecialchars($url); ?></link>
+ <title><![CDATA[<?php echo $title; ?>]]></title>
+ <description><![CDATA[<?php echo $description; ?>]]></description>
+</item>
diff --git a/mod/search/views/rss/search/entity.php b/mod/search/views/rss/search/entity.php
new file mode 100644
index 000000000..10d28e8e1
--- /dev/null
+++ b/mod/search/views/rss/search/entity.php
@@ -0,0 +1,24 @@
+<?php
+/**
+ * Search entity view for RSS feeds.
+ *
+ * @uses $vars['entity']
+ */
+
+if (!array_key_exists('entity', $vars) || !($vars['entity'] instanceof ElggEntity)) {
+ return FALSE;
+}
+
+// title cannot contain HTML but descriptions can.
+$title = strip_tags($vars['entity']->getVolatileData('search_matched_title'));
+$description = $vars['entity']->getVolatileData('search_matched_description');
+
+?>
+
+<item>
+ <guid isPermaLink='true'><?php echo htmlspecialchars($vars['entity']->getURL()); ?></guid>
+ <pubDate><?php echo date("r", $vars['entity']->time_created) ?></pubDate>
+ <link><?php echo htmlspecialchars($vars['entity']->getURL()); ?></link>
+ <title><![CDATA[<?php echo $title; ?>]]></title>
+ <description><![CDATA[<?php echo $description; ?>]]></description>
+</item>
diff --git a/mod/search/views/rss/search/layout.php b/mod/search/views/rss/search/layout.php
new file mode 100644
index 000000000..2c255a9cc
--- /dev/null
+++ b/mod/search/views/rss/search/layout.php
@@ -0,0 +1,6 @@
+<?php
+/**
+ * Search layout for RSS
+ */
+
+echo $vars['body'];
diff --git a/mod/search/views/rss/search/list.php b/mod/search/views/rss/search/list.php
new file mode 100644
index 000000000..32082fd31
--- /dev/null
+++ b/mod/search/views/rss/search/list.php
@@ -0,0 +1,25 @@
+<?php
+/**
+ * List a section of search results for RSS feeds.
+ *
+ * @uses $vars['results']
+ * @uses $vars['params']
+ */
+
+$entities = $vars['results']['entities'];
+
+if (!is_array($entities) || !count($entities)) {
+ return FALSE;
+}
+
+foreach ($entities as $entity) {
+ if ($view = search_get_search_view($vars['params'], 'entity')) {
+ $body .= elgg_view($view, array(
+ 'entity' => $entity,
+ 'params' => $vars['params'],
+ 'results' => $vars['results']
+ ));
+ }
+}
+
+echo $body; \ No newline at end of file